@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -0,0 +1,1369 @@
1
+ import {
2
+ clearFocusedInput,
3
+ clickNodeCenter,
4
+ clickPoint,
5
+ getFrameDocumentNodeId,
6
+ getAttributesMap,
7
+ getNodeBox,
8
+ getOuterHTML,
9
+ insertText,
10
+ pressKey,
11
+ querySelectorAll,
12
+ sleep
13
+ } from "../../core/browser/index.js";
14
+ import {
15
+ buildScreeningCandidateFromDetail,
16
+ htmlToText
17
+ } from "../../core/screening/index.js";
18
+ import {
19
+ CHAT_ACTIVE_CANDIDATE_SELECTORS,
20
+ CHAT_ASK_RESUME_BUTTON_SELECTORS,
21
+ CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
22
+ CHAT_CONFIRM_REQUEST_RESUME_SELECTORS,
23
+ CHAT_EDITOR_SELECTORS,
24
+ CHAT_MESSAGE_FILTER_SELECTORS,
25
+ CHAT_MESSAGE_LIST_SELECTORS,
26
+ CHAT_ONLINE_RESUME_BUTTON_SELECTORS,
27
+ CHAT_PRIMARY_LABEL_SELECTORS,
28
+ CHAT_PROFILE_NETWORK_PATTERNS,
29
+ CHAT_RESUME_CLOSE_SELECTORS,
30
+ CHAT_RESUME_CONTENT_SELECTORS,
31
+ CHAT_RESUME_IFRAME_SELECTORS,
32
+ CHAT_RESUME_MODAL_SELECTORS,
33
+ CHAT_SEND_BUTTON_SELECTORS
34
+ } from "./constants.js";
35
+ import {
36
+ getChatRoots,
37
+ queryFirstAcrossChatRoots
38
+ } from "./roots.js";
39
+ import {
40
+ assertChatShellNotResumeTopLevel,
41
+ getChatTopLevelState,
42
+ makeForbiddenChatResumeNavigationError
43
+ } from "./page-guard.js";
44
+
45
+ export function matchesChatProfileNetwork(url) {
46
+ return CHAT_PROFILE_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
47
+ }
48
+
49
+ export function createChatProfileNetworkRecorder(client) {
50
+ const events = [];
51
+ client.Network.responseReceived((event) => {
52
+ const url = event?.response?.url || "";
53
+ if (!matchesChatProfileNetwork(url)) return;
54
+ events.push({
55
+ requestId: event.requestId,
56
+ url,
57
+ status: event.response?.status,
58
+ mimeType: event.response?.mimeType,
59
+ type: event.type
60
+ });
61
+ });
62
+ if (typeof client.Network.loadingFinished === "function") {
63
+ client.Network.loadingFinished((event) => {
64
+ const found = events.find((item) => item.requestId === event.requestId);
65
+ if (!found) return;
66
+ found.loading_finished = true;
67
+ found.encodedDataLength = event.encodedDataLength;
68
+ });
69
+ }
70
+ if (typeof client.Network.loadingFailed === "function") {
71
+ client.Network.loadingFailed((event) => {
72
+ const found = events.find((item) => item.requestId === event.requestId);
73
+ if (!found) return;
74
+ found.loading_failed = true;
75
+ found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
76
+ });
77
+ }
78
+ return {
79
+ events,
80
+ clear() {
81
+ events.length = 0;
82
+ }
83
+ };
84
+ }
85
+
86
+ export async function waitForChatProfileNetworkEvents(recorder, {
87
+ minCount = 1,
88
+ requireLoaded = true,
89
+ timeoutMs = 8000,
90
+ intervalMs = 120
91
+ } = {}) {
92
+ const started = Date.now();
93
+ const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
94
+ let matching = [];
95
+ while (Date.now() - started <= timeoutMs) {
96
+ matching = events.filter((event) => (
97
+ !requireLoaded
98
+ || event.loading_finished === true
99
+ || event.loading_failed === true
100
+ ));
101
+ if (matching.length >= minCount) {
102
+ return {
103
+ ok: true,
104
+ elapsed_ms: Date.now() - started,
105
+ count: matching.length,
106
+ events: matching
107
+ };
108
+ }
109
+ await sleep(intervalMs);
110
+ }
111
+ return {
112
+ ok: false,
113
+ elapsed_ms: Date.now() - started,
114
+ count: matching.length,
115
+ events: matching,
116
+ total_event_count: events.length
117
+ };
118
+ }
119
+
120
+ export async function readChatProfileNetworkBodies(client, events = [], {
121
+ limit = 20
122
+ } = {}) {
123
+ const bodies = [];
124
+ for (const event of events.slice(0, limit)) {
125
+ try {
126
+ const body = await client.Network.getResponseBody({ requestId: event.requestId });
127
+ bodies.push({
128
+ ...event,
129
+ body,
130
+ body_length: String(body?.body || "").length
131
+ });
132
+ } catch (error) {
133
+ bodies.push({
134
+ ...event,
135
+ body_error: error?.message || String(error)
136
+ });
137
+ }
138
+ }
139
+ return bodies;
140
+ }
141
+
142
+ function normalizeDetailText(value = "") {
143
+ return String(value || "").replace(/\s+/g, " ").trim();
144
+ }
145
+
146
+ function chatCandidateIdFromAttributes(attributes = {}) {
147
+ return normalizeDetailText(
148
+ attributes["data-id"]
149
+ || attributes["data-geekid"]
150
+ || attributes["data-geek"]
151
+ || attributes["data-uid"]
152
+ || attributes.key
153
+ || attributes.id
154
+ || ""
155
+ );
156
+ }
157
+
158
+ async function hydrateActiveChatCandidate(client, activeCandidate = null) {
159
+ if (!activeCandidate?.node_id) return activeCandidate;
160
+ let attributes = {};
161
+ let outerHTML = "";
162
+ try {
163
+ [attributes, outerHTML] = await Promise.all([
164
+ getAttributesMap(client, activeCandidate.node_id),
165
+ getOuterHTML(client, activeCandidate.node_id)
166
+ ]);
167
+ } catch {}
168
+ return {
169
+ ...activeCandidate,
170
+ attributes,
171
+ candidate_id: chatCandidateIdFromAttributes(attributes) || null,
172
+ label: normalizeDetailText(htmlToText(outerHTML)),
173
+ outer_html_length: outerHTML.length
174
+ };
175
+ }
176
+
177
+ export async function waitForChatOnlineResumeButton(client, {
178
+ timeoutMs = 12000,
179
+ intervalMs = 250,
180
+ expectedCandidateId = ""
181
+ } = {}) {
182
+ const started = Date.now();
183
+ let lastState = null;
184
+ const expectedId = chatCandidateIdFromAttributes({ "data-id": expectedCandidateId });
185
+ while (Date.now() - started <= timeoutMs) {
186
+ const topLevelState = await getChatTopLevelState(client);
187
+ if (topLevelState.is_forbidden_resume_top_level) {
188
+ return {
189
+ forbidden_top_level_navigation: true,
190
+ top_level_state: topLevelState
191
+ };
192
+ }
193
+ const rootState = await getChatRoots(client);
194
+ const target = await findVisibleTarget(client, rootState.roots, CHAT_ONLINE_RESUME_BUTTON_SELECTORS);
195
+ const activeCandidate = await hydrateActiveChatCandidate(
196
+ client,
197
+ await queryFirstAcrossChatRoots(client, rootState.roots, CHAT_ACTIVE_CANDIDATE_SELECTORS)
198
+ );
199
+ const activeCandidateId = activeCandidate?.candidate_id || "";
200
+ const candidateSelectionVerified = expectedId
201
+ ? activeCandidateId === expectedId
202
+ : undefined;
203
+ lastState = {
204
+ roots: rootState.roots,
205
+ target,
206
+ activeCandidate,
207
+ expected_candidate_id: expectedId || null,
208
+ active_candidate_id: activeCandidateId || null,
209
+ candidate_selection_verified: candidateSelectionVerified
210
+ };
211
+ if (target && (!expectedId || candidateSelectionVerified)) {
212
+ return {
213
+ ok: true,
214
+ elapsed_ms: Date.now() - started,
215
+ ...lastState
216
+ };
217
+ }
218
+ await sleep(intervalMs);
219
+ }
220
+ return {
221
+ ok: false,
222
+ reason: expectedId && lastState?.candidate_selection_verified === false
223
+ ? "active_candidate_mismatch"
224
+ : "online_resume_button_unavailable",
225
+ elapsed_ms: Date.now() - started,
226
+ ...lastState
227
+ };
228
+ }
229
+
230
+ export async function selectChatCandidate(client, cardNodeId, {
231
+ timeoutMs = 12000,
232
+ settleMs = 1200
233
+ } = {}) {
234
+ const cardBox = await clickNodeCenter(client, cardNodeId, {
235
+ scrollIntoView: true
236
+ });
237
+ if (settleMs > 0) await sleep(settleMs);
238
+ const ready = await waitForChatOnlineResumeButton(client, { timeoutMs });
239
+ return {
240
+ card_box: cardBox,
241
+ ready
242
+ };
243
+ }
244
+
245
+ function hasActiveSignal(attributes = {}, outerHTML = "") {
246
+ return /\b(active|selected|current|curr)\b/i.test(String(attributes.class || ""))
247
+ || normalizeDetailText(attributes["aria-selected"]).toLowerCase() === "true"
248
+ || normalizeDetailText(attributes["data-active"]).toLowerCase() === "true"
249
+ || /\b(active|selected|current|curr)\b/i.test(String(outerHTML || "").slice(0, 500));
250
+ }
251
+
252
+ function isDisabledSignal(attributes = {}, outerHTML = "") {
253
+ return attributes.disabled !== undefined
254
+ || normalizeDetailText(attributes["aria-disabled"]).toLowerCase() === "true"
255
+ || /\b(disabled|disable|is-disabled)\b/i.test([
256
+ attributes.class,
257
+ String(outerHTML || "").slice(0, 500)
258
+ ].join(" "));
259
+ }
260
+
261
+ function isAskResumeText(text = "") {
262
+ const normalized = normalizeDetailText(text);
263
+ return Boolean(
264
+ normalized === "求简历"
265
+ || normalized === "索要简历"
266
+ || normalized === "求附件简历"
267
+ || normalized.includes("求简历")
268
+ || normalized.includes("索要简历")
269
+ || normalized.includes("求附件简历")
270
+ );
271
+ }
272
+
273
+ function isRequestedResumeText(text = "") {
274
+ const normalized = normalizeDetailText(text);
275
+ return Boolean(
276
+ normalized === "已求简历"
277
+ || normalized === "已索要简历"
278
+ || normalized === "已申请"
279
+ || normalized === "已发送"
280
+ || normalized.includes("已求简历")
281
+ || normalized.includes("已索要简历")
282
+ || normalized.includes("已申请")
283
+ || normalized.includes("已发送")
284
+ );
285
+ }
286
+
287
+ function isAttachmentResumeText(text = "") {
288
+ const normalized = normalizeDetailText(text);
289
+ return Boolean(
290
+ normalized === "附件简历"
291
+ || (normalized.includes("附件简历") && !normalized.includes("求附件简历"))
292
+ );
293
+ }
294
+
295
+ function isAttachmentResumeTarget(target = {}) {
296
+ return isAttachmentResumeText(target.label)
297
+ || /resume-btn-file/i.test(String(target.attributes?.class || target.selector || ""));
298
+ }
299
+
300
+ function isConfirmText(text = "") {
301
+ const normalized = normalizeDetailText(text);
302
+ return Boolean(
303
+ normalized === "确定"
304
+ || normalized === "确认"
305
+ || normalized === "提交"
306
+ || normalized === "发送"
307
+ || normalized === "继续"
308
+ || normalized.includes("确定")
309
+ || normalized.includes("确认")
310
+ );
311
+ }
312
+
313
+ function isSendText(text = "") {
314
+ const normalized = normalizeDetailText(text);
315
+ return normalized === "发送" || normalized.includes("发送");
316
+ }
317
+
318
+ function isRecoverableNodeError(error) {
319
+ return /(?:Could not find node|No node with given id|Cannot find node|Could not compute box model)/i
320
+ .test(String(error?.message || error || ""));
321
+ }
322
+
323
+ async function readTarget(client, root, selector, nodeId) {
324
+ let attributes = {};
325
+ let outerHTML = "";
326
+ let readError = "";
327
+ try {
328
+ [attributes, outerHTML] = await Promise.all([
329
+ getAttributesMap(client, nodeId),
330
+ getOuterHTML(client, nodeId)
331
+ ]);
332
+ } catch (error) {
333
+ readError = error?.message || String(error);
334
+ }
335
+ const label = normalizeDetailText(htmlToText(outerHTML));
336
+ let box = null;
337
+ try {
338
+ box = await getNodeBox(client, nodeId);
339
+ } catch {}
340
+ return {
341
+ root: root.name,
342
+ root_node_id: root.nodeId,
343
+ selector,
344
+ node_id: nodeId,
345
+ label,
346
+ attributes,
347
+ disabled: isDisabledSignal(attributes, outerHTML),
348
+ active: hasActiveSignal(attributes, outerHTML),
349
+ visible: Boolean(box && box.rect.width > 2 && box.rect.height > 2),
350
+ center: box?.center || null,
351
+ rect: box?.rect || null,
352
+ outer_html_length: outerHTML.length,
353
+ read_error: readError || null
354
+ };
355
+ }
356
+
357
+ async function findVisibleMatchingTarget(client, roots, selectors, predicate) {
358
+ for (const root of roots) {
359
+ if (!root?.nodeId) continue;
360
+ for (const selector of selectors) {
361
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
362
+ for (const nodeId of nodeIds) {
363
+ const target = await readTarget(client, root, selector, nodeId);
364
+ if (!target.visible) continue;
365
+ if (predicate(target)) return target;
366
+ }
367
+ }
368
+ }
369
+ return null;
370
+ }
371
+
372
+ export async function selectChatPrimaryLabel(client, {
373
+ label = "全部",
374
+ timeoutMs = 8000,
375
+ intervalMs = 300,
376
+ settleMs = 700
377
+ } = {}) {
378
+ const started = Date.now();
379
+ let lastCandidates = [];
380
+ while (Date.now() - started <= timeoutMs) {
381
+ const rootState = await getChatRoots(client);
382
+ const candidates = [];
383
+ for (const root of rootState.roots) {
384
+ for (const selector of CHAT_PRIMARY_LABEL_SELECTORS) {
385
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
386
+ for (const nodeId of nodeIds) {
387
+ const target = await readTarget(client, root, selector, nodeId);
388
+ if (target.visible) candidates.push(target);
389
+ }
390
+ }
391
+ }
392
+ lastCandidates = candidates;
393
+ const matched = candidates.find((target) => (
394
+ target.label === label || target.label.startsWith(`${label}(`)
395
+ ));
396
+ if (matched?.active) {
397
+ return {
398
+ ok: true,
399
+ changed: false,
400
+ verified: true,
401
+ active_label: matched.label,
402
+ control: matched
403
+ };
404
+ }
405
+ if (matched) {
406
+ if (matched.center) {
407
+ await clickPoint(client, matched.center.x, matched.center.y);
408
+ } else {
409
+ await clickNodeCenter(client, matched.node_id, { scrollIntoView: true });
410
+ }
411
+ if (settleMs > 0) await sleep(settleMs);
412
+ return {
413
+ ok: true,
414
+ changed: true,
415
+ verified: true,
416
+ active_label: label,
417
+ control: matched
418
+ };
419
+ }
420
+ await sleep(intervalMs);
421
+ }
422
+ return {
423
+ ok: false,
424
+ error: `CHAT_PRIMARY_LABEL_NOT_FOUND:${label}`,
425
+ candidates: lastCandidates.map((item) => ({
426
+ label: item.label,
427
+ selector: item.selector,
428
+ active: item.active
429
+ }))
430
+ };
431
+ }
432
+
433
+ export async function selectChatMessageFilter(client, {
434
+ startFrom = "unread",
435
+ timeoutMs = 8000,
436
+ intervalMs = 300,
437
+ settleMs = 900
438
+ } = {}) {
439
+ const label = startFrom === "all" ? "全部" : "未读";
440
+ const started = Date.now();
441
+ let lastCandidates = [];
442
+ while (Date.now() - started <= timeoutMs) {
443
+ const rootState = await getChatRoots(client);
444
+ const candidates = [];
445
+ for (const root of rootState.roots) {
446
+ for (const selector of CHAT_MESSAGE_FILTER_SELECTORS) {
447
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
448
+ for (const nodeId of nodeIds) {
449
+ const target = await readTarget(client, root, selector, nodeId);
450
+ if (target.visible && target.label === label) candidates.push(target);
451
+ }
452
+ }
453
+ }
454
+ lastCandidates = candidates;
455
+ const active = candidates.find((target) => target.active);
456
+ if (active) {
457
+ return {
458
+ ok: true,
459
+ changed: false,
460
+ verified: true,
461
+ active_label: active.label,
462
+ control: active
463
+ };
464
+ }
465
+ const matched = candidates[0];
466
+ if (matched) {
467
+ if (matched.center) {
468
+ await clickPoint(client, matched.center.x, matched.center.y);
469
+ } else {
470
+ await clickNodeCenter(client, matched.node_id, { scrollIntoView: true });
471
+ }
472
+ if (settleMs > 0) await sleep(settleMs);
473
+ return {
474
+ ok: true,
475
+ changed: true,
476
+ verified: true,
477
+ active_label: label,
478
+ control: matched
479
+ };
480
+ }
481
+ await sleep(intervalMs);
482
+ }
483
+ return {
484
+ ok: false,
485
+ error: `CHAT_MESSAGE_FILTER_NOT_FOUND:${label}`,
486
+ candidates: lastCandidates.map((item) => ({
487
+ label: item.label,
488
+ selector: item.selector,
489
+ active: item.active
490
+ }))
491
+ };
492
+ }
493
+
494
+ export async function waitForChatResumeModal(client, {
495
+ timeoutMs = 12000,
496
+ intervalMs = 250
497
+ } = {}) {
498
+ const started = Date.now();
499
+ let lastState = null;
500
+ while (Date.now() - started <= timeoutMs) {
501
+ const topLevelState = await getChatTopLevelState(client);
502
+ if (topLevelState.is_forbidden_resume_top_level) {
503
+ return {
504
+ forbidden_top_level_navigation: true,
505
+ top_level_state: topLevelState
506
+ };
507
+ }
508
+ const rootState = await getChatRoots(client);
509
+ const popup = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_MODAL_SELECTORS);
510
+ const content = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_CONTENT_SELECTORS);
511
+ const resumeIframe = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_IFRAME_SELECTORS);
512
+ lastState = {
513
+ roots: rootState.roots,
514
+ popup,
515
+ content,
516
+ resumeIframe
517
+ };
518
+ if (popup || content || resumeIframe) return lastState;
519
+ await sleep(intervalMs);
520
+ }
521
+ return lastState;
522
+ }
523
+
524
+ export async function readChatResumeHtml(client, resumeState) {
525
+ let popupHTML = "";
526
+ let contentHTML = "";
527
+ let resumeIframeHTML = "";
528
+ let resumeIframeDocumentNodeId = null;
529
+
530
+ if (resumeState?.popup?.node_id) {
531
+ popupHTML = await getOuterHTML(client, resumeState.popup.node_id);
532
+ }
533
+
534
+ if (resumeState?.content?.node_id && resumeState.content.node_id !== resumeState?.popup?.node_id) {
535
+ contentHTML = await getOuterHTML(client, resumeState.content.node_id);
536
+ }
537
+
538
+ if (resumeState?.resumeIframe?.node_id) {
539
+ resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, resumeState.resumeIframe.node_id);
540
+ resumeIframeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
541
+ }
542
+
543
+ return {
544
+ popupHTML,
545
+ contentHTML,
546
+ resumeIframeHTML,
547
+ resumeIframeDocumentNodeId,
548
+ popupText: htmlToText(popupHTML),
549
+ contentText: htmlToText(contentHTML),
550
+ resumeIframeText: htmlToText(resumeIframeHTML)
551
+ };
552
+ }
553
+
554
+ function emptyChatResumeHtml(readError = null) {
555
+ return {
556
+ popupHTML: "",
557
+ contentHTML: "",
558
+ resumeIframeHTML: "",
559
+ resumeIframeDocumentNodeId: null,
560
+ popupText: "",
561
+ contentText: "",
562
+ resumeIframeText: "",
563
+ readError: readError?.message || null
564
+ };
565
+ }
566
+
567
+ export async function waitForChatResumeContent(client, {
568
+ minTextLength = 120,
569
+ timeoutMs = 15000,
570
+ intervalMs = 250
571
+ } = {}) {
572
+ const started = Date.now();
573
+ let lastState = null;
574
+ let lastHtml = null;
575
+ let lastError = null;
576
+ while (Date.now() - started <= timeoutMs) {
577
+ try {
578
+ lastState = await waitForChatResumeModal(client, {
579
+ timeoutMs: 700,
580
+ intervalMs: 100
581
+ });
582
+ if (lastState?.popup || lastState?.content || lastState?.resumeIframe) {
583
+ lastHtml = await readChatResumeHtml(client, lastState);
584
+ const textLength = [
585
+ lastHtml.popupText,
586
+ lastHtml.contentText,
587
+ lastHtml.resumeIframeText
588
+ ].join("\n").length;
589
+ if (textLength >= minTextLength) {
590
+ return {
591
+ ok: true,
592
+ elapsed_ms: Date.now() - started,
593
+ text_length: textLength,
594
+ resume_state: lastState,
595
+ resume_html: lastHtml
596
+ };
597
+ }
598
+ }
599
+ } catch (error) {
600
+ lastError = error;
601
+ }
602
+ await sleep(intervalMs);
603
+ }
604
+
605
+ const textLength = [
606
+ lastHtml?.popupText,
607
+ lastHtml?.contentText,
608
+ lastHtml?.resumeIframeText
609
+ ].filter(Boolean).join("\n").length;
610
+ return {
611
+ ok: false,
612
+ elapsed_ms: Date.now() - started,
613
+ text_length: textLength,
614
+ resume_state: lastState,
615
+ resume_html: lastHtml,
616
+ error: lastError?.message || null
617
+ };
618
+ }
619
+
620
+ export async function openChatOnlineResume(client, {
621
+ timeoutMs = 15000,
622
+ attemptsLimit = 3,
623
+ settleMs = 1200
624
+ } = {}) {
625
+ const attempts = [];
626
+ for (let index = 0; index < attemptsLimit; index += 1) {
627
+ if (settleMs > 0) await sleep(settleMs);
628
+ await assertChatShellNotResumeTopLevel(client, {
629
+ context: "openChatOnlineResume:before_existing_modal_check"
630
+ });
631
+ const existingResumeState = await waitForChatResumeModal(client, {
632
+ timeoutMs: 500,
633
+ intervalMs: 100
634
+ });
635
+ if (existingResumeState?.forbidden_top_level_navigation) {
636
+ throw makeForbiddenChatResumeNavigationError(existingResumeState.top_level_state);
637
+ }
638
+ if (
639
+ existingResumeState?.popup
640
+ || existingResumeState?.content
641
+ || existingResumeState?.resumeIframe
642
+ ) {
643
+ attempts.push({
644
+ attempt: index + 1,
645
+ ok: true,
646
+ reused_existing_modal: true,
647
+ resume_popup_selector: existingResumeState?.popup?.selector || null,
648
+ resume_content_selector: existingResumeState?.content?.selector || null,
649
+ resume_iframe_selector: existingResumeState?.resumeIframe?.selector || null
650
+ });
651
+ return {
652
+ button: null,
653
+ button_html: "",
654
+ resume_state: existingResumeState,
655
+ attempts
656
+ };
657
+ }
658
+
659
+ const buttonState = await waitForChatOnlineResumeButton(client, {
660
+ timeoutMs: Math.min(timeoutMs, 8000)
661
+ });
662
+ if (!buttonState?.target?.node_id) {
663
+ attempts.push({
664
+ attempt: index + 1,
665
+ ok: false,
666
+ error: "ONLINE_RESUME_BUTTON_NOT_FOUND"
667
+ });
668
+ continue;
669
+ }
670
+
671
+ let buttonHTML = "";
672
+ try {
673
+ buttonHTML = await getOuterHTML(client, buttonState.target.node_id);
674
+ } catch {}
675
+
676
+ try {
677
+ if (buttonState.target.center) {
678
+ await clickPoint(client, buttonState.target.center.x, buttonState.target.center.y);
679
+ } else {
680
+ await clickNodeCenter(client, buttonState.target.node_id, {
681
+ scrollIntoView: true
682
+ });
683
+ }
684
+ } catch (error) {
685
+ attempts.push({
686
+ attempt: index + 1,
687
+ ok: false,
688
+ error: error?.message || String(error),
689
+ recoverable_stale_node: isRecoverableNodeError(error),
690
+ button_selector: buttonState.target.selector,
691
+ button_text: htmlToText(buttonHTML).slice(0, 120),
692
+ button_html_length: buttonHTML.length
693
+ });
694
+ if (isRecoverableNodeError(error)) {
695
+ await sleep(350);
696
+ continue;
697
+ }
698
+ throw error;
699
+ }
700
+ await assertChatShellNotResumeTopLevel(client, {
701
+ context: "openChatOnlineResume:after_online_resume_click"
702
+ });
703
+ const resumeState = await waitForChatResumeModal(client, {
704
+ timeoutMs: Math.max(2500, Math.floor(timeoutMs / attemptsLimit))
705
+ });
706
+ if (resumeState?.forbidden_top_level_navigation) {
707
+ throw makeForbiddenChatResumeNavigationError(resumeState.top_level_state);
708
+ }
709
+ attempts.push({
710
+ attempt: index + 1,
711
+ ok: Boolean(resumeState?.popup || resumeState?.content || resumeState?.resumeIframe),
712
+ button_selector: buttonState.target.selector,
713
+ button_text: htmlToText(buttonHTML).slice(0, 120),
714
+ button_html_length: buttonHTML.length,
715
+ resume_popup_selector: resumeState?.popup?.selector || null,
716
+ resume_content_selector: resumeState?.content?.selector || null,
717
+ resume_iframe_selector: resumeState?.resumeIframe?.selector || null
718
+ });
719
+ if (resumeState?.popup || resumeState?.content || resumeState?.resumeIframe) {
720
+ return {
721
+ button: buttonState.target,
722
+ button_html: buttonHTML,
723
+ resume_state: resumeState,
724
+ attempts
725
+ };
726
+ }
727
+ }
728
+
729
+ const error = new Error("Chat online resume modal did not open");
730
+ error.attempts = attempts;
731
+ throw error;
732
+ }
733
+
734
+ export async function readChatConversationReadyState(client) {
735
+ const rootState = await getChatRoots(client);
736
+ const onlineResume = await findVisibleMatchingTarget(
737
+ client,
738
+ rootState.roots,
739
+ CHAT_ONLINE_RESUME_BUTTON_SELECTORS,
740
+ (target) => target.label.includes("在线简历") && !target.disabled
741
+ );
742
+ const attachmentResume = await findVisibleMatchingTarget(
743
+ client,
744
+ rootState.roots,
745
+ CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
746
+ (target) => isAttachmentResumeText(target.label)
747
+ );
748
+ const askResume = await findVisibleMatchingTarget(
749
+ client,
750
+ rootState.roots,
751
+ CHAT_ASK_RESUME_BUTTON_SELECTORS,
752
+ (target) => isAskResumeText(target.label) && !isAttachmentResumeTarget(target)
753
+ );
754
+ const requestedResume = await findVisibleMatchingTarget(
755
+ client,
756
+ rootState.roots,
757
+ CHAT_ASK_RESUME_BUTTON_SELECTORS,
758
+ (target) => isRequestedResumeText(target.label)
759
+ );
760
+ const editor = await findVisibleMatchingTarget(
761
+ client,
762
+ rootState.roots,
763
+ CHAT_EDITOR_SELECTORS,
764
+ () => true
765
+ );
766
+ const sendButton = await findVisibleMatchingTarget(
767
+ client,
768
+ rootState.roots,
769
+ CHAT_SEND_BUTTON_SELECTORS,
770
+ (target) => isSendText(target.label) || /submit/i.test(String(target.attributes?.class || ""))
771
+ );
772
+ const resumeState = await waitForChatResumeModal(client, { timeoutMs: 300 });
773
+ return {
774
+ has_online_resume: Boolean(onlineResume),
775
+ online_resume: onlineResume,
776
+ has_ask_resume: Boolean(askResume),
777
+ ask_resume: askResume,
778
+ already_requested_resume: Boolean(requestedResume),
779
+ requested_resume: requestedResume,
780
+ has_attachment_resume: Boolean(attachmentResume),
781
+ attachment_resume_enabled: Boolean(attachmentResume && !attachmentResume.disabled),
782
+ attachment_resume: attachmentResume,
783
+ editor_visible: Boolean(editor),
784
+ editor,
785
+ send_button_visible: Boolean(sendButton),
786
+ send_button: sendButton,
787
+ resume_modal_open: Boolean(resumeState?.popup || resumeState?.content || resumeState?.resumeIframe),
788
+ panels_closed: !Boolean(resumeState?.popup || resumeState?.content || resumeState?.resumeIframe)
789
+ };
790
+ }
791
+
792
+ export async function setChatEditorMessage(client, message, {
793
+ timeoutMs = 8000
794
+ } = {}) {
795
+ const started = Date.now();
796
+ let lastState = null;
797
+ while (Date.now() - started <= timeoutMs) {
798
+ const state = await readChatConversationReadyState(client);
799
+ lastState = state;
800
+ if (state.editor?.node_id) {
801
+ try {
802
+ if (state.editor.center) {
803
+ await clickPoint(client, state.editor.center.x, state.editor.center.y);
804
+ } else {
805
+ await clickNodeCenter(client, state.editor.node_id, { scrollIntoView: true });
806
+ }
807
+ await sleep(120);
808
+ await clearFocusedInput(client);
809
+ await sleep(80);
810
+ await insertText(client, message);
811
+ await sleep(250);
812
+ const afterState = await readChatConversationReadyState(client);
813
+ const editorText = normalizeDetailText(afterState.editor?.label || "");
814
+ if (editorText.includes(normalizeDetailText(message))) {
815
+ return {
816
+ ok: true,
817
+ value: editorText,
818
+ editor: afterState.editor || state.editor
819
+ };
820
+ }
821
+ lastState = {
822
+ ...afterState,
823
+ editor_message_mismatch: true,
824
+ editor_text: editorText
825
+ };
826
+ } catch (error) {
827
+ if (!isRecoverableNodeError(error)) throw error;
828
+ lastState = {
829
+ ...state,
830
+ recoverable_error: error?.message || String(error),
831
+ recoverable_phase: "set_editor_message"
832
+ };
833
+ }
834
+ }
835
+ await sleep(250);
836
+ }
837
+ return {
838
+ ok: false,
839
+ error: "CHAT_EDITOR_NOT_FOUND",
840
+ state: lastState
841
+ };
842
+ }
843
+
844
+ export async function sendChatMessage(client, expectedText = "", {
845
+ timeoutMs = 8000,
846
+ settleMs = 800
847
+ } = {}) {
848
+ const started = Date.now();
849
+ let lastState = null;
850
+ while (Date.now() - started <= timeoutMs) {
851
+ const state = await readChatConversationReadyState(client);
852
+ lastState = state;
853
+ if (state.send_button?.node_id && !state.send_button.disabled) {
854
+ try {
855
+ if (state.send_button.center) {
856
+ await clickPoint(client, state.send_button.center.x, state.send_button.center.y);
857
+ } else {
858
+ await clickNodeCenter(client, state.send_button.node_id, { scrollIntoView: true });
859
+ }
860
+ if (settleMs > 0) await sleep(settleMs);
861
+ return {
862
+ sent: true,
863
+ method: "send-button",
864
+ control: state.send_button,
865
+ expected_text: expectedText
866
+ };
867
+ } catch (error) {
868
+ if (!isRecoverableNodeError(error)) throw error;
869
+ lastState = {
870
+ ...state,
871
+ recoverable_error: error?.message || String(error),
872
+ recoverable_phase: "send_button_click"
873
+ };
874
+ await sleep(250);
875
+ continue;
876
+ }
877
+ }
878
+ if (state.editor?.node_id) {
879
+ await pressKey(client, "Enter", {
880
+ code: "Enter",
881
+ windowsVirtualKeyCode: 13,
882
+ nativeVirtualKeyCode: 13
883
+ });
884
+ if (settleMs > 0) await sleep(settleMs);
885
+ return {
886
+ sent: true,
887
+ method: "enter",
888
+ expected_text: expectedText
889
+ };
890
+ }
891
+ await sleep(250);
892
+ }
893
+ return {
894
+ sent: false,
895
+ method: "none",
896
+ error: "CHAT_SEND_CONTROL_NOT_FOUND",
897
+ state: lastState
898
+ };
899
+ }
900
+
901
+ export async function clickChatAskResume(client, {
902
+ timeoutMs = 8000,
903
+ settleMs = 700
904
+ } = {}) {
905
+ const started = Date.now();
906
+ let lastState = null;
907
+ while (Date.now() - started <= timeoutMs) {
908
+ const state = await readChatConversationReadyState(client);
909
+ lastState = state;
910
+ if (state.attachment_resume_enabled) {
911
+ return {
912
+ ok: true,
913
+ already_requested: true,
914
+ attachment_resume_available: true,
915
+ control: state.attachment_resume
916
+ };
917
+ }
918
+ if (state.already_requested_resume) {
919
+ return {
920
+ ok: true,
921
+ already_requested: true,
922
+ control: state.requested_resume
923
+ };
924
+ }
925
+ if (state.ask_resume?.node_id && !state.ask_resume.disabled) {
926
+ try {
927
+ if (state.ask_resume.center) {
928
+ await clickPoint(client, state.ask_resume.center.x, state.ask_resume.center.y);
929
+ } else {
930
+ await clickNodeCenter(client, state.ask_resume.node_id, { scrollIntoView: true });
931
+ }
932
+ if (settleMs > 0) await sleep(settleMs);
933
+ return {
934
+ ok: true,
935
+ already_requested: false,
936
+ control: state.ask_resume
937
+ };
938
+ } catch (error) {
939
+ if (!isRecoverableNodeError(error)) throw error;
940
+ lastState = {
941
+ ...state,
942
+ recoverable_error: error?.message || String(error),
943
+ recoverable_phase: "ask_resume_click"
944
+ };
945
+ }
946
+ }
947
+ await sleep(250);
948
+ }
949
+ return {
950
+ ok: false,
951
+ error: "ASK_RESUME_BUTTON_NOT_FOUND",
952
+ state: lastState
953
+ };
954
+ }
955
+
956
+ export async function clickChatConfirmRequestResume(client, {
957
+ timeoutMs = 8000,
958
+ settleMs = 900
959
+ } = {}) {
960
+ const started = Date.now();
961
+ let lastTarget = null;
962
+ while (Date.now() - started <= timeoutMs) {
963
+ const state = await readChatConversationReadyState(client);
964
+ if (state.already_requested_resume) {
965
+ return {
966
+ confirmed: true,
967
+ assumed_requested: true,
968
+ state
969
+ };
970
+ }
971
+ const rootState = await getChatRoots(client);
972
+ const target = await findVisibleMatchingTarget(
973
+ client,
974
+ rootState.roots,
975
+ CHAT_CONFIRM_REQUEST_RESUME_SELECTORS,
976
+ (item) => isConfirmText(item.label) && !item.disabled
977
+ );
978
+ lastTarget = target;
979
+ if (target?.node_id) {
980
+ try {
981
+ if (target.center) {
982
+ await clickPoint(client, target.center.x, target.center.y);
983
+ } else {
984
+ await clickNodeCenter(client, target.node_id, { scrollIntoView: true });
985
+ }
986
+ if (settleMs > 0) await sleep(settleMs);
987
+ const afterState = await readChatConversationReadyState(client);
988
+ return {
989
+ confirmed: true,
990
+ assumed_requested: Boolean(afterState.already_requested_resume),
991
+ control: target,
992
+ state: afterState
993
+ };
994
+ } catch (error) {
995
+ if (!isRecoverableNodeError(error)) throw error;
996
+ lastTarget = {
997
+ ...target,
998
+ recoverable_error: error?.message || String(error),
999
+ recoverable_phase: "confirm_request_resume_click"
1000
+ };
1001
+ }
1002
+ }
1003
+ await sleep(250);
1004
+ }
1005
+ return {
1006
+ confirmed: false,
1007
+ error: "CONFIRM_BUTTON_NOT_FOUND",
1008
+ control: lastTarget
1009
+ };
1010
+ }
1011
+
1012
+ export async function getChatResumeRequestMessageState(client) {
1013
+ const rootState = await getChatRoots(client);
1014
+ let messageRoot = null;
1015
+ for (const root of rootState.roots) {
1016
+ for (const selector of CHAT_MESSAGE_LIST_SELECTORS) {
1017
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
1018
+ if (nodeIds.length) {
1019
+ messageRoot = {
1020
+ root,
1021
+ selector,
1022
+ node_id: nodeIds[0]
1023
+ };
1024
+ break;
1025
+ }
1026
+ }
1027
+ if (messageRoot) break;
1028
+ }
1029
+ const nodeId = messageRoot?.node_id || rootState.rootNodes.top;
1030
+ let text = "";
1031
+ try {
1032
+ text = htmlToText(await getOuterHTML(client, nodeId));
1033
+ } catch {}
1034
+ const lines = text.split(/\r?\n/).map(normalizeDetailText).filter(Boolean);
1035
+ const matching = lines.filter((line) => line.includes("简历请求已发送"));
1036
+ return {
1037
+ ok: Boolean(text),
1038
+ selector: messageRoot?.selector || "top",
1039
+ count: matching.length,
1040
+ last_text: matching[matching.length - 1] || lines[lines.length - 1] || "",
1041
+ recent: lines.slice(-12)
1042
+ };
1043
+ }
1044
+
1045
+ export async function waitForChatResumeRequestMessage(client, {
1046
+ baselineCount = 0,
1047
+ timeoutMs = 6500,
1048
+ intervalMs = 260
1049
+ } = {}) {
1050
+ const started = Date.now();
1051
+ let state = null;
1052
+ while (Date.now() - started <= timeoutMs) {
1053
+ state = await getChatResumeRequestMessageState(client);
1054
+ const observed = state.count > baselineCount
1055
+ || state.last_text.includes("简历请求已发送")
1056
+ || state.recent.some((item) => item.includes("简历请求已发送"));
1057
+ if (observed) {
1058
+ return {
1059
+ observed: true,
1060
+ elapsed_ms: Date.now() - started,
1061
+ state
1062
+ };
1063
+ }
1064
+ await sleep(intervalMs);
1065
+ }
1066
+ return {
1067
+ observed: false,
1068
+ elapsed_ms: Date.now() - started,
1069
+ state
1070
+ };
1071
+ }
1072
+
1073
+ export async function requestChatResumeForPassedCandidate(client, {
1074
+ greetingText = "Hi同学,能麻烦发下简历吗?",
1075
+ maxAttempts = 3,
1076
+ dryRun = false
1077
+ } = {}) {
1078
+ const effectiveGreetingText = normalizeDetailText(greetingText) || "Hi同学,能麻烦发下简历吗?";
1079
+ const initialState = await readChatConversationReadyState(client);
1080
+ if (initialState.attachment_resume_enabled) {
1081
+ return {
1082
+ requested: false,
1083
+ skipped: true,
1084
+ reason: "attachment_resume_already_available",
1085
+ initial_state: initialState
1086
+ };
1087
+ }
1088
+ if (initialState.already_requested_resume) {
1089
+ return {
1090
+ requested: true,
1091
+ skipped: true,
1092
+ reason: "resume_already_requested",
1093
+ initial_state: initialState
1094
+ };
1095
+ }
1096
+ if (dryRun) {
1097
+ return {
1098
+ requested: false,
1099
+ skipped: false,
1100
+ reason: "dry_run",
1101
+ initial_state: initialState,
1102
+ would_send_greeting: true,
1103
+ would_click_ask_resume: true
1104
+ };
1105
+ }
1106
+
1107
+ const closeBeforeGreeting = await closeChatResumeModal(client, { attemptsLimit: 3 });
1108
+ if (!closeBeforeGreeting.closed) {
1109
+ return {
1110
+ requested: false,
1111
+ skipped: true,
1112
+ reason: "resume_modal_close_failed_before_request",
1113
+ initial_state: initialState,
1114
+ close_before_greeting: closeBeforeGreeting
1115
+ };
1116
+ }
1117
+ const editorState = await setChatEditorMessage(client, effectiveGreetingText);
1118
+ if (!editorState.ok) {
1119
+ throw new Error("CHAT_EDITOR_MESSAGE_MISMATCH");
1120
+ }
1121
+ const sendResult = await sendChatMessage(client, effectiveGreetingText);
1122
+ if (!sendResult.sent) {
1123
+ throw new Error(`CHAT_GREETING_SEND_FAILED:${sendResult.error || sendResult.method || "unknown"}`);
1124
+ }
1125
+
1126
+ const attempts = [];
1127
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1128
+ const before = await getChatResumeRequestMessageState(client);
1129
+ const askResult = await clickChatAskResume(client);
1130
+ let confirmResult = {
1131
+ confirmed: false,
1132
+ assumed_requested: Boolean(askResult.already_requested),
1133
+ skipped: true,
1134
+ reason: askResult.attachment_resume_available
1135
+ ? "attachment_resume_already_available"
1136
+ : askResult.ok
1137
+ ? "already_requested"
1138
+ : (askResult.error || "ask_resume_not_clicked")
1139
+ };
1140
+ if (askResult.attachment_resume_available) {
1141
+ attempts.push({
1142
+ attempt: attempt + 1,
1143
+ ask_result: askResult,
1144
+ confirm_result: confirmResult,
1145
+ message_before_count: before.count,
1146
+ message_after_count: before.count,
1147
+ message_observed: false,
1148
+ message_last_text: before.last_text || ""
1149
+ });
1150
+ return {
1151
+ requested: false,
1152
+ skipped: true,
1153
+ reason: "attachment_resume_already_available",
1154
+ initial_state: initialState,
1155
+ close_before_greeting: closeBeforeGreeting,
1156
+ greeting_sent: true,
1157
+ greeting_send_result: sendResult,
1158
+ attempts
1159
+ };
1160
+ }
1161
+ if (askResult.ok && !askResult.already_requested) {
1162
+ confirmResult = await clickChatConfirmRequestResume(client);
1163
+ }
1164
+ const messageCheck = await waitForChatResumeRequestMessage(client, {
1165
+ baselineCount: before.count
1166
+ });
1167
+ const messageObserved = Boolean(messageCheck.observed);
1168
+ attempts.push({
1169
+ attempt: attempt + 1,
1170
+ ask_result: askResult,
1171
+ confirm_result: confirmResult,
1172
+ message_before_count: before.count,
1173
+ message_after_count: messageCheck.state?.count || 0,
1174
+ message_observed: messageObserved,
1175
+ message_last_text: messageCheck.state?.last_text || ""
1176
+ });
1177
+ if (messageObserved) {
1178
+ return {
1179
+ requested: true,
1180
+ skipped: false,
1181
+ reason: "requested",
1182
+ initial_state: initialState,
1183
+ close_before_greeting: closeBeforeGreeting,
1184
+ greeting_sent: true,
1185
+ greeting_send_result: sendResult,
1186
+ attempts
1187
+ };
1188
+ }
1189
+ await sleep(900);
1190
+ }
1191
+
1192
+ return {
1193
+ requested: false,
1194
+ skipped: false,
1195
+ reason: "resume_request_message_not_observed",
1196
+ initial_state: initialState,
1197
+ close_before_greeting: closeBeforeGreeting,
1198
+ greeting_sent: true,
1199
+ greeting_send_result: sendResult,
1200
+ attempts
1201
+ };
1202
+ }
1203
+
1204
+ export async function closeChatResumeModal(client, {
1205
+ attemptsLimit = 3
1206
+ } = {}) {
1207
+ const attempts = [];
1208
+ for (let index = 0; index < attemptsLimit; index += 1) {
1209
+ const existingState = await waitForChatResumeModal(client, { timeoutMs: 500 });
1210
+ if (!existingState?.popup && !existingState?.content && !existingState?.resumeIframe) {
1211
+ return {
1212
+ closed: true,
1213
+ attempts
1214
+ };
1215
+ }
1216
+
1217
+ const rootState = await getChatRoots(client);
1218
+ const closeTarget = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_CLOSE_SELECTORS);
1219
+ if (closeTarget) {
1220
+ try {
1221
+ await clickPoint(client, closeTarget.center.x, closeTarget.center.y);
1222
+ attempts.push({
1223
+ mode: "close-selector",
1224
+ selector: closeTarget.selector,
1225
+ root: closeTarget.root
1226
+ });
1227
+ } catch (error) {
1228
+ attempts.push({
1229
+ mode: "close-selector-error",
1230
+ selector: closeTarget.selector,
1231
+ root: closeTarget.root,
1232
+ error: error?.message || String(error)
1233
+ });
1234
+ }
1235
+ await sleep(700);
1236
+ } else {
1237
+ await pressEscape(client);
1238
+ attempts.push({ mode: "Escape" });
1239
+ await sleep(700);
1240
+ }
1241
+
1242
+ let state = await waitForChatResumeModal(client, { timeoutMs: 1000 });
1243
+ if (!state?.popup && !state?.content && !state?.resumeIframe) {
1244
+ return {
1245
+ closed: true,
1246
+ attempts
1247
+ };
1248
+ }
1249
+
1250
+ await pressEscape(client);
1251
+ attempts.push({ mode: "Escape-fallback" });
1252
+ await sleep(700);
1253
+
1254
+ state = await waitForChatResumeModal(client, { timeoutMs: 1000 });
1255
+ if (!state?.popup && !state?.content && !state?.resumeIframe) {
1256
+ return {
1257
+ closed: true,
1258
+ attempts
1259
+ };
1260
+ }
1261
+ }
1262
+
1263
+ return {
1264
+ closed: false,
1265
+ attempts
1266
+ };
1267
+ }
1268
+
1269
+ export async function extractChatProfileCandidate(client, {
1270
+ cardCandidate,
1271
+ cardNodeId,
1272
+ resumeState,
1273
+ resumeHtml: providedResumeHtml = null,
1274
+ networkEvents = [],
1275
+ targetUrl = "",
1276
+ closeResume = true
1277
+ } = {}) {
1278
+ await sleep(1000);
1279
+ const networkBodies = await readChatProfileNetworkBodies(client, networkEvents);
1280
+ let resumeHtml = providedResumeHtml || null;
1281
+ if (!resumeHtml) {
1282
+ try {
1283
+ resumeHtml = await readChatResumeHtml(client, resumeState);
1284
+ } catch (error) {
1285
+ if (!networkEvents.length) throw error;
1286
+ resumeHtml = emptyChatResumeHtml(error);
1287
+ }
1288
+ }
1289
+ const detailText = [
1290
+ resumeHtml.popupText,
1291
+ resumeHtml.contentText,
1292
+ resumeHtml.resumeIframeText
1293
+ ].filter(Boolean).join("\n\n");
1294
+
1295
+ const detailCandidateResult = buildScreeningCandidateFromDetail({
1296
+ domain: "chat",
1297
+ source: "chat-live-cdp-profile",
1298
+ cardCandidate,
1299
+ detailText,
1300
+ networkBodies,
1301
+ metadata: {
1302
+ target_url: targetUrl,
1303
+ card_node_id: cardNodeId,
1304
+ resume_popup_selector: resumeState?.popup?.selector || null,
1305
+ resume_content_selector: resumeState?.content?.selector || null,
1306
+ resume_iframe_selector: resumeState?.resumeIframe?.selector || null,
1307
+ resume_iframe_document_node_id: resumeHtml.resumeIframeDocumentNodeId
1308
+ }
1309
+ });
1310
+
1311
+ let closeResult = null;
1312
+ if (closeResume) {
1313
+ closeResult = await closeChatResumeModal(client);
1314
+ }
1315
+
1316
+ return {
1317
+ candidate: detailCandidateResult.candidate,
1318
+ parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
1319
+ network_bodies: networkBodies,
1320
+ detail: {
1321
+ popup_text: resumeHtml.popupText,
1322
+ content_text: resumeHtml.contentText,
1323
+ resume_iframe_text: resumeHtml.resumeIframeText,
1324
+ popup_html_length: resumeHtml.popupHTML.length,
1325
+ content_html_length: resumeHtml.contentHTML.length,
1326
+ resume_iframe_html_length: resumeHtml.resumeIframeHTML.length
1327
+ },
1328
+ resume_html_read_error: resumeHtml.readError || null,
1329
+ close_result: closeResult
1330
+ };
1331
+ }
1332
+
1333
+ async function findVisibleTarget(client, roots, selectors) {
1334
+ let fallback = null;
1335
+ for (const root of roots) {
1336
+ if (!root?.nodeId) continue;
1337
+ for (const selector of selectors) {
1338
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
1339
+ for (const nodeId of nodeIds) {
1340
+ const target = {
1341
+ root: root.name,
1342
+ root_node_id: root.nodeId,
1343
+ selector,
1344
+ node_id: nodeId
1345
+ };
1346
+ if (!fallback) fallback = target;
1347
+ try {
1348
+ const box = await getNodeBox(client, nodeId);
1349
+ if (box.rect.width > 2 && box.rect.height > 2) {
1350
+ return {
1351
+ ...target,
1352
+ center: box.center,
1353
+ rect: box.rect
1354
+ };
1355
+ }
1356
+ } catch {}
1357
+ }
1358
+ }
1359
+ }
1360
+ return fallback;
1361
+ }
1362
+
1363
+ async function pressEscape(client) {
1364
+ await pressKey(client, "Escape", {
1365
+ code: "Escape",
1366
+ windowsVirtualKeyCode: 27,
1367
+ nativeVirtualKeyCode: 27
1368
+ });
1369
+ }