@reconcrap/boss-recommend-mcp 2.1.22 → 2.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/bin/boss-recommend-mcp.js +4 -4
- package/package.json +14 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +43 -42
- package/skills/boss-chat/SKILL.md +107 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +47 -47
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +301 -127
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +291 -114
- package/src/core/capture/index.js +2930 -1201
- package/src/core/cv-acquisition/index.js +512 -238
- package/src/core/cv-capture-target/index.js +513 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +11 -2
- package/src/core/reporting/legacy-csv.js +12 -12
- package/src/core/run/detached-launcher.js +305 -0
- package/src/core/run/index.js +112 -42
- package/src/core/run/timing.js +33 -33
- package/src/core/run/windows-detached-worker.ps1 +106 -0
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +989 -973
- package/src/core/self-heal/viewport.js +1505 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/action-journal.js +443 -0
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +1684 -401
- package/src/domains/chat/index.js +8 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +157 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1801 -760
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +515 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +92 -92
- package/src/domains/recommend/detail.js +12 -3
- package/src/domains/recommend/filters.js +611 -611
- package/src/domains/recommend/index.js +9 -9
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -736
- package/src/domains/recommend/refresh.js +410 -329
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +1783 -592
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1817 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +124 -5
- package/src/parser.js +1296 -1296
- package/src/recommend-mcp.js +515 -80
- package/src/recommend-scheduler.js +66 -0
|
@@ -12,17 +12,17 @@ import {
|
|
|
12
12
|
querySelectorAll,
|
|
13
13
|
sleep
|
|
14
14
|
} from "../../core/browser/index.js";
|
|
15
|
-
import {
|
|
16
|
-
buildScreeningCandidateFromDetail,
|
|
17
|
-
htmlToText
|
|
18
|
-
} from "../../core/screening/index.js";
|
|
19
|
-
import {
|
|
20
|
-
closeBossAccountRightsBlockingPanel,
|
|
21
|
-
findBossAccountRightsBlockingPanel
|
|
22
|
-
} from "../common/account-rights-panel.js";
|
|
23
|
-
import {
|
|
24
|
-
CHAT_ACTIVE_CANDIDATE_SELECTORS,
|
|
25
|
-
CHAT_ASK_RESUME_BUTTON_SELECTORS,
|
|
15
|
+
import {
|
|
16
|
+
buildScreeningCandidateFromDetail,
|
|
17
|
+
htmlToText
|
|
18
|
+
} from "../../core/screening/index.js";
|
|
19
|
+
import {
|
|
20
|
+
closeBossAccountRightsBlockingPanel,
|
|
21
|
+
findBossAccountRightsBlockingPanel
|
|
22
|
+
} from "../common/account-rights-panel.js";
|
|
23
|
+
import {
|
|
24
|
+
CHAT_ACTIVE_CANDIDATE_SELECTORS,
|
|
25
|
+
CHAT_ASK_RESUME_BUTTON_SELECTORS,
|
|
26
26
|
CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
|
|
27
27
|
CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
|
|
28
28
|
CHAT_BLOCKING_PANEL_TEXT_QUERIES,
|
|
@@ -44,15 +44,16 @@ import {
|
|
|
44
44
|
getChatRoots,
|
|
45
45
|
queryFirstAcrossChatRoots
|
|
46
46
|
} from "./roots.js";
|
|
47
|
-
import {
|
|
48
|
-
assertChatShellNotResumeTopLevel,
|
|
49
|
-
getChatTopLevelState,
|
|
50
|
-
isForbiddenChatResumeTopLevelUrl,
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
import {
|
|
48
|
+
assertChatShellNotResumeTopLevel,
|
|
49
|
+
getChatTopLevelState,
|
|
50
|
+
isForbiddenChatResumeTopLevelUrl,
|
|
51
|
+
makeBossSecurityVerificationRequiredError,
|
|
52
|
+
makeForbiddenChatResumeNavigationError
|
|
53
|
+
} from "./page-guard.js";
|
|
53
54
|
|
|
54
|
-
export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
|
|
55
|
-
export const CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE = "CHAT_ONLINE_RESUME_MODAL_NOT_OPEN";
|
|
55
|
+
export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
|
|
56
|
+
export const CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE = "CHAT_ONLINE_RESUME_MODAL_NOT_OPEN";
|
|
56
57
|
|
|
57
58
|
const CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS = Object.freeze([
|
|
58
59
|
".conversation-main",
|
|
@@ -68,7 +69,7 @@ const CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS = Object.freeze([
|
|
|
68
69
|
".geek-detail-modal"
|
|
69
70
|
]);
|
|
70
71
|
|
|
71
|
-
const CHAT_REQUESTED_RESUME_SCOPE_SELECTORS = Object.freeze([
|
|
72
|
+
const CHAT_REQUESTED_RESUME_SCOPE_SELECTORS = Object.freeze([
|
|
72
73
|
".chat-message-list",
|
|
73
74
|
".conversation-editor",
|
|
74
75
|
".conversation-main",
|
|
@@ -78,8 +79,18 @@ const CHAT_REQUESTED_RESUME_SCOPE_SELECTORS = Object.freeze([
|
|
|
78
79
|
".exchange-tooltip",
|
|
79
80
|
".boss-popup__wrapper",
|
|
80
81
|
".boss-dialog",
|
|
81
|
-
".dialog-wrap.active"
|
|
82
|
-
]);
|
|
82
|
+
".dialog-wrap.active"
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const CHAT_REQUEST_RESUME_CONFIRM_PROMPT = "确定向牛人索取简历吗?";
|
|
86
|
+
const CHAT_REQUEST_RESUME_CONFIRM_SCOPE_SELECTORS = Object.freeze([
|
|
87
|
+
".exchange-tooltip",
|
|
88
|
+
".boss-popup__wrapper",
|
|
89
|
+
".boss-dialog",
|
|
90
|
+
".dialog-wrap.active"
|
|
91
|
+
]);
|
|
92
|
+
const CHAT_REQUEST_RESUME_CONFIRM_DEFAULT_TIMEOUT_MS = 20000;
|
|
93
|
+
const CHAT_REQUEST_RESUME_CONFIRM_POLL_INTERVAL_MS = 250;
|
|
83
94
|
|
|
84
95
|
export function matchesChatProfileNetwork(url) {
|
|
85
96
|
return CHAT_PROFILE_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
|
|
@@ -129,15 +140,15 @@ export function makeUnsafeChatOnlineResumeLinkError(target = {}, buttonHTML = ""
|
|
|
129
140
|
return error;
|
|
130
141
|
}
|
|
131
142
|
|
|
132
|
-
export function isUnsafeChatOnlineResumeLinkError(error) {
|
|
133
|
-
return error?.code === CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE
|
|
134
|
-
|| /CHAT_UNSAFE_ONLINE_RESUME_LINK/i.test(String(error?.message || error || ""));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function isChatOnlineResumeModalOpenFailureError(error) {
|
|
138
|
-
return error?.code === CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE
|
|
139
|
-
|| /Chat online resume modal did not open/i.test(String(error?.message || error || ""));
|
|
140
|
-
}
|
|
143
|
+
export function isUnsafeChatOnlineResumeLinkError(error) {
|
|
144
|
+
return error?.code === CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE
|
|
145
|
+
|| /CHAT_UNSAFE_ONLINE_RESUME_LINK/i.test(String(error?.message || error || ""));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function isChatOnlineResumeModalOpenFailureError(error) {
|
|
149
|
+
return error?.code === CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE
|
|
150
|
+
|| /Chat online resume modal did not open/i.test(String(error?.message || error || ""));
|
|
151
|
+
}
|
|
141
152
|
|
|
142
153
|
export function createChatProfileNetworkRecorder(client) {
|
|
143
154
|
const events = [];
|
|
@@ -248,9 +259,9 @@ function chatCandidateIdFromAttributes(attributes = {}) {
|
|
|
248
259
|
);
|
|
249
260
|
}
|
|
250
261
|
|
|
251
|
-
async function hydrateActiveChatCandidate(client, activeCandidate = null) {
|
|
252
|
-
if (!activeCandidate?.node_id) return activeCandidate;
|
|
253
|
-
let attributes = {};
|
|
262
|
+
async function hydrateActiveChatCandidate(client, activeCandidate = null) {
|
|
263
|
+
if (!activeCandidate?.node_id) return activeCandidate;
|
|
264
|
+
let attributes = {};
|
|
254
265
|
let outerHTML = "";
|
|
255
266
|
try {
|
|
256
267
|
[attributes, outerHTML] = await Promise.all([
|
|
@@ -264,25 +275,25 @@ async function hydrateActiveChatCandidate(client, activeCandidate = null) {
|
|
|
264
275
|
candidate_id: chatCandidateIdFromAttributes(attributes) || null,
|
|
265
276
|
label: normalizeDetailText(htmlToText(outerHTML)),
|
|
266
277
|
outer_html_length: outerHTML.length
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export async function readChatActiveCandidateState(client) {
|
|
271
|
-
const rootState = await getChatRoots(client);
|
|
272
|
-
const activeCandidate = await queryFirstAcrossChatRoots(
|
|
273
|
-
client,
|
|
274
|
-
rootState.roots,
|
|
275
|
-
CHAT_ACTIVE_CANDIDATE_SELECTORS
|
|
276
|
-
);
|
|
277
|
-
return {
|
|
278
|
-
roots: rootState.roots,
|
|
279
|
-
active_candidate: await hydrateActiveChatCandidate(client, activeCandidate)
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export async function waitForChatOnlineResumeButton(client, {
|
|
284
|
-
timeoutMs = 12000,
|
|
285
|
-
intervalMs = 250,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function readChatActiveCandidateState(client) {
|
|
282
|
+
const rootState = await getChatRoots(client);
|
|
283
|
+
const activeCandidate = await queryFirstAcrossChatRoots(
|
|
284
|
+
client,
|
|
285
|
+
rootState.roots,
|
|
286
|
+
CHAT_ACTIVE_CANDIDATE_SELECTORS
|
|
287
|
+
);
|
|
288
|
+
return {
|
|
289
|
+
roots: rootState.roots,
|
|
290
|
+
active_candidate: await hydrateActiveChatCandidate(client, activeCandidate)
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function waitForChatOnlineResumeButton(client, {
|
|
295
|
+
timeoutMs = 12000,
|
|
296
|
+
intervalMs = 250,
|
|
286
297
|
expectedCandidateId = ""
|
|
287
298
|
} = {}) {
|
|
288
299
|
const started = Date.now();
|
|
@@ -389,16 +400,15 @@ function isRequestedResumeText(text = "") {
|
|
|
389
400
|
);
|
|
390
401
|
}
|
|
391
402
|
|
|
392
|
-
function isResumeRequestSentMessageText(text = "") {
|
|
393
|
-
const normalized = normalizeDetailText(text);
|
|
394
|
-
return Boolean(
|
|
395
|
-
normalized.includes("简历请求已发送")
|
|
396
|
-
|| normalized.includes("已发送简历")
|
|
397
|
-
|| normalized.includes("已求简历")
|
|
398
|
-
|| normalized.includes("已索要简历")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
403
|
+
function isResumeRequestSentMessageText(text = "") {
|
|
404
|
+
const normalized = normalizeDetailText(text);
|
|
405
|
+
return Boolean(
|
|
406
|
+
normalized.includes("简历请求已发送")
|
|
407
|
+
|| normalized.includes("已发送简历")
|
|
408
|
+
|| normalized.includes("已求简历")
|
|
409
|
+
|| normalized.includes("已索要简历")
|
|
410
|
+
);
|
|
411
|
+
}
|
|
402
412
|
|
|
403
413
|
function countTextOccurrences(text = "", needle = "") {
|
|
404
414
|
if (!needle) return 0;
|
|
@@ -413,11 +423,11 @@ function countTextOccurrences(text = "", needle = "") {
|
|
|
413
423
|
return count;
|
|
414
424
|
}
|
|
415
425
|
|
|
416
|
-
function countResumeRequestSentMessageMarkers(lines = []) {
|
|
417
|
-
const markers = ["简历请求已发送", "已发送简历", "已求简历", "已索要简历"
|
|
418
|
-
return lines.reduce((total, line) => (
|
|
419
|
-
total + markers.reduce((lineTotal, marker) => (
|
|
420
|
-
lineTotal + countTextOccurrences(line, marker)
|
|
426
|
+
function countResumeRequestSentMessageMarkers(lines = []) {
|
|
427
|
+
const markers = ["简历请求已发送", "已发送简历", "已求简历", "已索要简历"];
|
|
428
|
+
return lines.reduce((total, line) => (
|
|
429
|
+
total + markers.reduce((lineTotal, marker) => (
|
|
430
|
+
lineTotal + countTextOccurrences(line, marker)
|
|
421
431
|
), 0)
|
|
422
432
|
), 0);
|
|
423
433
|
}
|
|
@@ -463,17 +473,14 @@ function isAttachmentResumeTarget(target = {}) {
|
|
|
463
473
|
|| /resume-btn-file/i.test(String(target.attributes?.class || target.selector || ""));
|
|
464
474
|
}
|
|
465
475
|
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|| normalized.includes("确认")
|
|
475
|
-
);
|
|
476
|
-
}
|
|
476
|
+
function isRequestResumeConfirmPrompt(text = "") {
|
|
477
|
+
return normalizeDetailText(text).includes(CHAT_REQUEST_RESUME_CONFIRM_PROMPT);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function isExactRequestResumeConfirmText(text = "") {
|
|
481
|
+
const normalized = normalizeDetailText(text);
|
|
482
|
+
return normalized === "确定" || normalized === "确认";
|
|
483
|
+
}
|
|
477
484
|
|
|
478
485
|
function isSendText(text = "") {
|
|
479
486
|
const normalized = normalizeDetailText(text);
|
|
@@ -534,9 +541,9 @@ async function findVisibleMatchingTarget(client, roots, selectors, predicate) {
|
|
|
534
541
|
return null;
|
|
535
542
|
}
|
|
536
543
|
|
|
537
|
-
async function resolveScopedRoots(client, roots = [], selectors = [], {
|
|
538
|
-
fallbackToRoots = true
|
|
539
|
-
} = {}) {
|
|
544
|
+
async function resolveScopedRoots(client, roots = [], selectors = [], {
|
|
545
|
+
fallbackToRoots = true
|
|
546
|
+
} = {}) {
|
|
540
547
|
const scoped = [];
|
|
541
548
|
const seen = new Set();
|
|
542
549
|
for (const root of roots) {
|
|
@@ -559,9 +566,56 @@ async function resolveScopedRoots(client, roots = [], selectors = [], {
|
|
|
559
566
|
}
|
|
560
567
|
}
|
|
561
568
|
}
|
|
562
|
-
if (scoped.length || !fallbackToRoots) return scoped;
|
|
563
|
-
return roots;
|
|
564
|
-
}
|
|
569
|
+
if (scoped.length || !fallbackToRoots) return scoped;
|
|
570
|
+
return roots;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function findAnchoredChatRequestResumeConfirmTarget(client, roots = []) {
|
|
574
|
+
const promptRoots = await resolveScopedRoots(
|
|
575
|
+
client,
|
|
576
|
+
roots,
|
|
577
|
+
CHAT_REQUEST_RESUME_CONFIRM_SCOPE_SELECTORS,
|
|
578
|
+
{ fallbackToRoots: false }
|
|
579
|
+
);
|
|
580
|
+
let lastPrompt = null;
|
|
581
|
+
let lastPromptCandidate = null;
|
|
582
|
+
let lastTargetCandidate = null;
|
|
583
|
+
for (const promptRoot of promptRoots) {
|
|
584
|
+
const prompt = await readTarget(
|
|
585
|
+
client,
|
|
586
|
+
promptRoot,
|
|
587
|
+
"request-resume-confirm-prompt",
|
|
588
|
+
promptRoot.nodeId
|
|
589
|
+
);
|
|
590
|
+
lastPromptCandidate = prompt;
|
|
591
|
+
if (!prompt.visible || !isRequestResumeConfirmPrompt(prompt.label)) continue;
|
|
592
|
+
lastPrompt = prompt;
|
|
593
|
+
for (const selector of CHAT_CONFIRM_REQUEST_RESUME_SELECTORS) {
|
|
594
|
+
const nodeIds = await querySelectorAll(client, promptRoot.nodeId, selector);
|
|
595
|
+
for (const nodeId of nodeIds) {
|
|
596
|
+
const target = await readTarget(client, promptRoot, selector, nodeId);
|
|
597
|
+
lastTargetCandidate = target;
|
|
598
|
+
if (!target.visible || target.disabled || !isExactRequestResumeConfirmText(target.label)) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
prompt,
|
|
603
|
+
prompt_candidate: lastPromptCandidate,
|
|
604
|
+
target,
|
|
605
|
+
target_candidate: lastTargetCandidate,
|
|
606
|
+
discovery_mode: "exact_request_prompt"
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
prompt: lastPrompt,
|
|
613
|
+
prompt_candidate: lastPromptCandidate,
|
|
614
|
+
target: null,
|
|
615
|
+
target_candidate: lastTargetCandidate,
|
|
616
|
+
discovery_mode: lastPrompt ? "exact_request_prompt" : null
|
|
617
|
+
};
|
|
618
|
+
}
|
|
565
619
|
|
|
566
620
|
export async function selectChatPrimaryLabel(client, {
|
|
567
621
|
label = "全部",
|
|
@@ -758,29 +812,29 @@ export async function quickChatResumeModalOpenProbe(client, {
|
|
|
758
812
|
};
|
|
759
813
|
}
|
|
760
814
|
|
|
761
|
-
export async function findChatBlockingPanel(client, {
|
|
762
|
-
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
763
|
-
} = {}) {
|
|
764
|
-
const panel = await findBossAccountRightsBlockingPanel(client, { textQueries });
|
|
765
|
-
return panel.open
|
|
766
|
-
? { ...panel, reason: "blocking_panel_text_visible" }
|
|
767
|
-
: panel;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
export async function closeChatBlockingPanels(client, {
|
|
771
|
-
attemptsLimit = 2,
|
|
772
|
-
closeSelectors = CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
|
|
773
|
-
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
774
|
-
} = {}) {
|
|
775
|
-
return closeBossAccountRightsBlockingPanel(client, {
|
|
776
|
-
attemptsLimit,
|
|
777
|
-
closeSelectors,
|
|
778
|
-
resolveRoots: getChatRoots,
|
|
779
|
-
textQueries
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
export async function readChatResumeHtml(client, resumeState) {
|
|
815
|
+
export async function findChatBlockingPanel(client, {
|
|
816
|
+
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
817
|
+
} = {}) {
|
|
818
|
+
const panel = await findBossAccountRightsBlockingPanel(client, { textQueries });
|
|
819
|
+
return panel.open
|
|
820
|
+
? { ...panel, reason: "blocking_panel_text_visible" }
|
|
821
|
+
: panel;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export async function closeChatBlockingPanels(client, {
|
|
825
|
+
attemptsLimit = 2,
|
|
826
|
+
closeSelectors = CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
|
|
827
|
+
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
828
|
+
} = {}) {
|
|
829
|
+
return closeBossAccountRightsBlockingPanel(client, {
|
|
830
|
+
attemptsLimit,
|
|
831
|
+
closeSelectors,
|
|
832
|
+
resolveRoots: getChatRoots,
|
|
833
|
+
textQueries
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export async function readChatResumeHtml(client, resumeState) {
|
|
784
838
|
let popupHTML = "";
|
|
785
839
|
let contentHTML = "";
|
|
786
840
|
let resumeIframeHTML = "";
|
|
@@ -1001,14 +1055,14 @@ export async function openChatOnlineResume(client, {
|
|
|
1001
1055
|
}
|
|
1002
1056
|
}
|
|
1003
1057
|
|
|
1004
|
-
const error = new Error("Chat online resume modal did not open");
|
|
1005
|
-
error.code = CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE;
|
|
1006
|
-
error.retryable = true;
|
|
1007
|
-
error.attempts = attempts;
|
|
1008
|
-
throw error;
|
|
1009
|
-
}
|
|
1058
|
+
const error = new Error("Chat online resume modal did not open");
|
|
1059
|
+
error.code = CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE;
|
|
1060
|
+
error.retryable = true;
|
|
1061
|
+
error.attempts = attempts;
|
|
1062
|
+
throw error;
|
|
1063
|
+
}
|
|
1010
1064
|
|
|
1011
|
-
export async function readChatConversationReadyState(client) {
|
|
1065
|
+
export async function readChatConversationReadyState(client) {
|
|
1012
1066
|
const rootState = await getChatRoots(client);
|
|
1013
1067
|
const scopedControlRoots = await resolveScopedRoots(
|
|
1014
1068
|
client,
|
|
@@ -1082,60 +1136,1000 @@ export async function readChatConversationReadyState(client) {
|
|
|
1082
1136
|
blocking_panel_open: blockingPanelOpen,
|
|
1083
1137
|
blocking_panel: blockingPanelOpen ? blockingPanel : null,
|
|
1084
1138
|
panels_closed: !resumeModalOpen && !blockingPanelOpen
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function readAccessibilityBooleanProperty(node, propertyName) {
|
|
1143
|
+
for (const property of node?.properties || []) {
|
|
1144
|
+
if (property?.name !== propertyName) continue;
|
|
1145
|
+
if (typeof property?.value?.value === "boolean") return property.value.value;
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
async function readFreshChatEditorTarget(client) {
|
|
1151
|
+
const rootState = await getChatRoots(client);
|
|
1152
|
+
const scopedControlRoots = await resolveScopedRoots(
|
|
1153
|
+
client,
|
|
1154
|
+
rootState.roots,
|
|
1155
|
+
CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS,
|
|
1156
|
+
{ fallbackToRoots: false }
|
|
1157
|
+
);
|
|
1158
|
+
const controlRoots = scopedControlRoots.length ? scopedControlRoots : rootState.roots;
|
|
1159
|
+
return findVisibleMatchingTarget(
|
|
1160
|
+
client,
|
|
1161
|
+
controlRoots,
|
|
1162
|
+
CHAT_EDITOR_SELECTORS,
|
|
1163
|
+
() => true
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
async function readChatEditorFocusState(client, editor) {
|
|
1168
|
+
if (!editor?.node_id) {
|
|
1169
|
+
return {
|
|
1170
|
+
available: false,
|
|
1171
|
+
focused: false,
|
|
1172
|
+
reason: "editor_node_missing"
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
if (typeof client?.Accessibility?.getPartialAXTree !== "function") {
|
|
1176
|
+
return {
|
|
1177
|
+
available: false,
|
|
1178
|
+
focused: false,
|
|
1179
|
+
reason: "accessibility_unavailable",
|
|
1180
|
+
editor_node_id: editor.node_id
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
const result = await client.Accessibility.getPartialAXTree({
|
|
1185
|
+
nodeId: editor.node_id,
|
|
1186
|
+
fetchRelatives: false
|
|
1187
|
+
});
|
|
1188
|
+
const node = result?.nodes?.[0] || null;
|
|
1189
|
+
if (!node) {
|
|
1190
|
+
return {
|
|
1191
|
+
available: false,
|
|
1192
|
+
focused: false,
|
|
1193
|
+
reason: "accessibility_node_missing",
|
|
1194
|
+
editor_node_id: editor.node_id
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
available: true,
|
|
1199
|
+
focused: readAccessibilityBooleanProperty(node, "focused") === true,
|
|
1200
|
+
focusable: readAccessibilityBooleanProperty(node, "focusable"),
|
|
1201
|
+
role: node?.role?.value || "",
|
|
1202
|
+
backend_dom_node_id: node?.backendDOMNodeId || null,
|
|
1203
|
+
editor_node_id: editor.node_id
|
|
1204
|
+
};
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
return {
|
|
1207
|
+
available: false,
|
|
1208
|
+
focused: false,
|
|
1209
|
+
reason: "accessibility_read_failed",
|
|
1210
|
+
read_error: error?.message || String(error),
|
|
1211
|
+
editor_node_id: editor.node_id
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const CHAT_EDITOR_FOCUS_SETTLE_LIMIT_MS = 600;
|
|
1217
|
+
const CHAT_EDITOR_FOCUS_POLL_INTERVAL_MS = 100;
|
|
1218
|
+
|
|
1219
|
+
function canChatEditorFocusStillSettle(focus) {
|
|
1220
|
+
if (focus?.focused) return false;
|
|
1221
|
+
if (focus?.available && focus?.focusable === true && !focus?.read_error) return true;
|
|
1222
|
+
if (focus?.reason === "editor_node_missing" || focus?.reason === "accessibility_node_missing") {
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
return Boolean(focus?.read_error && isRecoverableNodeError(focus.read_error));
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
async function waitForFreshChatEditorFocus(client, {
|
|
1229
|
+
initialDelayMs = 0,
|
|
1230
|
+
settleLimitMs = CHAT_EDITOR_FOCUS_SETTLE_LIMIT_MS,
|
|
1231
|
+
pollIntervalMs = CHAT_EDITOR_FOCUS_POLL_INTERVAL_MS
|
|
1232
|
+
} = {}) {
|
|
1233
|
+
const startedAt = Date.now();
|
|
1234
|
+
const observations = [];
|
|
1235
|
+
let editor = null;
|
|
1236
|
+
let focus = {
|
|
1237
|
+
available: false,
|
|
1238
|
+
focused: false,
|
|
1239
|
+
reason: "focus_not_read"
|
|
1240
|
+
};
|
|
1241
|
+
let firstRead = true;
|
|
1242
|
+
while (true) {
|
|
1243
|
+
try {
|
|
1244
|
+
editor = await readFreshChatEditorTarget(client);
|
|
1245
|
+
focus = await readChatEditorFocusState(client, editor);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
1248
|
+
editor = null;
|
|
1249
|
+
focus = {
|
|
1250
|
+
available: false,
|
|
1251
|
+
focused: false,
|
|
1252
|
+
reason: "recoverable_node_error",
|
|
1253
|
+
read_error: error?.message || String(error)
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
observations.push({
|
|
1257
|
+
elapsed_ms: Math.max(0, Date.now() - startedAt),
|
|
1258
|
+
editor_node_id: editor?.node_id || null,
|
|
1259
|
+
available: focus.available,
|
|
1260
|
+
focused: focus.focused,
|
|
1261
|
+
focusable: focus.focusable ?? null,
|
|
1262
|
+
role: focus.role || null,
|
|
1263
|
+
backend_dom_node_id: focus.backend_dom_node_id || null,
|
|
1264
|
+
reason: focus.reason || null,
|
|
1265
|
+
read_error: focus.read_error || null
|
|
1266
|
+
});
|
|
1267
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1268
|
+
if (
|
|
1269
|
+
focus.focused
|
|
1270
|
+
|| !canChatEditorFocusStillSettle(focus)
|
|
1271
|
+
|| elapsedMs >= settleLimitMs
|
|
1272
|
+
) {
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
const delayMs = firstRead && initialDelayMs > 0
|
|
1276
|
+
? initialDelayMs
|
|
1277
|
+
: pollIntervalMs;
|
|
1278
|
+
firstRead = false;
|
|
1279
|
+
await sleep(Math.min(delayMs, Math.max(1, settleLimitMs - elapsedMs)));
|
|
1280
|
+
}
|
|
1281
|
+
return {
|
|
1282
|
+
editor,
|
|
1283
|
+
focus,
|
|
1284
|
+
observations,
|
|
1285
|
+
poll_count: Math.max(0, observations.length - 1),
|
|
1286
|
+
elapsed_ms: observations.at(-1)?.elapsed_ms || 0,
|
|
1287
|
+
settled_after_poll: Boolean(focus.focused && observations.length > 1)
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
async function recoverChatEditorFocusWithDomFocus(client, editor, focus) {
|
|
1292
|
+
const backendNodeId = Number(focus?.backend_dom_node_id);
|
|
1293
|
+
const editorNodeId = Number(editor?.node_id);
|
|
1294
|
+
if (
|
|
1295
|
+
typeof client?.DOM?.focus !== "function"
|
|
1296
|
+
|| typeof client?.DOM?.describeNode !== "function"
|
|
1297
|
+
|| !Number.isInteger(editorNodeId)
|
|
1298
|
+
|| editorNodeId <= 0
|
|
1299
|
+
|| focus?.focused
|
|
1300
|
+
|| focus?.available !== true
|
|
1301
|
+
|| focus?.focusable !== true
|
|
1302
|
+
|| focus?.read_error
|
|
1303
|
+
|| !Number.isInteger(backendNodeId)
|
|
1304
|
+
|| backendNodeId <= 0
|
|
1305
|
+
) {
|
|
1306
|
+
return {
|
|
1307
|
+
attempted: false,
|
|
1308
|
+
dispatched: false,
|
|
1309
|
+
editor,
|
|
1310
|
+
focus,
|
|
1311
|
+
settle: null
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
let focusCallAttempted = false;
|
|
1316
|
+
try {
|
|
1317
|
+
const freshEditor = await readFreshChatEditorTarget(client);
|
|
1318
|
+
const freshEditorNodeId = Number(freshEditor?.node_id);
|
|
1319
|
+
const axEditorNodeId = Number(focus?.editor_node_id);
|
|
1320
|
+
if (
|
|
1321
|
+
!Number.isInteger(freshEditorNodeId)
|
|
1322
|
+
|| freshEditorNodeId <= 0
|
|
1323
|
+
|| freshEditorNodeId !== editorNodeId
|
|
1324
|
+
|| axEditorNodeId !== editorNodeId
|
|
1325
|
+
) {
|
|
1326
|
+
return {
|
|
1327
|
+
attempted: true,
|
|
1328
|
+
dispatched: false,
|
|
1329
|
+
target_kind: "backend_node_id",
|
|
1330
|
+
target_id: backendNodeId,
|
|
1331
|
+
editor: freshEditor || editor,
|
|
1332
|
+
focus,
|
|
1333
|
+
settle: null,
|
|
1334
|
+
pre_focus_editor_node_id: editorNodeId,
|
|
1335
|
+
fresh_editor_node_id: Number.isInteger(freshEditorNodeId) ? freshEditorNodeId : null,
|
|
1336
|
+
ax_editor_node_id: Number.isInteger(axEditorNodeId) ? axEditorNodeId : null,
|
|
1337
|
+
focus_call_attempted: false,
|
|
1338
|
+
binding_failed: true,
|
|
1339
|
+
pre_focus_binding_verified: false,
|
|
1340
|
+
post_focus_binding_verified: false,
|
|
1341
|
+
error: "fresh_editor_node_binding_mismatch"
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const described = await client.DOM.describeNode({ nodeId: freshEditorNodeId });
|
|
1346
|
+
const describedBackendNodeId = Number(described?.node?.backendNodeId);
|
|
1347
|
+
if (
|
|
1348
|
+
!Number.isInteger(describedBackendNodeId)
|
|
1349
|
+
|| describedBackendNodeId <= 0
|
|
1350
|
+
|| describedBackendNodeId !== backendNodeId
|
|
1351
|
+
) {
|
|
1352
|
+
return {
|
|
1353
|
+
attempted: true,
|
|
1354
|
+
dispatched: false,
|
|
1355
|
+
target_kind: "backend_node_id",
|
|
1356
|
+
target_id: backendNodeId,
|
|
1357
|
+
editor: freshEditor,
|
|
1358
|
+
focus,
|
|
1359
|
+
settle: null,
|
|
1360
|
+
pre_focus_editor_node_id: editorNodeId,
|
|
1361
|
+
fresh_editor_node_id: freshEditorNodeId,
|
|
1362
|
+
ax_editor_node_id: axEditorNodeId,
|
|
1363
|
+
described_backend_dom_node_id: Number.isInteger(describedBackendNodeId)
|
|
1364
|
+
? describedBackendNodeId
|
|
1365
|
+
: null,
|
|
1366
|
+
focus_call_attempted: false,
|
|
1367
|
+
binding_failed: true,
|
|
1368
|
+
pre_focus_binding_verified: false,
|
|
1369
|
+
post_focus_binding_verified: false,
|
|
1370
|
+
error: "editor_backend_node_binding_mismatch"
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
focusCallAttempted = true;
|
|
1375
|
+
await client.DOM.focus({ backendNodeId });
|
|
1376
|
+
const settle = await waitForFreshChatEditorFocus(client, {
|
|
1377
|
+
initialDelayMs: 80
|
|
1378
|
+
});
|
|
1379
|
+
const postFocusEditorNodeId = Number(settle.editor?.node_id);
|
|
1380
|
+
const postFocusAxEditorNodeId = Number(settle.focus?.editor_node_id);
|
|
1381
|
+
const postFocusBackendNodeId = Number(settle.focus?.backend_dom_node_id);
|
|
1382
|
+
const postFocusBindingVerified = Boolean(
|
|
1383
|
+
Number.isInteger(postFocusEditorNodeId)
|
|
1384
|
+
&& postFocusEditorNodeId > 0
|
|
1385
|
+
&& postFocusAxEditorNodeId === postFocusEditorNodeId
|
|
1386
|
+
&& postFocusBackendNodeId === backendNodeId
|
|
1387
|
+
);
|
|
1388
|
+
const postFocusVerified = postFocusBindingVerified && settle.focus?.focused === true;
|
|
1389
|
+
const verifiedFocus = postFocusVerified
|
|
1390
|
+
? settle.focus
|
|
1391
|
+
: {
|
|
1392
|
+
...settle.focus,
|
|
1393
|
+
focused: false,
|
|
1394
|
+
reason: postFocusBindingVerified
|
|
1395
|
+
? "native_focus_not_verified"
|
|
1396
|
+
: "native_focus_post_binding_mismatch"
|
|
1397
|
+
};
|
|
1398
|
+
return {
|
|
1399
|
+
attempted: true,
|
|
1400
|
+
dispatched: true,
|
|
1401
|
+
target_kind: "backend_node_id",
|
|
1402
|
+
target_id: backendNodeId,
|
|
1403
|
+
editor: settle.editor || editor,
|
|
1404
|
+
focus: verifiedFocus,
|
|
1405
|
+
settle: {
|
|
1406
|
+
...settle,
|
|
1407
|
+
focus: verifiedFocus
|
|
1408
|
+
},
|
|
1409
|
+
pre_focus_editor_node_id: editorNodeId,
|
|
1410
|
+
fresh_editor_node_id: freshEditorNodeId,
|
|
1411
|
+
ax_editor_node_id: axEditorNodeId,
|
|
1412
|
+
described_backend_dom_node_id: describedBackendNodeId,
|
|
1413
|
+
focus_call_attempted: true,
|
|
1414
|
+
binding_failed: !postFocusVerified,
|
|
1415
|
+
pre_focus_binding_verified: true,
|
|
1416
|
+
post_focus_editor_node_id: Number.isInteger(postFocusEditorNodeId)
|
|
1417
|
+
? postFocusEditorNodeId
|
|
1418
|
+
: null,
|
|
1419
|
+
post_focus_ax_editor_node_id: Number.isInteger(postFocusAxEditorNodeId)
|
|
1420
|
+
? postFocusAxEditorNodeId
|
|
1421
|
+
: null,
|
|
1422
|
+
post_focus_backend_dom_node_id: Number.isInteger(postFocusBackendNodeId)
|
|
1423
|
+
? postFocusBackendNodeId
|
|
1424
|
+
: null,
|
|
1425
|
+
post_focus_binding_verified: postFocusBindingVerified,
|
|
1426
|
+
post_focus_verified: postFocusVerified,
|
|
1427
|
+
error: postFocusVerified
|
|
1428
|
+
? null
|
|
1429
|
+
: postFocusBindingVerified
|
|
1430
|
+
? "native_focus_not_verified"
|
|
1431
|
+
: "post_focus_backend_node_binding_mismatch"
|
|
1432
|
+
};
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
if (!focusCallAttempted && !isRecoverableNodeError(error)) throw error;
|
|
1435
|
+
return {
|
|
1436
|
+
attempted: true,
|
|
1437
|
+
dispatched: false,
|
|
1438
|
+
target_kind: "backend_node_id",
|
|
1439
|
+
target_id: backendNodeId,
|
|
1440
|
+
editor,
|
|
1441
|
+
focus,
|
|
1442
|
+
settle: null,
|
|
1443
|
+
focus_call_attempted: focusCallAttempted,
|
|
1444
|
+
binding_failed: focusCallAttempted,
|
|
1445
|
+
error: error?.message || String(error)
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function focusChatEditorForInput(client, initialEditor, {
|
|
1451
|
+
deterministic = false,
|
|
1452
|
+
attemptsLimit = 3
|
|
1453
|
+
} = {}) {
|
|
1454
|
+
let editor = initialEditor || null;
|
|
1455
|
+
let lastState = null;
|
|
1456
|
+
const attempts = [];
|
|
1457
|
+
let nativeFocusAttempted = false;
|
|
1458
|
+
const limit = Math.max(1, Number(attemptsLimit) || 1);
|
|
1459
|
+
for (let attempt = 1; attempt <= limit; attempt += 1) {
|
|
1460
|
+
if (!editor?.node_id) {
|
|
1461
|
+
editor = await readFreshChatEditorTarget(client);
|
|
1462
|
+
lastState = { editor };
|
|
1463
|
+
}
|
|
1464
|
+
if (!editor?.node_id) {
|
|
1465
|
+
attempts.push({
|
|
1466
|
+
attempt,
|
|
1467
|
+
editor_node_id: null,
|
|
1468
|
+
focused: false,
|
|
1469
|
+
reason: "editor_not_found"
|
|
1470
|
+
});
|
|
1471
|
+
if (attempt < limit) await sleep(160);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
let clickResult = null;
|
|
1476
|
+
try {
|
|
1477
|
+
const useDeterministicClick = deterministic || attempt > 1;
|
|
1478
|
+
if (editor.center) {
|
|
1479
|
+
clickResult = await clickPoint(
|
|
1480
|
+
client,
|
|
1481
|
+
editor.center.x,
|
|
1482
|
+
editor.center.y,
|
|
1483
|
+
useDeterministicClick ? DETERMINISTIC_CLICK_OPTIONS : undefined
|
|
1484
|
+
);
|
|
1485
|
+
} else {
|
|
1486
|
+
clickResult = await clickNodeCenter(client, editor.node_id, {
|
|
1487
|
+
scrollIntoView: true,
|
|
1488
|
+
...(useDeterministicClick ? DETERMINISTIC_CLICK_OPTIONS : {})
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
const clickedEditor = editor;
|
|
1492
|
+
// Boss can remount the contenteditable as a direct consequence of focus.
|
|
1493
|
+
// Frontend DOM node ids are session/document-epoch scoped, so never ask
|
|
1494
|
+
// Accessibility about the pre-click id. Reacquire the editor first.
|
|
1495
|
+
const clickFocusSettle = await waitForFreshChatEditorFocus(client, {
|
|
1496
|
+
initialDelayMs: attempt === 1 ? 120 : 180
|
|
1497
|
+
});
|
|
1498
|
+
let verificationEditor = clickFocusSettle.editor;
|
|
1499
|
+
lastState = { editor: verificationEditor };
|
|
1500
|
+
let focus = clickFocusSettle.focus;
|
|
1501
|
+
let focusSettle = clickFocusSettle;
|
|
1502
|
+
const nativeFocusRecovery = nativeFocusAttempted
|
|
1503
|
+
? {
|
|
1504
|
+
attempted: false,
|
|
1505
|
+
dispatched: false,
|
|
1506
|
+
editor: verificationEditor,
|
|
1507
|
+
focus,
|
|
1508
|
+
settle: null
|
|
1509
|
+
}
|
|
1510
|
+
: await recoverChatEditorFocusWithDomFocus(
|
|
1511
|
+
client,
|
|
1512
|
+
verificationEditor,
|
|
1513
|
+
focus
|
|
1514
|
+
);
|
|
1515
|
+
nativeFocusAttempted = nativeFocusAttempted || nativeFocusRecovery.attempted;
|
|
1516
|
+
if (nativeFocusRecovery.attempted) {
|
|
1517
|
+
verificationEditor = nativeFocusRecovery.editor || verificationEditor;
|
|
1518
|
+
focus = nativeFocusRecovery.focus || focus;
|
|
1519
|
+
focusSettle = nativeFocusRecovery.settle || focusSettle;
|
|
1520
|
+
lastState = { editor: verificationEditor };
|
|
1521
|
+
}
|
|
1522
|
+
attempts.push({
|
|
1523
|
+
attempt,
|
|
1524
|
+
clicked_editor_node_id: clickedEditor.node_id,
|
|
1525
|
+
editor_node_id: verificationEditor?.node_id || null,
|
|
1526
|
+
editor_reacquired: Boolean(verificationEditor?.node_id),
|
|
1527
|
+
editor_node_changed: Boolean(
|
|
1528
|
+
verificationEditor?.node_id
|
|
1529
|
+
&& verificationEditor.node_id !== clickedEditor.node_id
|
|
1530
|
+
),
|
|
1531
|
+
editor_selector: verificationEditor?.selector || clickedEditor.selector || null,
|
|
1532
|
+
editor_trace_id: verificationEditor?.attributes?.traceid || clickedEditor.attributes?.traceid || null,
|
|
1533
|
+
click_mode: clickResult?.mode || clickResult?.click_result?.mode || null,
|
|
1534
|
+
focus_available: focus.available,
|
|
1535
|
+
focused: focus.focused,
|
|
1536
|
+
focusable: focus.focusable ?? null,
|
|
1537
|
+
focus_role: focus.role || null,
|
|
1538
|
+
focus_backend_dom_node_id: focus.backend_dom_node_id || null,
|
|
1539
|
+
focus_reason: focus.reason || null,
|
|
1540
|
+
focus_read_error: focus.read_error || null,
|
|
1541
|
+
focus_poll_count: focusSettle.poll_count,
|
|
1542
|
+
focus_settle_elapsed_ms: focusSettle.elapsed_ms,
|
|
1543
|
+
focus_settled_after_poll: focusSettle.settled_after_poll,
|
|
1544
|
+
focus_observations: focusSettle.observations,
|
|
1545
|
+
click_focus_poll_count: clickFocusSettle.poll_count,
|
|
1546
|
+
click_focus_settle_elapsed_ms: clickFocusSettle.elapsed_ms,
|
|
1547
|
+
click_focus_settled_after_poll: clickFocusSettle.settled_after_poll,
|
|
1548
|
+
click_focus_observations: clickFocusSettle.observations,
|
|
1549
|
+
native_focus_attempted: nativeFocusRecovery.attempted,
|
|
1550
|
+
native_focus_dispatched: nativeFocusRecovery.dispatched,
|
|
1551
|
+
native_focus_target_kind: nativeFocusRecovery.target_kind || null,
|
|
1552
|
+
native_focus_target_id: nativeFocusRecovery.target_id || null,
|
|
1553
|
+
native_focus_error: nativeFocusRecovery.error || null,
|
|
1554
|
+
native_focus_call_attempted: nativeFocusRecovery.focus_call_attempted === true,
|
|
1555
|
+
native_focus_binding_failed: nativeFocusRecovery.binding_failed === true,
|
|
1556
|
+
native_focus_pre_editor_node_id: nativeFocusRecovery.pre_focus_editor_node_id || null,
|
|
1557
|
+
native_focus_fresh_editor_node_id: nativeFocusRecovery.fresh_editor_node_id || null,
|
|
1558
|
+
native_focus_ax_editor_node_id: nativeFocusRecovery.ax_editor_node_id || null,
|
|
1559
|
+
native_focus_described_backend_dom_node_id: nativeFocusRecovery.described_backend_dom_node_id || null,
|
|
1560
|
+
native_focus_pre_binding_verified: nativeFocusRecovery.pre_focus_binding_verified === true,
|
|
1561
|
+
native_focus_post_editor_node_id: nativeFocusRecovery.post_focus_editor_node_id || null,
|
|
1562
|
+
native_focus_post_ax_editor_node_id: nativeFocusRecovery.post_focus_ax_editor_node_id || null,
|
|
1563
|
+
native_focus_post_backend_dom_node_id: nativeFocusRecovery.post_focus_backend_dom_node_id || null,
|
|
1564
|
+
native_focus_post_binding_verified: nativeFocusRecovery.post_focus_binding_verified === true,
|
|
1565
|
+
native_focus_post_verified: nativeFocusRecovery.post_focus_verified === true,
|
|
1566
|
+
native_focus_poll_count: nativeFocusRecovery.settle?.poll_count ?? null,
|
|
1567
|
+
native_focus_settle_elapsed_ms: nativeFocusRecovery.settle?.elapsed_ms ?? null,
|
|
1568
|
+
native_focus_settled_after_poll: nativeFocusRecovery.settle?.settled_after_poll ?? null,
|
|
1569
|
+
native_focus_observations: nativeFocusRecovery.settle?.observations || []
|
|
1570
|
+
});
|
|
1571
|
+
if (nativeFocusRecovery.binding_failed) {
|
|
1572
|
+
return {
|
|
1573
|
+
ok: false,
|
|
1574
|
+
terminal_binding_failure: true,
|
|
1575
|
+
editor: verificationEditor,
|
|
1576
|
+
focus,
|
|
1577
|
+
attempts,
|
|
1578
|
+
state: lastState,
|
|
1579
|
+
terminal_focus_binding_failure: nativeFocusRecovery.focus_call_attempted === true,
|
|
1580
|
+
focus_binding_error: nativeFocusRecovery.error || null
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
if (focus.focused) {
|
|
1584
|
+
return {
|
|
1585
|
+
ok: true,
|
|
1586
|
+
editor: verificationEditor,
|
|
1587
|
+
focus,
|
|
1588
|
+
native_focus_backend_node_id: nativeFocusRecovery.dispatched
|
|
1589
|
+
&& nativeFocusRecovery.pre_focus_binding_verified
|
|
1590
|
+
&& nativeFocusRecovery.post_focus_binding_verified
|
|
1591
|
+
? nativeFocusRecovery.target_id
|
|
1592
|
+
: null,
|
|
1593
|
+
attempts,
|
|
1594
|
+
state: lastState
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
editor = focus.read_error && isRecoverableNodeError(focus.read_error)
|
|
1598
|
+
? null
|
|
1599
|
+
: verificationEditor;
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
1602
|
+
attempts.push({
|
|
1603
|
+
attempt,
|
|
1604
|
+
editor_node_id: editor.node_id,
|
|
1605
|
+
editor_selector: editor.selector || null,
|
|
1606
|
+
editor_trace_id: editor.attributes?.traceid || null,
|
|
1607
|
+
focused: false,
|
|
1608
|
+
recoverable_error: error?.message || String(error)
|
|
1609
|
+
});
|
|
1610
|
+
editor = null;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
if (attempt < limit) await sleep(160);
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
ok: false,
|
|
1617
|
+
editor,
|
|
1618
|
+
focus: attempts[attempts.length - 1] || null,
|
|
1619
|
+
attempts,
|
|
1620
|
+
state: lastState
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
async function verifyChatEditorBackendBinding(client, editor, focus, expectedBackendNodeId) {
|
|
1625
|
+
const expected = Number(expectedBackendNodeId);
|
|
1626
|
+
if (!Number.isInteger(expected) || expected <= 0) {
|
|
1627
|
+
return {
|
|
1628
|
+
required: false,
|
|
1629
|
+
verified: true
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
const editorNodeId = Number(editor?.node_id);
|
|
1633
|
+
const axEditorNodeId = Number(focus?.editor_node_id);
|
|
1634
|
+
const axBackendNodeId = Number(focus?.backend_dom_node_id);
|
|
1635
|
+
const base = {
|
|
1636
|
+
required: true,
|
|
1637
|
+
expected_backend_dom_node_id: expected,
|
|
1638
|
+
editor_node_id: Number.isInteger(editorNodeId) ? editorNodeId : null,
|
|
1639
|
+
ax_editor_node_id: Number.isInteger(axEditorNodeId) ? axEditorNodeId : null,
|
|
1640
|
+
ax_backend_dom_node_id: Number.isInteger(axBackendNodeId) ? axBackendNodeId : null
|
|
1641
|
+
};
|
|
1642
|
+
if (
|
|
1643
|
+
focus?.focused !== true
|
|
1644
|
+
|| !Number.isInteger(editorNodeId)
|
|
1645
|
+
|| editorNodeId <= 0
|
|
1646
|
+
|| axEditorNodeId !== editorNodeId
|
|
1647
|
+
|| axBackendNodeId !== expected
|
|
1648
|
+
|| typeof client?.DOM?.describeNode !== "function"
|
|
1649
|
+
) {
|
|
1650
|
+
return {
|
|
1651
|
+
...base,
|
|
1652
|
+
verified: false,
|
|
1653
|
+
error: "editor_backend_node_binding_mismatch"
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
try {
|
|
1657
|
+
const described = await client.DOM.describeNode({ nodeId: editorNodeId });
|
|
1658
|
+
const describedBackendNodeId = Number(described?.node?.backendNodeId);
|
|
1659
|
+
const verified = Number.isInteger(describedBackendNodeId)
|
|
1660
|
+
&& describedBackendNodeId > 0
|
|
1661
|
+
&& describedBackendNodeId === expected;
|
|
1662
|
+
return {
|
|
1663
|
+
...base,
|
|
1664
|
+
described_backend_dom_node_id: Number.isInteger(describedBackendNodeId)
|
|
1665
|
+
? describedBackendNodeId
|
|
1666
|
+
: null,
|
|
1667
|
+
verified,
|
|
1668
|
+
error: verified ? null : "editor_backend_node_binding_mismatch"
|
|
1669
|
+
};
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
return {
|
|
1672
|
+
...base,
|
|
1673
|
+
verified: false,
|
|
1674
|
+
error: error?.message || String(error)
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
export async function setChatEditorMessage(client, message, {
|
|
1680
|
+
timeoutMs = 8000
|
|
1681
|
+
} = {}) {
|
|
1682
|
+
const expectedText = normalizeDetailText(message);
|
|
1683
|
+
const started = Date.now();
|
|
1684
|
+
let lastState = null;
|
|
1685
|
+
let editorFound = false;
|
|
1686
|
+
const attempts = [];
|
|
1687
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1688
|
+
const state = await readChatConversationReadyState(client);
|
|
1689
|
+
lastState = state;
|
|
1690
|
+
if (state.editor?.node_id) {
|
|
1691
|
+
editorFound = true;
|
|
1692
|
+
try {
|
|
1693
|
+
const focusResult = await focusChatEditorForInput(client, state.editor, {
|
|
1694
|
+
deterministic: false,
|
|
1695
|
+
attemptsLimit: 3
|
|
1696
|
+
});
|
|
1697
|
+
const focusedEditor = focusResult.editor || state.editor;
|
|
1698
|
+
if (!focusResult.ok) {
|
|
1699
|
+
const observedText = normalizeDetailText(focusedEditor?.label || "");
|
|
1700
|
+
attempts.push({
|
|
1701
|
+
stage: "normal",
|
|
1702
|
+
editor_node_id: focusedEditor?.node_id || state.editor.node_id,
|
|
1703
|
+
editor_selector: focusedEditor?.selector || state.editor.selector || null,
|
|
1704
|
+
editor_trace_id: focusedEditor?.attributes?.traceid || state.editor.attributes?.traceid || null,
|
|
1705
|
+
input_mode: null,
|
|
1706
|
+
chunk_count: 0,
|
|
1707
|
+
expected_length: expectedText.length,
|
|
1708
|
+
observed_length: observedText.length,
|
|
1709
|
+
exact_match: false,
|
|
1710
|
+
focus_verified: false,
|
|
1711
|
+
focus_attempts: focusResult.attempts,
|
|
1712
|
+
terminal_focus_binding_failure: focusResult.terminal_focus_binding_failure === true,
|
|
1713
|
+
focus_binding_error: focusResult.focus_binding_error || null
|
|
1714
|
+
});
|
|
1715
|
+
lastState = {
|
|
1716
|
+
...(focusResult.state || state),
|
|
1717
|
+
editor_focus_unverified: true,
|
|
1718
|
+
terminal_focus_binding_failure: focusResult.terminal_focus_binding_failure === true,
|
|
1719
|
+
focus_binding_error: focusResult.focus_binding_error || null,
|
|
1720
|
+
editor_text_length: observedText.length
|
|
1721
|
+
};
|
|
1722
|
+
if (focusResult.terminal_focus_binding_failure) {
|
|
1723
|
+
return {
|
|
1724
|
+
ok: false,
|
|
1725
|
+
error: "CHAT_EDITOR_MESSAGE_MISMATCH",
|
|
1726
|
+
editor_found: true,
|
|
1727
|
+
attempts,
|
|
1728
|
+
deterministic_fallback_attempted: false,
|
|
1729
|
+
state: lastState
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
const clearSkipped = normalizeDetailText(focusedEditor.label || "") === "";
|
|
1735
|
+
const nativeFocusBackendNodeId = Number(focusResult.native_focus_backend_node_id);
|
|
1736
|
+
let preClearFocusSettle = null;
|
|
1737
|
+
let preClearBinding = {
|
|
1738
|
+
required: false,
|
|
1739
|
+
verified: true
|
|
1740
|
+
};
|
|
1741
|
+
if (Number.isInteger(nativeFocusBackendNodeId) && nativeFocusBackendNodeId > 0) {
|
|
1742
|
+
preClearFocusSettle = await waitForFreshChatEditorFocus(client, {
|
|
1743
|
+
initialDelayMs: 80
|
|
1744
|
+
});
|
|
1745
|
+
preClearBinding = await verifyChatEditorBackendBinding(
|
|
1746
|
+
client,
|
|
1747
|
+
preClearFocusSettle.editor,
|
|
1748
|
+
preClearFocusSettle.focus,
|
|
1749
|
+
nativeFocusBackendNodeId
|
|
1750
|
+
);
|
|
1751
|
+
if (!preClearBinding.verified) {
|
|
1752
|
+
attempts.push({
|
|
1753
|
+
stage: "normal",
|
|
1754
|
+
editor_node_id: preClearFocusSettle.editor?.node_id || null,
|
|
1755
|
+
editor_selector: preClearFocusSettle.editor?.selector || focusedEditor.selector || null,
|
|
1756
|
+
editor_trace_id: preClearFocusSettle.editor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
|
|
1757
|
+
input_mode: null,
|
|
1758
|
+
chunk_count: 0,
|
|
1759
|
+
expected_length: expectedText.length,
|
|
1760
|
+
observed_length: normalizeDetailText(focusedEditor.label || "").length,
|
|
1761
|
+
exact_match: false,
|
|
1762
|
+
focus_verified: false,
|
|
1763
|
+
terminal_focus_binding_failure: true,
|
|
1764
|
+
focus_binding_error: preClearBinding.error || null,
|
|
1765
|
+
focus_attempts: focusResult.attempts,
|
|
1766
|
+
pre_clear_focus: preClearFocusSettle.focus,
|
|
1767
|
+
pre_clear_focus_observations: preClearFocusSettle.observations,
|
|
1768
|
+
pre_clear_binding: preClearBinding,
|
|
1769
|
+
clear_skipped: true
|
|
1770
|
+
});
|
|
1771
|
+
lastState = {
|
|
1772
|
+
editor: preClearFocusSettle.editor,
|
|
1773
|
+
terminal_focus_binding_failure: true,
|
|
1774
|
+
focus_binding_error: preClearBinding.error || null,
|
|
1775
|
+
editor_focus_unverified: true
|
|
1776
|
+
};
|
|
1777
|
+
return {
|
|
1778
|
+
ok: false,
|
|
1779
|
+
error: "CHAT_EDITOR_MESSAGE_MISMATCH",
|
|
1780
|
+
editor_found: true,
|
|
1781
|
+
attempts,
|
|
1782
|
+
deterministic_fallback_attempted: false,
|
|
1783
|
+
state: lastState
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (!clearSkipped) await clearFocusedInput(client);
|
|
1788
|
+
const preInsertFocusSettle = clearSkipped && preClearFocusSettle
|
|
1789
|
+
? preClearFocusSettle
|
|
1790
|
+
: await waitForFreshChatEditorFocus(client, {
|
|
1791
|
+
initialDelayMs: 80
|
|
1792
|
+
});
|
|
1793
|
+
const preInsertEditor = preInsertFocusSettle.editor;
|
|
1794
|
+
const preInsertState = { editor: preInsertEditor };
|
|
1795
|
+
const preInsertFocus = preInsertFocusSettle.focus;
|
|
1796
|
+
const preInsertBinding = clearSkipped && preClearFocusSettle
|
|
1797
|
+
? preClearBinding
|
|
1798
|
+
: await verifyChatEditorBackendBinding(
|
|
1799
|
+
client,
|
|
1800
|
+
preInsertEditor,
|
|
1801
|
+
preInsertFocus,
|
|
1802
|
+
nativeFocusBackendNodeId
|
|
1803
|
+
);
|
|
1804
|
+
if (!preInsertFocus.focused || !preInsertBinding.verified) {
|
|
1805
|
+
attempts.push({
|
|
1806
|
+
stage: "normal",
|
|
1807
|
+
editor_node_id: preInsertEditor?.node_id || null,
|
|
1808
|
+
editor_node_changed_before_insert: Boolean(
|
|
1809
|
+
preInsertEditor?.node_id
|
|
1810
|
+
&& preInsertEditor.node_id !== focusedEditor.node_id
|
|
1811
|
+
),
|
|
1812
|
+
editor_selector: preInsertEditor?.selector || focusedEditor.selector || null,
|
|
1813
|
+
editor_trace_id: preInsertEditor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
|
|
1814
|
+
input_mode: null,
|
|
1815
|
+
chunk_count: 0,
|
|
1816
|
+
expected_length: expectedText.length,
|
|
1817
|
+
observed_length: normalizeDetailText(focusedEditor.label || "").length,
|
|
1818
|
+
exact_match: false,
|
|
1819
|
+
focus_verified: false,
|
|
1820
|
+
focus_lost_before_insert: true,
|
|
1821
|
+
terminal_focus_binding_failure: preInsertBinding.required && !preInsertBinding.verified,
|
|
1822
|
+
focus_binding_error: preInsertBinding.error || null,
|
|
1823
|
+
focus_attempts: focusResult.attempts,
|
|
1824
|
+
pre_insert_focus: preInsertFocus,
|
|
1825
|
+
pre_insert_binding: preInsertBinding,
|
|
1826
|
+
pre_insert_focus_poll_count: preInsertFocusSettle.poll_count,
|
|
1827
|
+
pre_insert_focus_settle_elapsed_ms: preInsertFocusSettle.elapsed_ms,
|
|
1828
|
+
pre_insert_focus_observations: preInsertFocusSettle.observations,
|
|
1829
|
+
clear_skipped: clearSkipped
|
|
1830
|
+
});
|
|
1831
|
+
lastState = {
|
|
1832
|
+
...preInsertState,
|
|
1833
|
+
editor_focus_lost_before_insert: true,
|
|
1834
|
+
terminal_focus_binding_failure: preInsertBinding.required && !preInsertBinding.verified,
|
|
1835
|
+
focus_binding_error: preInsertBinding.error || null
|
|
1836
|
+
};
|
|
1837
|
+
if (preInsertBinding.required && !preInsertBinding.verified) {
|
|
1838
|
+
return {
|
|
1839
|
+
ok: false,
|
|
1840
|
+
error: "CHAT_EDITOR_MESSAGE_MISMATCH",
|
|
1841
|
+
editor_found: true,
|
|
1842
|
+
attempts,
|
|
1843
|
+
deterministic_fallback_attempted: false,
|
|
1844
|
+
state: lastState
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
const inputResult = await insertText(client, message);
|
|
1850
|
+
await sleep(250);
|
|
1851
|
+
const afterState = await readChatConversationReadyState(client);
|
|
1852
|
+
const editorText = normalizeDetailText(afterState.editor?.label || "");
|
|
1853
|
+
const exactMatch = editorText === expectedText;
|
|
1854
|
+
attempts.push({
|
|
1855
|
+
stage: "normal",
|
|
1856
|
+
editor_node_id: preInsertEditor?.node_id || focusedEditor.node_id,
|
|
1857
|
+
editor_node_changed_before_insert: Boolean(
|
|
1858
|
+
preInsertEditor?.node_id
|
|
1859
|
+
&& preInsertEditor.node_id !== focusedEditor.node_id
|
|
1860
|
+
),
|
|
1861
|
+
editor_selector: preInsertEditor?.selector || focusedEditor.selector || null,
|
|
1862
|
+
editor_trace_id: preInsertEditor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
|
|
1863
|
+
input_mode: inputResult?.mode || null,
|
|
1864
|
+
chunk_count: Number(inputResult?.chunk_count || 0),
|
|
1865
|
+
expected_length: expectedText.length,
|
|
1866
|
+
observed_length: editorText.length,
|
|
1867
|
+
exact_match: exactMatch,
|
|
1868
|
+
focus_verified: true,
|
|
1869
|
+
focus_attempts: focusResult.attempts,
|
|
1870
|
+
pre_insert_focus: preInsertFocus,
|
|
1871
|
+
pre_insert_binding: preInsertBinding,
|
|
1872
|
+
pre_insert_focus_poll_count: preInsertFocusSettle.poll_count,
|
|
1873
|
+
pre_insert_focus_settle_elapsed_ms: preInsertFocusSettle.elapsed_ms,
|
|
1874
|
+
pre_insert_focus_observations: preInsertFocusSettle.observations,
|
|
1875
|
+
clear_skipped: clearSkipped,
|
|
1876
|
+
read_error: afterState.editor?.read_error || null
|
|
1877
|
+
});
|
|
1878
|
+
if (exactMatch) {
|
|
1879
|
+
return {
|
|
1880
|
+
ok: true,
|
|
1881
|
+
value: editorText,
|
|
1882
|
+
editor: afterState.editor || state.editor,
|
|
1883
|
+
attempts,
|
|
1884
|
+
deterministic_fallback_attempted: false
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
lastState = {
|
|
1888
|
+
...afterState,
|
|
1889
|
+
editor_message_mismatch: true,
|
|
1890
|
+
editor_text_length: editorText.length
|
|
1891
|
+
};
|
|
1892
|
+
break;
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
1895
|
+
attempts.push({
|
|
1896
|
+
stage: "normal",
|
|
1897
|
+
editor_node_id: state.editor.node_id,
|
|
1898
|
+
editor_selector: state.editor.selector || null,
|
|
1899
|
+
editor_trace_id: state.editor.attributes?.traceid || null,
|
|
1900
|
+
expected_length: expectedText.length,
|
|
1901
|
+
exact_match: false,
|
|
1902
|
+
recoverable_error: error?.message || String(error)
|
|
1903
|
+
});
|
|
1904
|
+
lastState = {
|
|
1905
|
+
...state,
|
|
1906
|
+
recoverable_error: error?.message || String(error),
|
|
1127
1907
|
recoverable_phase: "set_editor_message"
|
|
1128
1908
|
};
|
|
1129
1909
|
}
|
|
1130
|
-
}
|
|
1131
|
-
await sleep(250);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1910
|
+
}
|
|
1911
|
+
await sleep(250);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// A chat transition can briefly steal focus or remount the contenteditable
|
|
1915
|
+
// while human-style chunked input is in progress. No outbound action has
|
|
1916
|
+
// started yet, so one freshly reacquired, deterministic editor-only retry is
|
|
1917
|
+
// safe. The message still must be read back exactly before callers may send.
|
|
1918
|
+
let deterministicFallbackAttempted = false;
|
|
1919
|
+
const fallbackState = await readChatConversationReadyState(client);
|
|
1920
|
+
lastState = fallbackState || lastState;
|
|
1921
|
+
if (fallbackState?.editor?.node_id) {
|
|
1922
|
+
editorFound = true;
|
|
1923
|
+
deterministicFallbackAttempted = true;
|
|
1924
|
+
try {
|
|
1925
|
+
const focusResult = await focusChatEditorForInput(client, fallbackState.editor, {
|
|
1926
|
+
deterministic: true,
|
|
1927
|
+
attemptsLimit: 3
|
|
1928
|
+
});
|
|
1929
|
+
const focusedEditor = focusResult.editor || fallbackState.editor;
|
|
1930
|
+
if (!focusResult.ok) {
|
|
1931
|
+
const observedText = normalizeDetailText(focusedEditor?.label || "");
|
|
1932
|
+
attempts.push({
|
|
1933
|
+
stage: "deterministic_fallback",
|
|
1934
|
+
editor_node_id: focusedEditor?.node_id || fallbackState.editor.node_id,
|
|
1935
|
+
editor_selector: focusedEditor?.selector || fallbackState.editor.selector || null,
|
|
1936
|
+
editor_trace_id: focusedEditor?.attributes?.traceid || fallbackState.editor.attributes?.traceid || null,
|
|
1937
|
+
input_mode: null,
|
|
1938
|
+
chunk_count: 0,
|
|
1939
|
+
expected_length: expectedText.length,
|
|
1940
|
+
observed_length: observedText.length,
|
|
1941
|
+
exact_match: false,
|
|
1942
|
+
focus_verified: false,
|
|
1943
|
+
focus_attempts: focusResult.attempts
|
|
1944
|
+
});
|
|
1945
|
+
lastState = {
|
|
1946
|
+
...(focusResult.state || fallbackState),
|
|
1947
|
+
editor_focus_unverified: true,
|
|
1948
|
+
editor_text_length: observedText.length
|
|
1949
|
+
};
|
|
1950
|
+
} else {
|
|
1951
|
+
const clearSkipped = normalizeDetailText(focusedEditor.label || "") === "";
|
|
1952
|
+
const nativeFocusBackendNodeId = Number(focusResult.native_focus_backend_node_id);
|
|
1953
|
+
let preClearFocusSettle = null;
|
|
1954
|
+
let preClearBinding = {
|
|
1955
|
+
required: false,
|
|
1956
|
+
verified: true
|
|
1957
|
+
};
|
|
1958
|
+
if (Number.isInteger(nativeFocusBackendNodeId) && nativeFocusBackendNodeId > 0) {
|
|
1959
|
+
preClearFocusSettle = await waitForFreshChatEditorFocus(client, {
|
|
1960
|
+
initialDelayMs: 80
|
|
1961
|
+
});
|
|
1962
|
+
preClearBinding = await verifyChatEditorBackendBinding(
|
|
1963
|
+
client,
|
|
1964
|
+
preClearFocusSettle.editor,
|
|
1965
|
+
preClearFocusSettle.focus,
|
|
1966
|
+
nativeFocusBackendNodeId
|
|
1967
|
+
);
|
|
1968
|
+
if (!preClearBinding.verified) {
|
|
1969
|
+
attempts.push({
|
|
1970
|
+
stage: "deterministic_fallback",
|
|
1971
|
+
editor_node_id: preClearFocusSettle.editor?.node_id || null,
|
|
1972
|
+
editor_selector: preClearFocusSettle.editor?.selector || focusedEditor.selector || null,
|
|
1973
|
+
editor_trace_id: preClearFocusSettle.editor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
|
|
1974
|
+
input_mode: null,
|
|
1975
|
+
chunk_count: 0,
|
|
1976
|
+
expected_length: expectedText.length,
|
|
1977
|
+
observed_length: normalizeDetailText(focusedEditor.label || "").length,
|
|
1978
|
+
exact_match: false,
|
|
1979
|
+
focus_verified: false,
|
|
1980
|
+
terminal_focus_binding_failure: true,
|
|
1981
|
+
focus_binding_error: preClearBinding.error || null,
|
|
1982
|
+
focus_attempts: focusResult.attempts,
|
|
1983
|
+
pre_clear_focus: preClearFocusSettle.focus,
|
|
1984
|
+
pre_clear_focus_observations: preClearFocusSettle.observations,
|
|
1985
|
+
pre_clear_binding: preClearBinding,
|
|
1986
|
+
clear_skipped: true
|
|
1987
|
+
});
|
|
1988
|
+
lastState = {
|
|
1989
|
+
editor: preClearFocusSettle.editor,
|
|
1990
|
+
terminal_focus_binding_failure: true,
|
|
1991
|
+
focus_binding_error: preClearBinding.error || null,
|
|
1992
|
+
editor_focus_unverified: true
|
|
1993
|
+
};
|
|
1994
|
+
return {
|
|
1995
|
+
ok: false,
|
|
1996
|
+
error: "CHAT_EDITOR_MESSAGE_MISMATCH",
|
|
1997
|
+
editor_found: true,
|
|
1998
|
+
attempts,
|
|
1999
|
+
deterministic_fallback_attempted: true,
|
|
2000
|
+
state: lastState
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (!clearSkipped) await clearFocusedInput(client);
|
|
2005
|
+
const preInsertFocusSettle = clearSkipped && preClearFocusSettle
|
|
2006
|
+
? preClearFocusSettle
|
|
2007
|
+
: await waitForFreshChatEditorFocus(client, {
|
|
2008
|
+
initialDelayMs: 80
|
|
2009
|
+
});
|
|
2010
|
+
const preInsertEditor = preInsertFocusSettle.editor;
|
|
2011
|
+
const preInsertState = { editor: preInsertEditor };
|
|
2012
|
+
const preInsertFocus = preInsertFocusSettle.focus;
|
|
2013
|
+
const preInsertBinding = clearSkipped && preClearFocusSettle
|
|
2014
|
+
? preClearBinding
|
|
2015
|
+
: await verifyChatEditorBackendBinding(
|
|
2016
|
+
client,
|
|
2017
|
+
preInsertEditor,
|
|
2018
|
+
preInsertFocus,
|
|
2019
|
+
nativeFocusBackendNodeId
|
|
2020
|
+
);
|
|
2021
|
+
if (!preInsertFocus.focused || !preInsertBinding.verified) {
|
|
2022
|
+
attempts.push({
|
|
2023
|
+
stage: "deterministic_fallback",
|
|
2024
|
+
editor_node_id: preInsertEditor?.node_id || null,
|
|
2025
|
+
editor_node_changed_before_insert: Boolean(
|
|
2026
|
+
preInsertEditor?.node_id
|
|
2027
|
+
&& preInsertEditor.node_id !== focusedEditor.node_id
|
|
2028
|
+
),
|
|
2029
|
+
editor_selector: preInsertEditor?.selector || focusedEditor.selector || null,
|
|
2030
|
+
editor_trace_id: preInsertEditor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
|
|
2031
|
+
input_mode: null,
|
|
2032
|
+
chunk_count: 0,
|
|
2033
|
+
expected_length: expectedText.length,
|
|
2034
|
+
observed_length: normalizeDetailText(focusedEditor.label || "").length,
|
|
2035
|
+
exact_match: false,
|
|
2036
|
+
focus_verified: false,
|
|
2037
|
+
focus_lost_before_insert: true,
|
|
2038
|
+
terminal_focus_binding_failure: preInsertBinding.required && !preInsertBinding.verified,
|
|
2039
|
+
focus_binding_error: preInsertBinding.error || null,
|
|
2040
|
+
focus_attempts: focusResult.attempts,
|
|
2041
|
+
pre_insert_focus: preInsertFocus,
|
|
2042
|
+
pre_insert_binding: preInsertBinding,
|
|
2043
|
+
pre_insert_focus_poll_count: preInsertFocusSettle.poll_count,
|
|
2044
|
+
pre_insert_focus_settle_elapsed_ms: preInsertFocusSettle.elapsed_ms,
|
|
2045
|
+
pre_insert_focus_observations: preInsertFocusSettle.observations,
|
|
2046
|
+
clear_skipped: clearSkipped
|
|
2047
|
+
});
|
|
2048
|
+
lastState = {
|
|
2049
|
+
...preInsertState,
|
|
2050
|
+
editor_focus_lost_before_insert: true,
|
|
2051
|
+
terminal_focus_binding_failure: preInsertBinding.required && !preInsertBinding.verified,
|
|
2052
|
+
focus_binding_error: preInsertBinding.error || null
|
|
2053
|
+
};
|
|
2054
|
+
} else {
|
|
2055
|
+
const inputResult = await insertText(client, message, {
|
|
2056
|
+
humanTextEntryEnabled: false
|
|
2057
|
+
});
|
|
2058
|
+
await sleep(350);
|
|
2059
|
+
const afterState = await readChatConversationReadyState(client);
|
|
2060
|
+
const editorText = normalizeDetailText(afterState.editor?.label || "");
|
|
2061
|
+
const exactMatch = editorText === expectedText;
|
|
2062
|
+
attempts.push({
|
|
2063
|
+
stage: "deterministic_fallback",
|
|
2064
|
+
editor_node_id: preInsertEditor?.node_id || focusedEditor.node_id,
|
|
2065
|
+
editor_node_changed_before_insert: Boolean(
|
|
2066
|
+
preInsertEditor?.node_id
|
|
2067
|
+
&& preInsertEditor.node_id !== focusedEditor.node_id
|
|
2068
|
+
),
|
|
2069
|
+
editor_selector: preInsertEditor?.selector || focusedEditor.selector || null,
|
|
2070
|
+
editor_trace_id: preInsertEditor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
|
|
2071
|
+
input_mode: inputResult?.mode || null,
|
|
2072
|
+
chunk_count: Number(inputResult?.chunk_count || 0),
|
|
2073
|
+
expected_length: expectedText.length,
|
|
2074
|
+
observed_length: editorText.length,
|
|
2075
|
+
exact_match: exactMatch,
|
|
2076
|
+
focus_verified: true,
|
|
2077
|
+
focus_attempts: focusResult.attempts,
|
|
2078
|
+
pre_insert_focus: preInsertFocus,
|
|
2079
|
+
pre_insert_binding: preInsertBinding,
|
|
2080
|
+
pre_insert_focus_poll_count: preInsertFocusSettle.poll_count,
|
|
2081
|
+
pre_insert_focus_settle_elapsed_ms: preInsertFocusSettle.elapsed_ms,
|
|
2082
|
+
pre_insert_focus_observations: preInsertFocusSettle.observations,
|
|
2083
|
+
clear_skipped: clearSkipped,
|
|
2084
|
+
read_error: afterState.editor?.read_error || null
|
|
2085
|
+
});
|
|
2086
|
+
if (exactMatch) {
|
|
2087
|
+
return {
|
|
2088
|
+
ok: true,
|
|
2089
|
+
value: editorText,
|
|
2090
|
+
editor: afterState.editor || preInsertEditor || focusedEditor,
|
|
2091
|
+
attempts,
|
|
2092
|
+
deterministic_fallback_attempted: true,
|
|
2093
|
+
recovery: {
|
|
2094
|
+
mode: "deterministic_reacquired_editor"
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
lastState = {
|
|
2099
|
+
...afterState,
|
|
2100
|
+
editor_message_mismatch: true,
|
|
2101
|
+
editor_text_length: editorText.length
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
2107
|
+
attempts.push({
|
|
2108
|
+
stage: "deterministic_fallback",
|
|
2109
|
+
editor_node_id: fallbackState.editor.node_id,
|
|
2110
|
+
editor_selector: fallbackState.editor.selector || null,
|
|
2111
|
+
editor_trace_id: fallbackState.editor.attributes?.traceid || null,
|
|
2112
|
+
expected_length: expectedText.length,
|
|
2113
|
+
exact_match: false,
|
|
2114
|
+
recoverable_error: error?.message || String(error)
|
|
2115
|
+
});
|
|
2116
|
+
lastState = {
|
|
2117
|
+
...fallbackState,
|
|
2118
|
+
recoverable_error: error?.message || String(error),
|
|
2119
|
+
recoverable_phase: "set_editor_message_deterministic_fallback"
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
return {
|
|
2125
|
+
ok: false,
|
|
2126
|
+
error: editorFound ? "CHAT_EDITOR_MESSAGE_MISMATCH" : "CHAT_EDITOR_NOT_FOUND",
|
|
2127
|
+
editor_found: editorFound,
|
|
2128
|
+
attempts,
|
|
2129
|
+
deterministic_fallback_attempted: deterministicFallbackAttempted,
|
|
2130
|
+
state: lastState
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
1139
2133
|
|
|
1140
2134
|
export async function sendChatMessage(client, expectedText = "", {
|
|
1141
2135
|
timeoutMs = 8000,
|
|
@@ -1194,21 +2188,21 @@ export async function sendChatMessage(client, expectedText = "", {
|
|
|
1194
2188
|
};
|
|
1195
2189
|
}
|
|
1196
2190
|
|
|
1197
|
-
export async function clickChatAskResume(client, {
|
|
1198
|
-
timeoutMs = 8000,
|
|
1199
|
-
settleMs = 700,
|
|
1200
|
-
skipWhenAttachmentResumeAvailable = true
|
|
1201
|
-
} = {}) {
|
|
2191
|
+
export async function clickChatAskResume(client, {
|
|
2192
|
+
timeoutMs = 8000,
|
|
2193
|
+
settleMs = 700,
|
|
2194
|
+
skipWhenAttachmentResumeAvailable = true
|
|
2195
|
+
} = {}) {
|
|
1202
2196
|
const started = Date.now();
|
|
1203
2197
|
let lastState = null;
|
|
1204
2198
|
let lastDisabledAskResume = null;
|
|
1205
2199
|
while (Date.now() - started <= timeoutMs) {
|
|
1206
2200
|
const state = await readChatConversationReadyState(client);
|
|
1207
2201
|
lastState = state;
|
|
1208
|
-
if (skipWhenAttachmentResumeAvailable && state.attachment_resume_enabled) {
|
|
1209
|
-
return {
|
|
1210
|
-
ok: true,
|
|
1211
|
-
already_requested: true,
|
|
2202
|
+
if (skipWhenAttachmentResumeAvailable && state.attachment_resume_enabled) {
|
|
2203
|
+
return {
|
|
2204
|
+
ok: true,
|
|
2205
|
+
already_requested: true,
|
|
1212
2206
|
attachment_resume_available: true,
|
|
1213
2207
|
control: state.attachment_resume
|
|
1214
2208
|
};
|
|
@@ -1247,12 +2241,12 @@ export async function clickChatAskResume(client, {
|
|
|
1247
2241
|
}
|
|
1248
2242
|
await sleep(250);
|
|
1249
2243
|
}
|
|
1250
|
-
if (lastDisabledAskResume) {
|
|
1251
|
-
return {
|
|
1252
|
-
ok: false,
|
|
1253
|
-
already_requested:
|
|
1254
|
-
request_pending:
|
|
1255
|
-
error: "ASK_RESUME_BUTTON_DISABLED",
|
|
2244
|
+
if (lastDisabledAskResume) {
|
|
2245
|
+
return {
|
|
2246
|
+
ok: false,
|
|
2247
|
+
already_requested: false,
|
|
2248
|
+
request_pending: false,
|
|
2249
|
+
error: "ASK_RESUME_BUTTON_DISABLED",
|
|
1256
2250
|
control: lastDisabledAskResume,
|
|
1257
2251
|
state: lastState
|
|
1258
2252
|
};
|
|
@@ -1264,63 +2258,161 @@ export async function clickChatAskResume(client, {
|
|
|
1264
2258
|
};
|
|
1265
2259
|
}
|
|
1266
2260
|
|
|
1267
|
-
export
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
2261
|
+
export function resolveChatConfirmResumeTimeoutMs(value) {
|
|
2262
|
+
const hasExplicitValue = value !== null
|
|
2263
|
+
&& value !== undefined
|
|
2264
|
+
&& !(typeof value === "string" && value.trim() === "");
|
|
2265
|
+
if (!hasExplicitValue) return CHAT_REQUEST_RESUME_CONFIRM_DEFAULT_TIMEOUT_MS;
|
|
2266
|
+
const parsedValue = Number(value);
|
|
2267
|
+
return Number.isFinite(parsedValue)
|
|
2268
|
+
? Math.max(0, parsedValue)
|
|
2269
|
+
: CHAT_REQUEST_RESUME_CONFIRM_DEFAULT_TIMEOUT_MS;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
export async function clickChatConfirmRequestResume(client, {
|
|
2273
|
+
timeoutMs = null,
|
|
2274
|
+
settleMs = 900,
|
|
2275
|
+
pollIntervalMs = CHAT_REQUEST_RESUME_CONFIRM_POLL_INTERVAL_MS
|
|
2276
|
+
} = {}) {
|
|
2277
|
+
const effectiveTimeoutMs = resolveChatConfirmResumeTimeoutMs(timeoutMs);
|
|
2278
|
+
const parsedPollIntervalMs = Number(pollIntervalMs);
|
|
2279
|
+
const effectivePollIntervalMs = Number.isFinite(parsedPollIntervalMs)
|
|
2280
|
+
? Math.max(1, parsedPollIntervalMs)
|
|
2281
|
+
: CHAT_REQUEST_RESUME_CONFIRM_POLL_INTERVAL_MS;
|
|
2282
|
+
const started = Date.now();
|
|
2283
|
+
let lastPrompt = null;
|
|
2284
|
+
let lastTarget = null;
|
|
2285
|
+
let lastState = null;
|
|
2286
|
+
let discoveryMode = null;
|
|
2287
|
+
let pollCount = 0;
|
|
2288
|
+
let firstPromptObservedElapsedMs = null;
|
|
2289
|
+
let firstTargetObservedElapsedMs = null;
|
|
2290
|
+
let lastPromptReadError = null;
|
|
2291
|
+
let lastTargetReadError = null;
|
|
2292
|
+
let clickAttempted = false;
|
|
2293
|
+
let clickDispatched = false;
|
|
2294
|
+
const diagnostics = () => ({
|
|
2295
|
+
timeout_ms: effectiveTimeoutMs,
|
|
2296
|
+
poll_interval_ms: effectivePollIntervalMs,
|
|
2297
|
+
poll_count: pollCount,
|
|
2298
|
+
elapsed_ms: Math.max(0, Date.now() - started),
|
|
2299
|
+
discovery_mode: discoveryMode,
|
|
2300
|
+
prompt_observed: Boolean(lastPrompt),
|
|
2301
|
+
first_prompt_observed_elapsed_ms: firstPromptObservedElapsedMs,
|
|
2302
|
+
last_prompt_read_error: lastPromptReadError,
|
|
2303
|
+
target_observed: Boolean(lastTarget),
|
|
2304
|
+
first_target_observed_elapsed_ms: firstTargetObservedElapsedMs,
|
|
2305
|
+
last_target_read_error: lastTargetReadError,
|
|
2306
|
+
click_attempted: clickAttempted,
|
|
2307
|
+
click_dispatched: clickDispatched
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
while (true) {
|
|
2311
|
+
pollCount += 1;
|
|
2312
|
+
lastState = await readChatConversationReadyState(client);
|
|
2313
|
+
if (lastState.already_requested_resume || lastState.attachment_resume_enabled) {
|
|
2314
|
+
return {
|
|
2315
|
+
confirmed: true,
|
|
2316
|
+
assumed_requested: true,
|
|
2317
|
+
reason: lastState.attachment_resume_enabled
|
|
2318
|
+
? "attachment_resume_available"
|
|
2319
|
+
: "request_state_observed",
|
|
2320
|
+
control: lastState.requested_resume || lastState.attachment_resume || null,
|
|
2321
|
+
prompt: lastPrompt,
|
|
2322
|
+
state: lastState,
|
|
2323
|
+
...diagnostics()
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
const rootState = await getChatRoots(client);
|
|
2327
|
+
let discovered;
|
|
2328
|
+
try {
|
|
2329
|
+
discovered = await findAnchoredChatRequestResumeConfirmTarget(
|
|
2330
|
+
client,
|
|
2331
|
+
rootState.roots
|
|
2332
|
+
);
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
2335
|
+
lastTargetReadError = error?.message || String(error);
|
|
2336
|
+
discovered = {
|
|
2337
|
+
prompt: null,
|
|
2338
|
+
prompt_candidate: null,
|
|
2339
|
+
target: null,
|
|
2340
|
+
target_candidate: null,
|
|
2341
|
+
discovery_mode: null
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
lastPromptReadError = discovered.prompt_candidate?.read_error || lastPromptReadError;
|
|
2345
|
+
lastTargetReadError = discovered.target_candidate?.read_error || lastTargetReadError;
|
|
2346
|
+
if (discovered.prompt) {
|
|
2347
|
+
lastPrompt = discovered.prompt;
|
|
2348
|
+
discoveryMode = discovered.discovery_mode;
|
|
2349
|
+
if (firstPromptObservedElapsedMs === null) {
|
|
2350
|
+
firstPromptObservedElapsedMs = Math.max(0, Date.now() - started);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
lastTarget = discovered.target;
|
|
2354
|
+
if (lastTarget && firstTargetObservedElapsedMs === null) {
|
|
2355
|
+
firstTargetObservedElapsedMs = Math.max(0, Date.now() - started);
|
|
2356
|
+
}
|
|
2357
|
+
if (lastTarget?.node_id) {
|
|
2358
|
+
clickAttempted = true;
|
|
2359
|
+
try {
|
|
2360
|
+
if (lastTarget.center) {
|
|
2361
|
+
await clickPoint(client, lastTarget.center.x, lastTarget.center.y);
|
|
2362
|
+
} else {
|
|
2363
|
+
await clickNodeCenter(client, lastTarget.node_id, { scrollIntoView: true });
|
|
2364
|
+
}
|
|
2365
|
+
clickDispatched = true;
|
|
2366
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
2367
|
+
const afterState = await readChatConversationReadyState(client);
|
|
2368
|
+
return {
|
|
2369
|
+
confirmed: true,
|
|
2370
|
+
assumed_requested: Boolean(afterState.already_requested_resume),
|
|
2371
|
+
control: lastTarget,
|
|
2372
|
+
prompt: lastPrompt,
|
|
2373
|
+
state: afterState,
|
|
2374
|
+
...diagnostics()
|
|
2375
|
+
};
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
2378
|
+
const control = {
|
|
2379
|
+
...lastTarget,
|
|
2380
|
+
recoverable_error: error?.message || String(error),
|
|
2381
|
+
recoverable_phase: clickDispatched
|
|
2382
|
+
? "confirm_request_resume_post_click_state"
|
|
2383
|
+
: "confirm_request_resume_click"
|
|
2384
|
+
};
|
|
2385
|
+
return {
|
|
2386
|
+
confirmed: false,
|
|
2387
|
+
outcome_unknown: true,
|
|
2388
|
+
error: clickDispatched
|
|
2389
|
+
? "CONFIRM_CLICK_OUTCOME_UNKNOWN"
|
|
2390
|
+
: "CONFIRM_CLICK_FAILED",
|
|
2391
|
+
control,
|
|
2392
|
+
prompt: lastPrompt,
|
|
2393
|
+
state: lastState,
|
|
2394
|
+
...diagnostics()
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
const elapsedMs = Date.now() - started;
|
|
2399
|
+
if (elapsedMs >= effectiveTimeoutMs) break;
|
|
2400
|
+
await sleep(Math.min(
|
|
2401
|
+
effectivePollIntervalMs,
|
|
2402
|
+
Math.max(1, effectiveTimeoutMs - elapsedMs)
|
|
2403
|
+
));
|
|
2404
|
+
}
|
|
2405
|
+
return {
|
|
2406
|
+
confirmed: false,
|
|
2407
|
+
error: "CONFIRM_BUTTON_NOT_FOUND",
|
|
2408
|
+
control: null,
|
|
2409
|
+
prompt: lastPrompt,
|
|
2410
|
+
state: lastState,
|
|
2411
|
+
...diagnostics()
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
export async function getChatResumeRequestMessageState(client) {
|
|
1324
2416
|
const rootState = await getChatRoots(client);
|
|
1325
2417
|
let messageRoot = null;
|
|
1326
2418
|
for (const root of rootState.roots) {
|
|
@@ -1357,65 +2449,152 @@ export async function getChatResumeRequestMessageState(client) {
|
|
|
1357
2449
|
last_resume_attachment_text: attachmentMatching[attachmentMatching.length - 1] || "",
|
|
1358
2450
|
last_success_text: matching[matching.length - 1] || attachmentMatching[attachmentMatching.length - 1] || "",
|
|
1359
2451
|
recent: lines.slice(-12)
|
|
1360
|
-
};
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
export async function
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
export async function readChatRequestVerificationEvidence(client) {
|
|
2456
|
+
// Both readers reacquire the document root. Running them concurrently can
|
|
2457
|
+
// invalidate the first reader's frontend node IDs when the second
|
|
2458
|
+
// DOM.getDocument call refreshes Chrome's node-id namespace.
|
|
2459
|
+
const messageState = await getChatResumeRequestMessageState(client);
|
|
2460
|
+
const readyState = await readChatConversationReadyState(client);
|
|
2461
|
+
return {
|
|
2462
|
+
message_state: messageState,
|
|
2463
|
+
ready_state: readyState
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
const CHAT_GREETING_DELIVERY_PREFIX_PATTERN = /^(?:送达|已送达|已读|未读)\s*/u;
|
|
2468
|
+
|
|
2469
|
+
export function isExactChatGreetingMessageText(value = "", expectedValue = "") {
|
|
2470
|
+
const line = normalizeDetailText(value);
|
|
2471
|
+
const expected = normalizeDetailText(expectedValue);
|
|
2472
|
+
if (!line || !expected) return false;
|
|
2473
|
+
if (line === expected) return true;
|
|
2474
|
+
return normalizeDetailText(line.replace(CHAT_GREETING_DELIVERY_PREFIX_PATTERN, "")) === expected;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
export async function getChatGreetingMessageState(client, greetingText = "") {
|
|
2478
|
+
const expected = normalizeDetailText(greetingText);
|
|
2479
|
+
const rootState = await getChatRoots(client);
|
|
2480
|
+
let messageRoot = null;
|
|
2481
|
+
for (const root of rootState.roots) {
|
|
2482
|
+
for (const selector of CHAT_MESSAGE_LIST_SELECTORS) {
|
|
2483
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
2484
|
+
if (nodeIds.length) {
|
|
2485
|
+
messageRoot = {
|
|
2486
|
+
root,
|
|
2487
|
+
selector,
|
|
2488
|
+
node_id: nodeIds[0]
|
|
2489
|
+
};
|
|
2490
|
+
break;
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
if (messageRoot) break;
|
|
2494
|
+
}
|
|
2495
|
+
if (!messageRoot?.node_id || !expected) {
|
|
2496
|
+
return {
|
|
2497
|
+
ok: false,
|
|
2498
|
+
selector: messageRoot?.selector || null,
|
|
2499
|
+
expected_text: expected,
|
|
2500
|
+
exact_count: 0,
|
|
2501
|
+
recent: []
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
let text = "";
|
|
2505
|
+
try {
|
|
2506
|
+
text = htmlToText(await getOuterHTML(client, messageRoot.node_id));
|
|
2507
|
+
} catch {}
|
|
2508
|
+
const lines = text.split(/\r?\n/).map(normalizeDetailText).filter(Boolean);
|
|
2509
|
+
return {
|
|
2510
|
+
ok: Boolean(text),
|
|
2511
|
+
selector: messageRoot.selector,
|
|
2512
|
+
expected_text: expected,
|
|
2513
|
+
exact_count: lines.filter((line) => isExactChatGreetingMessageText(line, expected)).length,
|
|
2514
|
+
recent: lines.slice(-12)
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
export async function waitForChatResumeRequestMessage(client, {
|
|
1364
2519
|
baselineCount = 0,
|
|
1365
2520
|
baselineResumeAttachmentCount = 0,
|
|
1366
2521
|
timeoutMs = 6500,
|
|
1367
2522
|
intervalMs = 260
|
|
1368
|
-
} = {}) {
|
|
1369
|
-
const started = Date.now();
|
|
1370
|
-
let state = null;
|
|
1371
|
-
while (
|
|
1372
|
-
state = await getChatResumeRequestMessageState(client);
|
|
1373
|
-
const observed = (
|
|
1374
|
-
state.count > baselineCount
|
|
2523
|
+
} = {}) {
|
|
2524
|
+
const started = Date.now();
|
|
2525
|
+
let state = null;
|
|
2526
|
+
while (true) {
|
|
2527
|
+
state = await getChatResumeRequestMessageState(client);
|
|
2528
|
+
const observed = (
|
|
2529
|
+
state.count > baselineCount
|
|
1375
2530
|
|| state.resume_attachment_count > baselineResumeAttachmentCount
|
|
1376
2531
|
);
|
|
1377
2532
|
if (observed) {
|
|
1378
2533
|
return {
|
|
1379
2534
|
observed: true,
|
|
1380
2535
|
elapsed_ms: Date.now() - started,
|
|
1381
|
-
state
|
|
1382
|
-
};
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
2536
|
+
state
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
const elapsedMs = Date.now() - started;
|
|
2540
|
+
if (elapsedMs >= timeoutMs) break;
|
|
2541
|
+
await sleep(Math.min(intervalMs, Math.max(0, timeoutMs - elapsedMs)));
|
|
2542
|
+
}
|
|
2543
|
+
return {
|
|
1387
2544
|
observed: false,
|
|
1388
2545
|
elapsed_ms: Date.now() - started,
|
|
1389
2546
|
state
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
export
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
export function resolveChatRequestVerificationTimeoutMs(value, maxAttempts = 3) {
|
|
2551
|
+
const parsedAttempts = Number(maxAttempts);
|
|
2552
|
+
const effectiveAttempts = Number.isFinite(parsedAttempts)
|
|
2553
|
+
? Math.max(0, parsedAttempts)
|
|
2554
|
+
: 3;
|
|
2555
|
+
const fallbackMs = Math.max(6500, Math.min(20000, effectiveAttempts * 3250));
|
|
2556
|
+
const hasExplicitValue = value !== null
|
|
2557
|
+
&& value !== undefined
|
|
2558
|
+
&& !(typeof value === "string" && value.trim() === "");
|
|
2559
|
+
if (!hasExplicitValue) return fallbackMs;
|
|
2560
|
+
const parsedValue = Number(value);
|
|
2561
|
+
return Number.isFinite(parsedValue)
|
|
2562
|
+
? Math.max(0, parsedValue)
|
|
2563
|
+
: fallbackMs;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
export async function requestChatResumeForPassedCandidate(client, {
|
|
2567
|
+
greetingText = "Hi同学,能麻烦发下简历吗?",
|
|
2568
|
+
maxAttempts = 3,
|
|
2569
|
+
askResumeTimeoutMs = 8000,
|
|
2570
|
+
confirmResumeTimeoutMs = null,
|
|
2571
|
+
confirmResumePollIntervalMs = CHAT_REQUEST_RESUME_CONFIRM_POLL_INTERVAL_MS,
|
|
2572
|
+
requestVerificationTimeoutMs = null,
|
|
2573
|
+
requestFinalVerificationSettleMs = 260,
|
|
2574
|
+
dryRun = false,
|
|
2575
|
+
skipWhenAttachmentResumeAvailable = true,
|
|
2576
|
+
skipGreeting = false,
|
|
2577
|
+
actionTransition = null
|
|
2578
|
+
} = {}) {
|
|
2579
|
+
const effectiveGreetingText = normalizeDetailText(greetingText) || "Hi同学,能麻烦发下简历吗?";
|
|
2580
|
+
const initialState = await readChatConversationReadyState(client);
|
|
2581
|
+
if (skipWhenAttachmentResumeAvailable && initialState.attachment_resume_enabled) {
|
|
2582
|
+
return {
|
|
2583
|
+
requested: false,
|
|
2584
|
+
skipped: true,
|
|
2585
|
+
reason: "attachment_resume_already_available",
|
|
2586
|
+
initial_state: initialState
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
if (initialState.already_requested_resume) {
|
|
2590
|
+
return {
|
|
2591
|
+
requested: false,
|
|
2592
|
+
skipped: true,
|
|
2593
|
+
reason: "resume_request_already_pending",
|
|
2594
|
+
initial_state: initialState
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
if (dryRun) {
|
|
1419
2598
|
return {
|
|
1420
2599
|
requested: false,
|
|
1421
2600
|
skipped: false,
|
|
@@ -1436,123 +2615,227 @@ export async function requestChatResumeForPassedCandidate(client, {
|
|
|
1436
2615
|
close_before_greeting: closeBeforeGreeting
|
|
1437
2616
|
};
|
|
1438
2617
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
:
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
await
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
2618
|
+
let greetingBaseline = null;
|
|
2619
|
+
let editorState = null;
|
|
2620
|
+
let sendResult = null;
|
|
2621
|
+
if (!skipGreeting) {
|
|
2622
|
+
greetingBaseline = await getChatGreetingMessageState(client, effectiveGreetingText);
|
|
2623
|
+
editorState = await setChatEditorMessage(client, effectiveGreetingText);
|
|
2624
|
+
if (!editorState.ok) {
|
|
2625
|
+
const error = new Error(editorState.error || "CHAT_EDITOR_MESSAGE_MISMATCH");
|
|
2626
|
+
error.code = editorState.error || "CHAT_EDITOR_MESSAGE_MISMATCH";
|
|
2627
|
+
error.retryable = true;
|
|
2628
|
+
error.attempts = Array.isArray(editorState.attempts)
|
|
2629
|
+
? editorState.attempts
|
|
2630
|
+
: [];
|
|
2631
|
+
throw error;
|
|
2632
|
+
}
|
|
2633
|
+
if (typeof actionTransition === "function") {
|
|
2634
|
+
await actionTransition("greeting_send_in_flight", {
|
|
2635
|
+
greeting_baseline_count: greetingBaseline.exact_count,
|
|
2636
|
+
greeting_evidence_readable: greetingBaseline.ok
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
sendResult = await sendChatMessage(client, effectiveGreetingText);
|
|
2640
|
+
if (!sendResult.sent) {
|
|
2641
|
+
if (typeof actionTransition === "function") {
|
|
2642
|
+
await actionTransition("outcome_unknown", {
|
|
2643
|
+
action: "greeting_send",
|
|
2644
|
+
reason: sendResult.error || sendResult.method || "unknown"
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
throw new Error(`CHAT_GREETING_SEND_FAILED:${sendResult.error || sendResult.method || "unknown"}`);
|
|
2648
|
+
}
|
|
2649
|
+
const postGreetingPageState = await getChatTopLevelState(client);
|
|
2650
|
+
if (postGreetingPageState.is_security_verification) {
|
|
2651
|
+
if (typeof actionTransition === "function") {
|
|
2652
|
+
await actionTransition("outcome_unknown", {
|
|
2653
|
+
action: "greeting_send",
|
|
2654
|
+
reason: "boss_security_verification_required"
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
throw makeBossSecurityVerificationRequiredError(
|
|
2658
|
+
postGreetingPageState,
|
|
2659
|
+
"chat_greeting_confirmation"
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
if (typeof actionTransition === "function") {
|
|
2663
|
+
await actionTransition("greeting_confirmed", {
|
|
2664
|
+
greeting_baseline_count: greetingBaseline.exact_count,
|
|
2665
|
+
send_method: sendResult.method || null
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
const attempts = [];
|
|
2671
|
+
const before = await getChatResumeRequestMessageState(client);
|
|
2672
|
+
if (typeof actionTransition === "function") {
|
|
2673
|
+
await actionTransition("request_in_flight", {
|
|
2674
|
+
request_baseline_count: before.count,
|
|
2675
|
+
resume_attachment_baseline_count: before.resume_attachment_count || 0
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
const askResult = await clickChatAskResume(client, {
|
|
2679
|
+
timeoutMs: askResumeTimeoutMs,
|
|
2680
|
+
skipWhenAttachmentResumeAvailable
|
|
2681
|
+
});
|
|
2682
|
+
let confirmResult = {
|
|
2683
|
+
confirmed: false,
|
|
2684
|
+
assumed_requested: Boolean(askResult.already_requested),
|
|
2685
|
+
skipped: true,
|
|
2686
|
+
reason: askResult.attachment_resume_available
|
|
2687
|
+
? "attachment_resume_already_available"
|
|
2688
|
+
: askResult.request_pending
|
|
2689
|
+
? "resume_request_already_pending"
|
|
2690
|
+
: askResult.ok
|
|
2691
|
+
? "already_requested"
|
|
2692
|
+
: (askResult.error || "ask_resume_not_clicked")
|
|
2693
|
+
};
|
|
2694
|
+
if (askResult.ok && !askResult.already_requested) {
|
|
2695
|
+
confirmResult = await clickChatConfirmRequestResume(client, {
|
|
2696
|
+
timeoutMs: confirmResumeTimeoutMs,
|
|
2697
|
+
pollIntervalMs: confirmResumePollIntervalMs
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
let messageCheck = (askResult.attachment_resume_available || askResult.request_pending || askResult.already_requested)
|
|
2701
|
+
? { observed: false, state: before, elapsed_ms: 0 }
|
|
2702
|
+
: await waitForChatResumeRequestMessage(client, {
|
|
2703
|
+
baselineCount: before.count,
|
|
2704
|
+
baselineResumeAttachmentCount: before.resume_attachment_count,
|
|
2705
|
+
timeoutMs: resolveChatRequestVerificationTimeoutMs(
|
|
2706
|
+
requestVerificationTimeoutMs,
|
|
2707
|
+
maxAttempts
|
|
2708
|
+
)
|
|
2709
|
+
});
|
|
2710
|
+
let finalReadyState = confirmResult?.assumed_requested
|
|
2711
|
+
? (confirmResult.state || null)
|
|
2712
|
+
: null;
|
|
2713
|
+
let passiveVerificationAttempted = false;
|
|
2714
|
+
if (!messageCheck.observed && !finalReadyState) {
|
|
2715
|
+
passiveVerificationAttempted = true;
|
|
2716
|
+
const settleMs = Math.max(0, Number(requestFinalVerificationSettleMs) || 0);
|
|
2717
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
2718
|
+
const {
|
|
2719
|
+
message_state: freshMessageState,
|
|
2720
|
+
ready_state: freshReadyState
|
|
2721
|
+
} = await readChatRequestVerificationEvidence(client);
|
|
2722
|
+
messageCheck = {
|
|
2723
|
+
observed: Boolean(
|
|
2724
|
+
freshMessageState.count > before.count
|
|
2725
|
+
|| freshMessageState.resume_attachment_count > before.resume_attachment_count
|
|
2726
|
+
),
|
|
2727
|
+
elapsed_ms: messageCheck.elapsed_ms + settleMs,
|
|
2728
|
+
state: freshMessageState,
|
|
2729
|
+
passive_final_read: true
|
|
2730
|
+
};
|
|
2731
|
+
finalReadyState = freshReadyState;
|
|
2732
|
+
}
|
|
2733
|
+
const postRequestPageState = await getChatTopLevelState(client);
|
|
2734
|
+
if (postRequestPageState.is_security_verification) {
|
|
2735
|
+
if (typeof actionTransition === "function") {
|
|
2736
|
+
await actionTransition("outcome_unknown", {
|
|
2737
|
+
action: "request_resume",
|
|
2738
|
+
reason: "boss_security_verification_required"
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
throw makeBossSecurityVerificationRequiredError(
|
|
2742
|
+
postRequestPageState,
|
|
2743
|
+
"chat_resume_request_confirmation"
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
const messageObserved = Boolean(
|
|
2747
|
+
(messageCheck.state?.count || 0) > (before.count || 0)
|
|
2748
|
+
);
|
|
2749
|
+
const attachmentObserved = Boolean(
|
|
2750
|
+
(messageCheck.state?.resume_attachment_count || 0) > (before.resume_attachment_count || 0)
|
|
2751
|
+
);
|
|
2752
|
+
const readyStateObserved = Boolean(
|
|
2753
|
+
finalReadyState?.already_requested_resume
|
|
2754
|
+
|| finalReadyState?.attachment_resume_enabled
|
|
2755
|
+
);
|
|
2756
|
+
const requestObserved = messageObserved || attachmentObserved || readyStateObserved;
|
|
2757
|
+
const confirmationSource = messageObserved
|
|
2758
|
+
? "message_delta"
|
|
2759
|
+
: attachmentObserved
|
|
2760
|
+
? "attachment_resume_delta"
|
|
2761
|
+
: finalReadyState?.already_requested_resume
|
|
2762
|
+
? "exact_requested_resume_state"
|
|
2763
|
+
: finalReadyState?.attachment_resume_enabled
|
|
2764
|
+
? "attachment_resume_available"
|
|
2765
|
+
: null;
|
|
2766
|
+
attempts.push({
|
|
2767
|
+
attempt: 1,
|
|
2768
|
+
ask_result: askResult,
|
|
2769
|
+
confirm_result: confirmResult,
|
|
2770
|
+
message_before_count: before.count,
|
|
2771
|
+
message_after_count: messageCheck.state?.count || 0,
|
|
2772
|
+
resume_attachment_before_count: before.resume_attachment_count || 0,
|
|
2773
|
+
resume_attachment_after_count: messageCheck.state?.resume_attachment_count || 0,
|
|
2774
|
+
message_observed: messageObserved,
|
|
2775
|
+
message_last_text: messageCheck.state?.last_success_text || messageCheck.state?.last_text || "",
|
|
2776
|
+
ready_state_observed: readyStateObserved,
|
|
2777
|
+
confirmation_source: confirmationSource,
|
|
2778
|
+
passive_verification_attempted: passiveVerificationAttempted
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
const satisfiedReason = askResult.attachment_resume_available
|
|
2782
|
+
? "attachment_resume_already_available"
|
|
2783
|
+
: (askResult.request_pending || askResult.already_requested)
|
|
2784
|
+
? "resume_request_already_pending"
|
|
2785
|
+
: requestObserved
|
|
2786
|
+
? "requested"
|
|
2787
|
+
: "";
|
|
2788
|
+
if (satisfiedReason) {
|
|
2789
|
+
if (typeof actionTransition === "function") {
|
|
2790
|
+
await actionTransition("request_confirmed", {
|
|
2791
|
+
reason: satisfiedReason,
|
|
2792
|
+
message_observed: messageObserved,
|
|
2793
|
+
request_ready_state_observed: readyStateObserved,
|
|
2794
|
+
request_confirmation_source: confirmationSource,
|
|
2795
|
+
request_after_count: messageCheck.state?.count || before.count,
|
|
2796
|
+
resume_attachment_after_count: messageCheck.state?.resume_attachment_count || before.resume_attachment_count || 0
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
return {
|
|
2800
|
+
requested: satisfiedReason === "requested",
|
|
2801
|
+
skipped: satisfiedReason !== "requested",
|
|
2802
|
+
reason: satisfiedReason,
|
|
2803
|
+
initial_state: initialState,
|
|
2804
|
+
close_before_greeting: closeBeforeGreeting,
|
|
2805
|
+
greeting_sent: !skipGreeting,
|
|
2806
|
+
greeting_skipped_from_journal: Boolean(skipGreeting),
|
|
2807
|
+
greeting_send_result: sendResult,
|
|
2808
|
+
attempts
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
if (typeof actionTransition === "function") {
|
|
2813
|
+
await actionTransition("outcome_unknown", {
|
|
2814
|
+
action: "request_resume",
|
|
2815
|
+
reason: "resume_request_message_not_observed",
|
|
2816
|
+
ask_result: {
|
|
2817
|
+
ok: Boolean(askResult.ok),
|
|
2818
|
+
error: askResult.error || null
|
|
2819
|
+
},
|
|
2820
|
+
confirm_result: {
|
|
2821
|
+
confirmed: Boolean(confirmResult.confirmed),
|
|
2822
|
+
error: confirmResult.error || null
|
|
2823
|
+
}
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
return {
|
|
2827
|
+
requested: false,
|
|
2828
|
+
skipped: false,
|
|
2829
|
+
reason: "resume_request_message_not_observed",
|
|
2830
|
+
outcome_unknown: true,
|
|
2831
|
+
initial_state: initialState,
|
|
2832
|
+
close_before_greeting: closeBeforeGreeting,
|
|
2833
|
+
greeting_sent: !skipGreeting,
|
|
2834
|
+
greeting_skipped_from_journal: Boolean(skipGreeting),
|
|
2835
|
+
greeting_send_result: sendResult,
|
|
2836
|
+
attempts
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
1556
2839
|
|
|
1557
2840
|
export async function closeChatResumeModal(client, {
|
|
1558
2841
|
attemptsLimit = 3
|