@reconcrap/boss-recommend-mcp 2.0.53 → 2.0.55
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/bin/boss-recommend-mcp.js +0 -0
- package/config/screening-config.example.json +1 -1
- package/package.json +120 -120
- package/src/cli.js +3121 -3121
- package/src/core/run/index.js +310 -310
- package/src/domains/chat/constants.js +229 -220
- package/src/domains/chat/detail.js +1686 -1653
- package/src/domains/chat/run-service.js +2039 -1979
- package/src/domains/common/account-rights-panel.js +314 -0
- package/src/domains/recommend/detail.js +546 -531
- package/src/domains/recommend/run-service.js +1245 -1180
- package/src/domains/recruit/detail.js +15 -0
- package/src/domains/recruit/run-service.js +78 -1
- package/src/recommend-mcp.js +1701 -1701
- package/src/run-state.js +358 -358
|
@@ -1,1668 +1,1701 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clearFocusedInput,
|
|
3
|
-
clickNodeCenter,
|
|
4
|
-
clickPoint,
|
|
5
|
-
DETERMINISTIC_CLICK_OPTIONS,
|
|
6
|
-
getFrameDocumentNodeId,
|
|
7
|
-
getAttributesMap,
|
|
8
|
-
getNodeBox,
|
|
9
|
-
getOuterHTML,
|
|
10
|
-
insertText,
|
|
11
|
-
pressKey,
|
|
12
|
-
querySelectorAll,
|
|
13
|
-
sleep
|
|
14
|
-
} from "../../core/browser/index.js";
|
|
1
|
+
import {
|
|
2
|
+
clearFocusedInput,
|
|
3
|
+
clickNodeCenter,
|
|
4
|
+
clickPoint,
|
|
5
|
+
DETERMINISTIC_CLICK_OPTIONS,
|
|
6
|
+
getFrameDocumentNodeId,
|
|
7
|
+
getAttributesMap,
|
|
8
|
+
getNodeBox,
|
|
9
|
+
getOuterHTML,
|
|
10
|
+
insertText,
|
|
11
|
+
pressKey,
|
|
12
|
+
querySelectorAll,
|
|
13
|
+
sleep
|
|
14
|
+
} from "../../core/browser/index.js";
|
|
15
15
|
import {
|
|
16
16
|
buildScreeningCandidateFromDetail,
|
|
17
17
|
htmlToText
|
|
18
18
|
} from "../../core/screening/index.js";
|
|
19
|
+
import {
|
|
20
|
+
closeBossAccountRightsBlockingPanel,
|
|
21
|
+
findBossAccountRightsBlockingPanel
|
|
22
|
+
} from "../common/account-rights-panel.js";
|
|
19
23
|
import {
|
|
20
24
|
CHAT_ACTIVE_CANDIDATE_SELECTORS,
|
|
21
25
|
CHAT_ASK_RESUME_BUTTON_SELECTORS,
|
|
22
|
-
CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
".
|
|
54
|
-
".
|
|
55
|
-
".
|
|
56
|
-
".
|
|
57
|
-
".exchange-
|
|
58
|
-
".
|
|
59
|
-
".
|
|
60
|
-
".
|
|
61
|
-
".
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
".
|
|
68
|
-
".
|
|
69
|
-
".
|
|
70
|
-
".
|
|
71
|
-
".exchange-
|
|
72
|
-
".
|
|
73
|
-
".
|
|
74
|
-
".
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|| attributes
|
|
105
|
-
||
|
|
106
|
-
|
|
107
|
-
||
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
||
|
|
115
|
-
||
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
error
|
|
119
|
-
error.
|
|
120
|
-
error.
|
|
121
|
-
error.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
found
|
|
148
|
-
found
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
found
|
|
156
|
-
found
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|| attributes["data-
|
|
233
|
-
|| attributes
|
|
234
|
-
|| attributes
|
|
235
|
-
||
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
26
|
+
CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
|
|
27
|
+
CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
|
|
28
|
+
CHAT_BLOCKING_PANEL_TEXT_QUERIES,
|
|
29
|
+
CHAT_CONFIRM_REQUEST_RESUME_SELECTORS,
|
|
30
|
+
CHAT_EDITOR_SELECTORS,
|
|
31
|
+
CHAT_MESSAGE_FILTER_SELECTORS,
|
|
32
|
+
CHAT_MESSAGE_LIST_SELECTORS,
|
|
33
|
+
CHAT_ONLINE_RESUME_BUTTON_SELECTORS,
|
|
34
|
+
CHAT_PRIMARY_LABEL_SELECTORS,
|
|
35
|
+
CHAT_PROFILE_NETWORK_PATTERNS,
|
|
36
|
+
CHAT_RESUME_CLOSE_SELECTORS,
|
|
37
|
+
CHAT_RESUME_CONTENT_SELECTORS,
|
|
38
|
+
CHAT_RESUME_FAST_MODAL_SELECTORS,
|
|
39
|
+
CHAT_RESUME_IFRAME_SELECTORS,
|
|
40
|
+
CHAT_RESUME_MODAL_SELECTORS,
|
|
41
|
+
CHAT_SEND_BUTTON_SELECTORS
|
|
42
|
+
} from "./constants.js";
|
|
43
|
+
import {
|
|
44
|
+
getChatRoots,
|
|
45
|
+
queryFirstAcrossChatRoots
|
|
46
|
+
} from "./roots.js";
|
|
47
|
+
import {
|
|
48
|
+
assertChatShellNotResumeTopLevel,
|
|
49
|
+
getChatTopLevelState,
|
|
50
|
+
isForbiddenChatResumeTopLevelUrl,
|
|
51
|
+
makeForbiddenChatResumeNavigationError
|
|
52
|
+
} from "./page-guard.js";
|
|
53
|
+
|
|
54
|
+
export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
|
|
55
|
+
|
|
56
|
+
const CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS = Object.freeze([
|
|
57
|
+
".conversation-main",
|
|
58
|
+
".conversation-editor",
|
|
59
|
+
".chat-message-list",
|
|
60
|
+
".toolbar-box-right",
|
|
61
|
+
".operate-exchange-left",
|
|
62
|
+
".operate-icon-item",
|
|
63
|
+
".exchange-tooltip",
|
|
64
|
+
".boss-popup__wrapper",
|
|
65
|
+
".boss-dialog",
|
|
66
|
+
".dialog-wrap.active",
|
|
67
|
+
".geek-detail-modal"
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const CHAT_REQUESTED_RESUME_SCOPE_SELECTORS = Object.freeze([
|
|
71
|
+
".chat-message-list",
|
|
72
|
+
".conversation-editor",
|
|
73
|
+
".conversation-main",
|
|
74
|
+
".toolbar-box-right",
|
|
75
|
+
".operate-exchange-left",
|
|
76
|
+
".operate-icon-item",
|
|
77
|
+
".exchange-tooltip",
|
|
78
|
+
".boss-popup__wrapper",
|
|
79
|
+
".boss-dialog",
|
|
80
|
+
".dialog-wrap.active"
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
export function matchesChatProfileNetwork(url) {
|
|
84
|
+
return CHAT_PROFILE_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function looksLikeForbiddenChatResumePath(value = "") {
|
|
88
|
+
const normalized = String(value || "");
|
|
89
|
+
return isForbiddenChatResumeTopLevelUrl(normalized)
|
|
90
|
+
|| /(?:^|["'\s=])(?:https?:\/\/[^"'\s>]*zhipin\.com)?\/web\/frame\/c-resume(?:[/?#"' >]|$)/i
|
|
91
|
+
.test(normalized);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractFirstHtmlAttribute(html = "", names = []) {
|
|
95
|
+
const source = String(html || "");
|
|
96
|
+
for (const name of names) {
|
|
97
|
+
const escaped = String(name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
98
|
+
const regex = new RegExp(`${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, "i");
|
|
99
|
+
const match = source.match(regex);
|
|
100
|
+
if (match) return match[1] ?? match[2] ?? match[3] ?? "";
|
|
101
|
+
}
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isUnsafeChatOnlineResumeTarget(target = {}, buttonHTML = "") {
|
|
106
|
+
const attributes = target?.attributes || {};
|
|
107
|
+
const href = attributes.href
|
|
108
|
+
|| attributes["data-href"]
|
|
109
|
+
|| attributes["data-url"]
|
|
110
|
+
|| attributes.url
|
|
111
|
+
|| extractFirstHtmlAttribute(buttonHTML, ["href", "data-href", "data-url", "url"]);
|
|
112
|
+
return looksLikeForbiddenChatResumePath(href)
|
|
113
|
+
|| looksLikeForbiddenChatResumePath(buttonHTML);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function makeUnsafeChatOnlineResumeLinkError(target = {}, buttonHTML = "") {
|
|
117
|
+
const href = target?.attributes?.href
|
|
118
|
+
|| target?.attributes?.["data-href"]
|
|
119
|
+
|| target?.attributes?.["data-url"]
|
|
120
|
+
|| extractFirstHtmlAttribute(buttonHTML, ["href", "data-href", "data-url", "url"])
|
|
121
|
+
|| null;
|
|
122
|
+
const error = new Error("CHAT_UNSAFE_ONLINE_RESUME_LINK: refusing to click an online resume link that can navigate the chat tab to /web/frame/c-resume/");
|
|
123
|
+
error.code = CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE;
|
|
124
|
+
error.href = href;
|
|
125
|
+
error.button_selector = target?.selector || null;
|
|
126
|
+
error.button_text = htmlToText(buttonHTML).slice(0, 120);
|
|
127
|
+
error.button_html_length = String(buttonHTML || "").length;
|
|
128
|
+
return error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isUnsafeChatOnlineResumeLinkError(error) {
|
|
132
|
+
return error?.code === CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE
|
|
133
|
+
|| /CHAT_UNSAFE_ONLINE_RESUME_LINK/i.test(String(error?.message || error || ""));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createChatProfileNetworkRecorder(client) {
|
|
137
|
+
const events = [];
|
|
138
|
+
client.Network.responseReceived((event) => {
|
|
139
|
+
const url = event?.response?.url || "";
|
|
140
|
+
if (!matchesChatProfileNetwork(url)) return;
|
|
141
|
+
events.push({
|
|
142
|
+
requestId: event.requestId,
|
|
143
|
+
url,
|
|
144
|
+
status: event.response?.status,
|
|
145
|
+
mimeType: event.response?.mimeType,
|
|
146
|
+
type: event.type
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
if (typeof client.Network.loadingFinished === "function") {
|
|
150
|
+
client.Network.loadingFinished((event) => {
|
|
151
|
+
const found = events.find((item) => item.requestId === event.requestId);
|
|
152
|
+
if (!found) return;
|
|
153
|
+
found.loading_finished = true;
|
|
154
|
+
found.encodedDataLength = event.encodedDataLength;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (typeof client.Network.loadingFailed === "function") {
|
|
158
|
+
client.Network.loadingFailed((event) => {
|
|
159
|
+
const found = events.find((item) => item.requestId === event.requestId);
|
|
160
|
+
if (!found) return;
|
|
161
|
+
found.loading_failed = true;
|
|
162
|
+
found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
events,
|
|
167
|
+
clear() {
|
|
168
|
+
events.length = 0;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function waitForChatProfileNetworkEvents(recorder, {
|
|
174
|
+
minCount = 1,
|
|
175
|
+
requireLoaded = true,
|
|
176
|
+
timeoutMs = 8000,
|
|
177
|
+
intervalMs = 120
|
|
178
|
+
} = {}) {
|
|
179
|
+
const started = Date.now();
|
|
180
|
+
const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
|
|
181
|
+
let matching = [];
|
|
182
|
+
while (Date.now() - started <= timeoutMs) {
|
|
183
|
+
matching = events.filter((event) => (
|
|
184
|
+
!requireLoaded
|
|
185
|
+
|| event.loading_finished === true
|
|
186
|
+
|| event.loading_failed === true
|
|
187
|
+
));
|
|
188
|
+
if (matching.length >= minCount) {
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
elapsed_ms: Date.now() - started,
|
|
192
|
+
count: matching.length,
|
|
193
|
+
events: matching
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
await sleep(intervalMs);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
elapsed_ms: Date.now() - started,
|
|
201
|
+
count: matching.length,
|
|
202
|
+
events: matching,
|
|
203
|
+
total_event_count: events.length
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function readChatProfileNetworkBodies(client, events = [], {
|
|
208
|
+
limit = 20
|
|
209
|
+
} = {}) {
|
|
210
|
+
const bodies = [];
|
|
211
|
+
for (const event of events.slice(0, limit)) {
|
|
212
|
+
try {
|
|
213
|
+
const body = await client.Network.getResponseBody({ requestId: event.requestId });
|
|
214
|
+
bodies.push({
|
|
215
|
+
...event,
|
|
216
|
+
body,
|
|
217
|
+
body_length: String(body?.body || "").length
|
|
218
|
+
});
|
|
219
|
+
} catch (error) {
|
|
220
|
+
bodies.push({
|
|
221
|
+
...event,
|
|
222
|
+
body_error: error?.message || String(error)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return bodies;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function normalizeDetailText(value = "") {
|
|
230
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function chatCandidateIdFromAttributes(attributes = {}) {
|
|
234
|
+
return normalizeDetailText(
|
|
235
|
+
attributes["data-id"]
|
|
236
|
+
|| attributes["data-geekid"]
|
|
237
|
+
|| attributes["data-geek"]
|
|
238
|
+
|| attributes["data-uid"]
|
|
239
|
+
|| attributes.key
|
|
240
|
+
|| attributes.id
|
|
241
|
+
|| ""
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function hydrateActiveChatCandidate(client, activeCandidate = null) {
|
|
246
|
+
if (!activeCandidate?.node_id) return activeCandidate;
|
|
247
|
+
let attributes = {};
|
|
248
|
+
let outerHTML = "";
|
|
249
|
+
try {
|
|
250
|
+
[attributes, outerHTML] = await Promise.all([
|
|
251
|
+
getAttributesMap(client, activeCandidate.node_id),
|
|
252
|
+
getOuterHTML(client, activeCandidate.node_id)
|
|
253
|
+
]);
|
|
254
|
+
} catch {}
|
|
255
|
+
return {
|
|
256
|
+
...activeCandidate,
|
|
257
|
+
attributes,
|
|
258
|
+
candidate_id: chatCandidateIdFromAttributes(attributes) || null,
|
|
259
|
+
label: normalizeDetailText(htmlToText(outerHTML)),
|
|
260
|
+
outer_html_length: outerHTML.length
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function waitForChatOnlineResumeButton(client, {
|
|
265
|
+
timeoutMs = 12000,
|
|
266
|
+
intervalMs = 250,
|
|
267
|
+
expectedCandidateId = ""
|
|
268
|
+
} = {}) {
|
|
269
|
+
const started = Date.now();
|
|
270
|
+
let lastState = null;
|
|
271
|
+
const expectedId = chatCandidateIdFromAttributes({ "data-id": expectedCandidateId });
|
|
272
|
+
while (Date.now() - started <= timeoutMs) {
|
|
273
|
+
const topLevelState = await getChatTopLevelState(client);
|
|
274
|
+
if (topLevelState.is_forbidden_resume_top_level) {
|
|
275
|
+
return {
|
|
276
|
+
forbidden_top_level_navigation: true,
|
|
277
|
+
top_level_state: topLevelState
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const rootState = await getChatRoots(client);
|
|
281
|
+
const target = await findVisibleTarget(client, rootState.roots, CHAT_ONLINE_RESUME_BUTTON_SELECTORS);
|
|
282
|
+
const activeCandidate = await hydrateActiveChatCandidate(
|
|
283
|
+
client,
|
|
284
|
+
await queryFirstAcrossChatRoots(client, rootState.roots, CHAT_ACTIVE_CANDIDATE_SELECTORS)
|
|
285
|
+
);
|
|
286
|
+
const activeCandidateId = activeCandidate?.candidate_id || "";
|
|
287
|
+
const candidateSelectionVerified = expectedId
|
|
288
|
+
? activeCandidateId === expectedId
|
|
289
|
+
: undefined;
|
|
290
|
+
lastState = {
|
|
291
|
+
roots: rootState.roots,
|
|
292
|
+
target,
|
|
293
|
+
activeCandidate,
|
|
294
|
+
expected_candidate_id: expectedId || null,
|
|
295
|
+
active_candidate_id: activeCandidateId || null,
|
|
296
|
+
candidate_selection_verified: candidateSelectionVerified
|
|
297
|
+
};
|
|
298
|
+
if (target && (!expectedId || candidateSelectionVerified)) {
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
elapsed_ms: Date.now() - started,
|
|
302
|
+
...lastState
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
await sleep(intervalMs);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
reason: expectedId && lastState?.candidate_selection_verified === false
|
|
310
|
+
? "active_candidate_mismatch"
|
|
311
|
+
: "online_resume_button_unavailable",
|
|
312
|
+
elapsed_ms: Date.now() - started,
|
|
313
|
+
...lastState
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function selectChatCandidate(client, cardNodeId, {
|
|
318
|
+
timeoutMs = 12000,
|
|
319
|
+
settleMs = 1200
|
|
320
|
+
} = {}) {
|
|
321
|
+
const cardBox = await clickNodeCenter(client, cardNodeId, {
|
|
322
|
+
scrollIntoView: true
|
|
323
|
+
});
|
|
324
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
325
|
+
const ready = await waitForChatOnlineResumeButton(client, { timeoutMs });
|
|
326
|
+
return {
|
|
327
|
+
card_box: cardBox,
|
|
328
|
+
ready
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function hasActiveSignal(attributes = {}, outerHTML = "") {
|
|
333
|
+
return /\b(active|selected|current|curr)\b/i.test(String(attributes.class || ""))
|
|
334
|
+
|| normalizeDetailText(attributes["aria-selected"]).toLowerCase() === "true"
|
|
335
|
+
|| normalizeDetailText(attributes["data-active"]).toLowerCase() === "true"
|
|
336
|
+
|| /\b(active|selected|current|curr)\b/i.test(String(outerHTML || "").slice(0, 500));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isDisabledSignal(attributes = {}, outerHTML = "") {
|
|
340
|
+
return attributes.disabled !== undefined
|
|
341
|
+
|| normalizeDetailText(attributes["aria-disabled"]).toLowerCase() === "true"
|
|
342
|
+
|| /\b(disabled|disable|is-disabled)\b/i.test([
|
|
343
|
+
attributes.class,
|
|
344
|
+
String(outerHTML || "").slice(0, 500)
|
|
345
|
+
].join(" "));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isAskResumeText(text = "") {
|
|
349
|
+
const normalized = normalizeDetailText(text);
|
|
350
|
+
return Boolean(
|
|
351
|
+
normalized === "求简历"
|
|
352
|
+
|| normalized === "索要简历"
|
|
353
|
+
|| normalized === "求附件简历"
|
|
354
|
+
|| normalized.includes("求简历")
|
|
355
|
+
|| normalized.includes("索要简历")
|
|
356
|
+
|| normalized.includes("求附件简历")
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isRequestedResumeText(text = "") {
|
|
361
|
+
const normalized = normalizeDetailText(text);
|
|
362
|
+
return Boolean(
|
|
363
|
+
normalized === "已求简历"
|
|
364
|
+
|| normalized === "已索要简历"
|
|
365
|
+
|| normalized.includes("已求简历")
|
|
366
|
+
|| normalized.includes("已索要简历")
|
|
367
|
+
|| normalized.includes("简历请求已发送")
|
|
368
|
+
|| normalized.includes("已发送简历")
|
|
369
|
+
|| (normalized.includes("已申请") && normalized.includes("简历"))
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function isResumeRequestSentMessageText(text = "") {
|
|
374
|
+
const normalized = normalizeDetailText(text);
|
|
375
|
+
return Boolean(
|
|
376
|
+
normalized.includes("简历请求已发送")
|
|
377
|
+
|| normalized.includes("已发送简历")
|
|
378
|
+
|| normalized.includes("已求简历")
|
|
379
|
+
|| normalized.includes("已索要简历")
|
|
380
|
+
|| normalized.includes("已发送")
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function countTextOccurrences(text = "", needle = "") {
|
|
385
|
+
if (!needle) return 0;
|
|
386
|
+
let count = 0;
|
|
387
|
+
let index = 0;
|
|
388
|
+
while (index < text.length) {
|
|
389
|
+
const found = text.indexOf(needle, index);
|
|
390
|
+
if (found < 0) break;
|
|
391
|
+
count += 1;
|
|
392
|
+
index = found + needle.length;
|
|
393
|
+
}
|
|
394
|
+
return count;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function countResumeRequestSentMessageMarkers(lines = []) {
|
|
398
|
+
const markers = ["简历请求已发送", "已发送简历", "已求简历", "已索要简历", "已发送"];
|
|
399
|
+
return lines.reduce((total, line) => (
|
|
400
|
+
total + markers.reduce((lineTotal, marker) => (
|
|
401
|
+
lineTotal + countTextOccurrences(line, marker)
|
|
402
|
+
), 0)
|
|
403
|
+
), 0);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isResumeAttachmentMessageText(text = "") {
|
|
407
|
+
const normalized = normalizeDetailText(text);
|
|
408
|
+
return Boolean(
|
|
409
|
+
/点击.*附件简历/.test(normalized)
|
|
410
|
+
|| /预览附件简历/.test(normalized)
|
|
411
|
+
|| /查看附件简历/.test(normalized)
|
|
412
|
+
|| /(?:简历|resume)[^\s]*\.(?:pdf|docx?|jpg|jpeg|png)\b/i.test(normalized)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function countResumeAttachmentMessageMarkers(lines = []) {
|
|
417
|
+
return lines.reduce((total, line) => total + (isResumeAttachmentMessageText(line) ? 1 : 0), 0);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isRequestedResumeControlTarget(target = {}) {
|
|
421
|
+
const label = normalizeDetailText(target.label);
|
|
422
|
+
const className = String(target.attributes?.class || "");
|
|
423
|
+
const selector = String(target.selector || "");
|
|
424
|
+
const controlLike = /\boperate-btn\b|operate|resume|button|btn/i.test(`${selector} ${className}`);
|
|
425
|
+
if (isRequestedResumeText(label)) return true;
|
|
426
|
+
return controlLike && Boolean(
|
|
427
|
+
label === "已申请"
|
|
428
|
+
|| label === "已发送"
|
|
429
|
+
|| label.includes("已申请")
|
|
430
|
+
|| label.includes("已发送")
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function isAttachmentResumeText(text = "") {
|
|
435
|
+
const normalized = normalizeDetailText(text);
|
|
436
|
+
return Boolean(
|
|
437
|
+
normalized === "附件简历"
|
|
438
|
+
|| (normalized.includes("附件简历") && !normalized.includes("求附件简历"))
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function isAttachmentResumeTarget(target = {}) {
|
|
443
|
+
return isAttachmentResumeText(target.label)
|
|
444
|
+
|| /resume-btn-file/i.test(String(target.attributes?.class || target.selector || ""));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function isConfirmText(text = "") {
|
|
448
|
+
const normalized = normalizeDetailText(text);
|
|
449
|
+
return Boolean(
|
|
450
|
+
normalized === "确定"
|
|
451
|
+
|| normalized === "确认"
|
|
452
|
+
|| normalized === "提交"
|
|
453
|
+
|| normalized === "继续"
|
|
454
|
+
|| normalized.includes("确定")
|
|
455
|
+
|| normalized.includes("确认")
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function isSendText(text = "") {
|
|
460
|
+
const normalized = normalizeDetailText(text);
|
|
461
|
+
return normalized === "发送" || normalized.includes("发送");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function isRecoverableNodeError(error) {
|
|
465
|
+
return /(?:Could not find node|No node with given id|Cannot find node|Could not compute box model)/i
|
|
466
|
+
.test(String(error?.message || error || ""));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function readTarget(client, root, selector, nodeId) {
|
|
470
|
+
let attributes = {};
|
|
471
|
+
let outerHTML = "";
|
|
472
|
+
let readError = "";
|
|
473
|
+
try {
|
|
474
|
+
[attributes, outerHTML] = await Promise.all([
|
|
475
|
+
getAttributesMap(client, nodeId),
|
|
476
|
+
getOuterHTML(client, nodeId)
|
|
477
|
+
]);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
readError = error?.message || String(error);
|
|
480
|
+
}
|
|
481
|
+
const label = normalizeDetailText(htmlToText(outerHTML));
|
|
482
|
+
let box = null;
|
|
483
|
+
try {
|
|
484
|
+
box = await getNodeBox(client, nodeId);
|
|
485
|
+
} catch {}
|
|
486
|
+
return {
|
|
487
|
+
root: root.name,
|
|
488
|
+
root_node_id: root.nodeId,
|
|
489
|
+
selector,
|
|
490
|
+
node_id: nodeId,
|
|
491
|
+
label,
|
|
492
|
+
attributes,
|
|
493
|
+
disabled: isDisabledSignal(attributes, outerHTML),
|
|
494
|
+
active: hasActiveSignal(attributes, outerHTML),
|
|
495
|
+
visible: Boolean(box && box.rect.width > 2 && box.rect.height > 2),
|
|
496
|
+
center: box?.center || null,
|
|
497
|
+
rect: box?.rect || null,
|
|
498
|
+
outer_html_length: outerHTML.length,
|
|
499
|
+
read_error: readError || null
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function findVisibleMatchingTarget(client, roots, selectors, predicate) {
|
|
504
|
+
for (const root of roots) {
|
|
505
|
+
if (!root?.nodeId) continue;
|
|
506
|
+
for (const selector of selectors) {
|
|
507
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
508
|
+
for (const nodeId of nodeIds) {
|
|
509
|
+
const target = await readTarget(client, root, selector, nodeId);
|
|
510
|
+
if (!target.visible) continue;
|
|
511
|
+
if (predicate(target)) return target;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function resolveScopedRoots(client, roots = [], selectors = [], {
|
|
519
|
+
fallbackToRoots = true
|
|
520
|
+
} = {}) {
|
|
521
|
+
const scoped = [];
|
|
522
|
+
const seen = new Set();
|
|
523
|
+
for (const root of roots) {
|
|
524
|
+
if (!root?.nodeId) continue;
|
|
525
|
+
for (const selector of selectors) {
|
|
526
|
+
let nodeIds = [];
|
|
527
|
+
try {
|
|
528
|
+
nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
529
|
+
} catch {
|
|
530
|
+
nodeIds = [];
|
|
531
|
+
}
|
|
532
|
+
for (const nodeId of nodeIds) {
|
|
533
|
+
const key = `${root.name}:${nodeId}`;
|
|
534
|
+
if (seen.has(key)) continue;
|
|
535
|
+
seen.add(key);
|
|
536
|
+
scoped.push({
|
|
537
|
+
name: `${root.name}:${selector}`,
|
|
538
|
+
nodeId
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (scoped.length || !fallbackToRoots) return scoped;
|
|
544
|
+
return roots;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export async function selectChatPrimaryLabel(client, {
|
|
548
|
+
label = "全部",
|
|
549
|
+
timeoutMs = 8000,
|
|
550
|
+
intervalMs = 300,
|
|
551
|
+
settleMs = 700
|
|
552
|
+
} = {}) {
|
|
553
|
+
const started = Date.now();
|
|
554
|
+
let lastCandidates = [];
|
|
555
|
+
while (Date.now() - started <= timeoutMs) {
|
|
556
|
+
const rootState = await getChatRoots(client);
|
|
557
|
+
const candidates = [];
|
|
558
|
+
for (const root of rootState.roots) {
|
|
559
|
+
for (const selector of CHAT_PRIMARY_LABEL_SELECTORS) {
|
|
560
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
561
|
+
for (const nodeId of nodeIds) {
|
|
562
|
+
const target = await readTarget(client, root, selector, nodeId);
|
|
563
|
+
if (target.visible) candidates.push(target);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
lastCandidates = candidates;
|
|
568
|
+
const matched = candidates.find((target) => (
|
|
569
|
+
target.label === label || target.label.startsWith(`${label}(`)
|
|
570
|
+
));
|
|
571
|
+
if (matched?.active) {
|
|
572
|
+
return {
|
|
573
|
+
ok: true,
|
|
574
|
+
changed: false,
|
|
575
|
+
verified: true,
|
|
576
|
+
active_label: matched.label,
|
|
577
|
+
control: matched
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (matched) {
|
|
581
|
+
if (matched.center) {
|
|
582
|
+
await clickPoint(client, matched.center.x, matched.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
583
|
+
} else {
|
|
584
|
+
await clickNodeCenter(client, matched.node_id, {
|
|
585
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
586
|
+
scrollIntoView: true
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
590
|
+
return {
|
|
591
|
+
ok: true,
|
|
592
|
+
changed: true,
|
|
593
|
+
verified: true,
|
|
594
|
+
active_label: label,
|
|
595
|
+
control: matched
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
await sleep(intervalMs);
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
ok: false,
|
|
602
|
+
error: `CHAT_PRIMARY_LABEL_NOT_FOUND:${label}`,
|
|
603
|
+
candidates: lastCandidates.map((item) => ({
|
|
604
|
+
label: item.label,
|
|
605
|
+
selector: item.selector,
|
|
606
|
+
active: item.active
|
|
607
|
+
}))
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export async function selectChatMessageFilter(client, {
|
|
612
|
+
startFrom = "unread",
|
|
613
|
+
timeoutMs = 8000,
|
|
614
|
+
intervalMs = 300,
|
|
615
|
+
settleMs = 900
|
|
616
|
+
} = {}) {
|
|
617
|
+
const label = startFrom === "all" ? "全部" : "未读";
|
|
618
|
+
const started = Date.now();
|
|
619
|
+
let lastCandidates = [];
|
|
620
|
+
while (Date.now() - started <= timeoutMs) {
|
|
621
|
+
const rootState = await getChatRoots(client);
|
|
622
|
+
const candidates = [];
|
|
623
|
+
for (const root of rootState.roots) {
|
|
624
|
+
for (const selector of CHAT_MESSAGE_FILTER_SELECTORS) {
|
|
625
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
626
|
+
for (const nodeId of nodeIds) {
|
|
627
|
+
const target = await readTarget(client, root, selector, nodeId);
|
|
628
|
+
if (target.visible && target.label === label) candidates.push(target);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
lastCandidates = candidates;
|
|
633
|
+
const active = candidates.find((target) => target.active);
|
|
634
|
+
if (active) {
|
|
635
|
+
return {
|
|
636
|
+
ok: true,
|
|
637
|
+
changed: false,
|
|
638
|
+
verified: true,
|
|
639
|
+
active_label: active.label,
|
|
640
|
+
control: active
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const matched = candidates[0];
|
|
644
|
+
if (matched) {
|
|
645
|
+
if (matched.center) {
|
|
646
|
+
await clickPoint(client, matched.center.x, matched.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
647
|
+
} else {
|
|
648
|
+
await clickNodeCenter(client, matched.node_id, {
|
|
649
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
650
|
+
scrollIntoView: true
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
654
|
+
return {
|
|
655
|
+
ok: true,
|
|
656
|
+
changed: true,
|
|
657
|
+
verified: true,
|
|
658
|
+
active_label: label,
|
|
659
|
+
control: matched
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
await sleep(intervalMs);
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
ok: false,
|
|
666
|
+
error: `CHAT_MESSAGE_FILTER_NOT_FOUND:${label}`,
|
|
667
|
+
candidates: lastCandidates.map((item) => ({
|
|
668
|
+
label: item.label,
|
|
669
|
+
selector: item.selector,
|
|
670
|
+
active: item.active
|
|
671
|
+
}))
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export async function waitForChatResumeModal(client, {
|
|
676
|
+
timeoutMs = 12000,
|
|
677
|
+
intervalMs = 250
|
|
678
|
+
} = {}) {
|
|
679
|
+
const started = Date.now();
|
|
680
|
+
let lastState = null;
|
|
681
|
+
while (Date.now() - started <= timeoutMs) {
|
|
682
|
+
const topLevelState = await getChatTopLevelState(client);
|
|
683
|
+
if (topLevelState.is_forbidden_resume_top_level) {
|
|
684
|
+
return {
|
|
685
|
+
forbidden_top_level_navigation: true,
|
|
686
|
+
top_level_state: topLevelState
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const rootState = await getChatRoots(client);
|
|
690
|
+
const popup = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_MODAL_SELECTORS);
|
|
691
|
+
const content = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_CONTENT_SELECTORS);
|
|
692
|
+
const resumeIframe = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_IFRAME_SELECTORS);
|
|
693
|
+
lastState = {
|
|
694
|
+
roots: rootState.roots,
|
|
695
|
+
popup,
|
|
696
|
+
content,
|
|
697
|
+
resumeIframe
|
|
698
|
+
};
|
|
699
|
+
if (popup || content || resumeIframe) return lastState;
|
|
700
|
+
await sleep(intervalMs);
|
|
701
|
+
}
|
|
702
|
+
return lastState;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export async function quickChatResumeModalOpenProbe(client, {
|
|
706
|
+
selectors = CHAT_RESUME_FAST_MODAL_SELECTORS
|
|
707
|
+
} = {}) {
|
|
708
|
+
const rootState = await getChatRoots(client);
|
|
709
|
+
for (const root of rootState.roots) {
|
|
710
|
+
if (!root?.nodeId) continue;
|
|
711
|
+
for (const selector of selectors) {
|
|
712
|
+
let nodeIds = [];
|
|
713
|
+
try {
|
|
714
|
+
nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
715
|
+
} catch {
|
|
716
|
+
nodeIds = [];
|
|
717
|
+
}
|
|
718
|
+
for (const nodeId of nodeIds.slice(0, 4)) {
|
|
719
|
+
try {
|
|
720
|
+
const box = await getNodeBox(client, nodeId);
|
|
721
|
+
if (box?.rect?.width > 8 && box?.rect?.height > 8) {
|
|
722
|
+
return {
|
|
723
|
+
open: true,
|
|
724
|
+
root: root.name,
|
|
725
|
+
selector,
|
|
726
|
+
node_id: nodeId,
|
|
727
|
+
rect: box.rect,
|
|
728
|
+
center: box.center
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
} catch {
|
|
732
|
+
// Hidden or stale modal probes are ignored.
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
open: false
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export async function findChatBlockingPanel(client, {
|
|
743
|
+
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
262
744
|
} = {}) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const topLevelState = await getChatTopLevelState(client);
|
|
268
|
-
if (topLevelState.is_forbidden_resume_top_level) {
|
|
269
|
-
return {
|
|
270
|
-
forbidden_top_level_navigation: true,
|
|
271
|
-
top_level_state: topLevelState
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
const rootState = await getChatRoots(client);
|
|
275
|
-
const target = await findVisibleTarget(client, rootState.roots, CHAT_ONLINE_RESUME_BUTTON_SELECTORS);
|
|
276
|
-
const activeCandidate = await hydrateActiveChatCandidate(
|
|
277
|
-
client,
|
|
278
|
-
await queryFirstAcrossChatRoots(client, rootState.roots, CHAT_ACTIVE_CANDIDATE_SELECTORS)
|
|
279
|
-
);
|
|
280
|
-
const activeCandidateId = activeCandidate?.candidate_id || "";
|
|
281
|
-
const candidateSelectionVerified = expectedId
|
|
282
|
-
? activeCandidateId === expectedId
|
|
283
|
-
: undefined;
|
|
284
|
-
lastState = {
|
|
285
|
-
roots: rootState.roots,
|
|
286
|
-
target,
|
|
287
|
-
activeCandidate,
|
|
288
|
-
expected_candidate_id: expectedId || null,
|
|
289
|
-
active_candidate_id: activeCandidateId || null,
|
|
290
|
-
candidate_selection_verified: candidateSelectionVerified
|
|
291
|
-
};
|
|
292
|
-
if (target && (!expectedId || candidateSelectionVerified)) {
|
|
293
|
-
return {
|
|
294
|
-
ok: true,
|
|
295
|
-
elapsed_ms: Date.now() - started,
|
|
296
|
-
...lastState
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
await sleep(intervalMs);
|
|
300
|
-
}
|
|
301
|
-
return {
|
|
302
|
-
ok: false,
|
|
303
|
-
reason: expectedId && lastState?.candidate_selection_verified === false
|
|
304
|
-
? "active_candidate_mismatch"
|
|
305
|
-
: "online_resume_button_unavailable",
|
|
306
|
-
elapsed_ms: Date.now() - started,
|
|
307
|
-
...lastState
|
|
308
|
-
};
|
|
745
|
+
const panel = await findBossAccountRightsBlockingPanel(client, { textQueries });
|
|
746
|
+
return panel.open
|
|
747
|
+
? { ...panel, reason: "blocking_panel_text_visible" }
|
|
748
|
+
: panel;
|
|
309
749
|
}
|
|
310
750
|
|
|
311
|
-
export async function
|
|
312
|
-
|
|
313
|
-
|
|
751
|
+
export async function closeChatBlockingPanels(client, {
|
|
752
|
+
attemptsLimit = 2,
|
|
753
|
+
closeSelectors = CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
|
|
754
|
+
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
314
755
|
} = {}) {
|
|
315
|
-
|
|
316
|
-
|
|
756
|
+
return closeBossAccountRightsBlockingPanel(client, {
|
|
757
|
+
attemptsLimit,
|
|
758
|
+
closeSelectors,
|
|
759
|
+
resolveRoots: getChatRoots,
|
|
760
|
+
textQueries
|
|
317
761
|
});
|
|
318
|
-
if (settleMs > 0) await sleep(settleMs);
|
|
319
|
-
const ready = await waitForChatOnlineResumeButton(client, { timeoutMs });
|
|
320
|
-
return {
|
|
321
|
-
card_box: cardBox,
|
|
322
|
-
ready
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function hasActiveSignal(attributes = {}, outerHTML = "") {
|
|
327
|
-
return /\b(active|selected|current|curr)\b/i.test(String(attributes.class || ""))
|
|
328
|
-
|| normalizeDetailText(attributes["aria-selected"]).toLowerCase() === "true"
|
|
329
|
-
|| normalizeDetailText(attributes["data-active"]).toLowerCase() === "true"
|
|
330
|
-
|| /\b(active|selected|current|curr)\b/i.test(String(outerHTML || "").slice(0, 500));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function isDisabledSignal(attributes = {}, outerHTML = "") {
|
|
334
|
-
return attributes.disabled !== undefined
|
|
335
|
-
|| normalizeDetailText(attributes["aria-disabled"]).toLowerCase() === "true"
|
|
336
|
-
|| /\b(disabled|disable|is-disabled)\b/i.test([
|
|
337
|
-
attributes.class,
|
|
338
|
-
String(outerHTML || "").slice(0, 500)
|
|
339
|
-
].join(" "));
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function isAskResumeText(text = "") {
|
|
343
|
-
const normalized = normalizeDetailText(text);
|
|
344
|
-
return Boolean(
|
|
345
|
-
normalized === "求简历"
|
|
346
|
-
|| normalized === "索要简历"
|
|
347
|
-
|| normalized === "求附件简历"
|
|
348
|
-
|| normalized.includes("求简历")
|
|
349
|
-
|| normalized.includes("索要简历")
|
|
350
|
-
|| normalized.includes("求附件简历")
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function isRequestedResumeText(text = "") {
|
|
355
|
-
const normalized = normalizeDetailText(text);
|
|
356
|
-
return Boolean(
|
|
357
|
-
normalized === "已求简历"
|
|
358
|
-
|| normalized === "已索要简历"
|
|
359
|
-
|| normalized.includes("已求简历")
|
|
360
|
-
|| normalized.includes("已索要简历")
|
|
361
|
-
|| normalized.includes("简历请求已发送")
|
|
362
|
-
|| normalized.includes("已发送简历")
|
|
363
|
-
|| (normalized.includes("已申请") && normalized.includes("简历"))
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function isResumeRequestSentMessageText(text = "") {
|
|
368
|
-
const normalized = normalizeDetailText(text);
|
|
369
|
-
return Boolean(
|
|
370
|
-
normalized.includes("简历请求已发送")
|
|
371
|
-
|| normalized.includes("已发送简历")
|
|
372
|
-
|| normalized.includes("已求简历")
|
|
373
|
-
|| normalized.includes("已索要简历")
|
|
374
|
-
|| normalized.includes("已发送")
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function countTextOccurrences(text = "", needle = "") {
|
|
379
|
-
if (!needle) return 0;
|
|
380
|
-
let count = 0;
|
|
381
|
-
let index = 0;
|
|
382
|
-
while (index < text.length) {
|
|
383
|
-
const found = text.indexOf(needle, index);
|
|
384
|
-
if (found < 0) break;
|
|
385
|
-
count += 1;
|
|
386
|
-
index = found + needle.length;
|
|
387
|
-
}
|
|
388
|
-
return count;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function countResumeRequestSentMessageMarkers(lines = []) {
|
|
392
|
-
const markers = ["简历请求已发送", "已发送简历", "已求简历", "已索要简历", "已发送"];
|
|
393
|
-
return lines.reduce((total, line) => (
|
|
394
|
-
total + markers.reduce((lineTotal, marker) => (
|
|
395
|
-
lineTotal + countTextOccurrences(line, marker)
|
|
396
|
-
), 0)
|
|
397
|
-
), 0);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function isResumeAttachmentMessageText(text = "") {
|
|
401
|
-
const normalized = normalizeDetailText(text);
|
|
402
|
-
return Boolean(
|
|
403
|
-
/点击.*附件简历/.test(normalized)
|
|
404
|
-
|| /预览附件简历/.test(normalized)
|
|
405
|
-
|| /查看附件简历/.test(normalized)
|
|
406
|
-
|| /(?:简历|resume)[^\s]*\.(?:pdf|docx?|jpg|jpeg|png)\b/i.test(normalized)
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function countResumeAttachmentMessageMarkers(lines = []) {
|
|
411
|
-
return lines.reduce((total, line) => total + (isResumeAttachmentMessageText(line) ? 1 : 0), 0);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function isRequestedResumeControlTarget(target = {}) {
|
|
415
|
-
const label = normalizeDetailText(target.label);
|
|
416
|
-
const className = String(target.attributes?.class || "");
|
|
417
|
-
const selector = String(target.selector || "");
|
|
418
|
-
const controlLike = /\boperate-btn\b|operate|resume|button|btn/i.test(`${selector} ${className}`);
|
|
419
|
-
if (isRequestedResumeText(label)) return true;
|
|
420
|
-
return controlLike && Boolean(
|
|
421
|
-
label === "已申请"
|
|
422
|
-
|| label === "已发送"
|
|
423
|
-
|| label.includes("已申请")
|
|
424
|
-
|| label.includes("已发送")
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function isAttachmentResumeText(text = "") {
|
|
429
|
-
const normalized = normalizeDetailText(text);
|
|
430
|
-
return Boolean(
|
|
431
|
-
normalized === "附件简历"
|
|
432
|
-
|| (normalized.includes("附件简历") && !normalized.includes("求附件简历"))
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function isAttachmentResumeTarget(target = {}) {
|
|
437
|
-
return isAttachmentResumeText(target.label)
|
|
438
|
-
|| /resume-btn-file/i.test(String(target.attributes?.class || target.selector || ""));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function isConfirmText(text = "") {
|
|
442
|
-
const normalized = normalizeDetailText(text);
|
|
443
|
-
return Boolean(
|
|
444
|
-
normalized === "确定"
|
|
445
|
-
|| normalized === "确认"
|
|
446
|
-
|| normalized === "提交"
|
|
447
|
-
|| normalized === "继续"
|
|
448
|
-
|| normalized.includes("确定")
|
|
449
|
-
|| normalized.includes("确认")
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function isSendText(text = "") {
|
|
454
|
-
const normalized = normalizeDetailText(text);
|
|
455
|
-
return normalized === "发送" || normalized.includes("发送");
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function isRecoverableNodeError(error) {
|
|
459
|
-
return /(?:Could not find node|No node with given id|Cannot find node|Could not compute box model)/i
|
|
460
|
-
.test(String(error?.message || error || ""));
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
async function readTarget(client, root, selector, nodeId) {
|
|
464
|
-
let attributes = {};
|
|
465
|
-
let outerHTML = "";
|
|
466
|
-
let readError = "";
|
|
467
|
-
try {
|
|
468
|
-
[attributes, outerHTML] = await Promise.all([
|
|
469
|
-
getAttributesMap(client, nodeId),
|
|
470
|
-
getOuterHTML(client, nodeId)
|
|
471
|
-
]);
|
|
472
|
-
} catch (error) {
|
|
473
|
-
readError = error?.message || String(error);
|
|
474
|
-
}
|
|
475
|
-
const label = normalizeDetailText(htmlToText(outerHTML));
|
|
476
|
-
let box = null;
|
|
477
|
-
try {
|
|
478
|
-
box = await getNodeBox(client, nodeId);
|
|
479
|
-
} catch {}
|
|
480
|
-
return {
|
|
481
|
-
root: root.name,
|
|
482
|
-
root_node_id: root.nodeId,
|
|
483
|
-
selector,
|
|
484
|
-
node_id: nodeId,
|
|
485
|
-
label,
|
|
486
|
-
attributes,
|
|
487
|
-
disabled: isDisabledSignal(attributes, outerHTML),
|
|
488
|
-
active: hasActiveSignal(attributes, outerHTML),
|
|
489
|
-
visible: Boolean(box && box.rect.width > 2 && box.rect.height > 2),
|
|
490
|
-
center: box?.center || null,
|
|
491
|
-
rect: box?.rect || null,
|
|
492
|
-
outer_html_length: outerHTML.length,
|
|
493
|
-
read_error: readError || null
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
async function findVisibleMatchingTarget(client, roots, selectors, predicate) {
|
|
498
|
-
for (const root of roots) {
|
|
499
|
-
if (!root?.nodeId) continue;
|
|
500
|
-
for (const selector of selectors) {
|
|
501
|
-
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
502
|
-
for (const nodeId of nodeIds) {
|
|
503
|
-
const target = await readTarget(client, root, selector, nodeId);
|
|
504
|
-
if (!target.visible) continue;
|
|
505
|
-
if (predicate(target)) return target;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
return null;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async function resolveScopedRoots(client, roots = [], selectors = [], {
|
|
513
|
-
fallbackToRoots = true
|
|
514
|
-
} = {}) {
|
|
515
|
-
const scoped = [];
|
|
516
|
-
const seen = new Set();
|
|
517
|
-
for (const root of roots) {
|
|
518
|
-
if (!root?.nodeId) continue;
|
|
519
|
-
for (const selector of selectors) {
|
|
520
|
-
let nodeIds = [];
|
|
521
|
-
try {
|
|
522
|
-
nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
523
|
-
} catch {
|
|
524
|
-
nodeIds = [];
|
|
525
|
-
}
|
|
526
|
-
for (const nodeId of nodeIds) {
|
|
527
|
-
const key = `${root.name}:${nodeId}`;
|
|
528
|
-
if (seen.has(key)) continue;
|
|
529
|
-
seen.add(key);
|
|
530
|
-
scoped.push({
|
|
531
|
-
name: `${root.name}:${selector}`,
|
|
532
|
-
nodeId
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (scoped.length || !fallbackToRoots) return scoped;
|
|
538
|
-
return roots;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export async function selectChatPrimaryLabel(client, {
|
|
542
|
-
label = "全部",
|
|
543
|
-
timeoutMs = 8000,
|
|
544
|
-
intervalMs = 300,
|
|
545
|
-
settleMs = 700
|
|
546
|
-
} = {}) {
|
|
547
|
-
const started = Date.now();
|
|
548
|
-
let lastCandidates = [];
|
|
549
|
-
while (Date.now() - started <= timeoutMs) {
|
|
550
|
-
const rootState = await getChatRoots(client);
|
|
551
|
-
const candidates = [];
|
|
552
|
-
for (const root of rootState.roots) {
|
|
553
|
-
for (const selector of CHAT_PRIMARY_LABEL_SELECTORS) {
|
|
554
|
-
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
555
|
-
for (const nodeId of nodeIds) {
|
|
556
|
-
const target = await readTarget(client, root, selector, nodeId);
|
|
557
|
-
if (target.visible) candidates.push(target);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
lastCandidates = candidates;
|
|
562
|
-
const matched = candidates.find((target) => (
|
|
563
|
-
target.label === label || target.label.startsWith(`${label}(`)
|
|
564
|
-
));
|
|
565
|
-
if (matched?.active) {
|
|
566
|
-
return {
|
|
567
|
-
ok: true,
|
|
568
|
-
changed: false,
|
|
569
|
-
verified: true,
|
|
570
|
-
active_label: matched.label,
|
|
571
|
-
control: matched
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
if (matched) {
|
|
575
|
-
if (matched.center) {
|
|
576
|
-
await clickPoint(client, matched.center.x, matched.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
577
|
-
} else {
|
|
578
|
-
await clickNodeCenter(client, matched.node_id, {
|
|
579
|
-
...DETERMINISTIC_CLICK_OPTIONS,
|
|
580
|
-
scrollIntoView: true
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
if (settleMs > 0) await sleep(settleMs);
|
|
584
|
-
return {
|
|
585
|
-
ok: true,
|
|
586
|
-
changed: true,
|
|
587
|
-
verified: true,
|
|
588
|
-
active_label: label,
|
|
589
|
-
control: matched
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
await sleep(intervalMs);
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
ok: false,
|
|
596
|
-
error: `CHAT_PRIMARY_LABEL_NOT_FOUND:${label}`,
|
|
597
|
-
candidates: lastCandidates.map((item) => ({
|
|
598
|
-
label: item.label,
|
|
599
|
-
selector: item.selector,
|
|
600
|
-
active: item.active
|
|
601
|
-
}))
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
export async function selectChatMessageFilter(client, {
|
|
606
|
-
startFrom = "unread",
|
|
607
|
-
timeoutMs = 8000,
|
|
608
|
-
intervalMs = 300,
|
|
609
|
-
settleMs = 900
|
|
610
|
-
} = {}) {
|
|
611
|
-
const label = startFrom === "all" ? "全部" : "未读";
|
|
612
|
-
const started = Date.now();
|
|
613
|
-
let lastCandidates = [];
|
|
614
|
-
while (Date.now() - started <= timeoutMs) {
|
|
615
|
-
const rootState = await getChatRoots(client);
|
|
616
|
-
const candidates = [];
|
|
617
|
-
for (const root of rootState.roots) {
|
|
618
|
-
for (const selector of CHAT_MESSAGE_FILTER_SELECTORS) {
|
|
619
|
-
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
620
|
-
for (const nodeId of nodeIds) {
|
|
621
|
-
const target = await readTarget(client, root, selector, nodeId);
|
|
622
|
-
if (target.visible && target.label === label) candidates.push(target);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
lastCandidates = candidates;
|
|
627
|
-
const active = candidates.find((target) => target.active);
|
|
628
|
-
if (active) {
|
|
629
|
-
return {
|
|
630
|
-
ok: true,
|
|
631
|
-
changed: false,
|
|
632
|
-
verified: true,
|
|
633
|
-
active_label: active.label,
|
|
634
|
-
control: active
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
const matched = candidates[0];
|
|
638
|
-
if (matched) {
|
|
639
|
-
if (matched.center) {
|
|
640
|
-
await clickPoint(client, matched.center.x, matched.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
641
|
-
} else {
|
|
642
|
-
await clickNodeCenter(client, matched.node_id, {
|
|
643
|
-
...DETERMINISTIC_CLICK_OPTIONS,
|
|
644
|
-
scrollIntoView: true
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
if (settleMs > 0) await sleep(settleMs);
|
|
648
|
-
return {
|
|
649
|
-
ok: true,
|
|
650
|
-
changed: true,
|
|
651
|
-
verified: true,
|
|
652
|
-
active_label: label,
|
|
653
|
-
control: matched
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
await sleep(intervalMs);
|
|
657
|
-
}
|
|
658
|
-
return {
|
|
659
|
-
ok: false,
|
|
660
|
-
error: `CHAT_MESSAGE_FILTER_NOT_FOUND:${label}`,
|
|
661
|
-
candidates: lastCandidates.map((item) => ({
|
|
662
|
-
label: item.label,
|
|
663
|
-
selector: item.selector,
|
|
664
|
-
active: item.active
|
|
665
|
-
}))
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
export async function waitForChatResumeModal(client, {
|
|
670
|
-
timeoutMs = 12000,
|
|
671
|
-
intervalMs = 250
|
|
672
|
-
} = {}) {
|
|
673
|
-
const started = Date.now();
|
|
674
|
-
let lastState = null;
|
|
675
|
-
while (Date.now() - started <= timeoutMs) {
|
|
676
|
-
const topLevelState = await getChatTopLevelState(client);
|
|
677
|
-
if (topLevelState.is_forbidden_resume_top_level) {
|
|
678
|
-
return {
|
|
679
|
-
forbidden_top_level_navigation: true,
|
|
680
|
-
top_level_state: topLevelState
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
const rootState = await getChatRoots(client);
|
|
684
|
-
const popup = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_MODAL_SELECTORS);
|
|
685
|
-
const content = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_CONTENT_SELECTORS);
|
|
686
|
-
const resumeIframe = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_IFRAME_SELECTORS);
|
|
687
|
-
lastState = {
|
|
688
|
-
roots: rootState.roots,
|
|
689
|
-
popup,
|
|
690
|
-
content,
|
|
691
|
-
resumeIframe
|
|
692
|
-
};
|
|
693
|
-
if (popup || content || resumeIframe) return lastState;
|
|
694
|
-
await sleep(intervalMs);
|
|
695
|
-
}
|
|
696
|
-
return lastState;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
export async function quickChatResumeModalOpenProbe(client, {
|
|
700
|
-
selectors = CHAT_RESUME_FAST_MODAL_SELECTORS
|
|
701
|
-
} = {}) {
|
|
702
|
-
const rootState = await getChatRoots(client);
|
|
703
|
-
for (const root of rootState.roots) {
|
|
704
|
-
if (!root?.nodeId) continue;
|
|
705
|
-
for (const selector of selectors) {
|
|
706
|
-
let nodeIds = [];
|
|
707
|
-
try {
|
|
708
|
-
nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
709
|
-
} catch {
|
|
710
|
-
nodeIds = [];
|
|
711
|
-
}
|
|
712
|
-
for (const nodeId of nodeIds.slice(0, 4)) {
|
|
713
|
-
try {
|
|
714
|
-
const box = await getNodeBox(client, nodeId);
|
|
715
|
-
if (box?.rect?.width > 8 && box?.rect?.height > 8) {
|
|
716
|
-
return {
|
|
717
|
-
open: true,
|
|
718
|
-
root: root.name,
|
|
719
|
-
selector,
|
|
720
|
-
node_id: nodeId,
|
|
721
|
-
rect: box.rect,
|
|
722
|
-
center: box.center
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
} catch {
|
|
726
|
-
// Hidden or stale modal probes are ignored.
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return {
|
|
732
|
-
open: false
|
|
733
|
-
};
|
|
734
762
|
}
|
|
735
763
|
|
|
736
764
|
export async function readChatResumeHtml(client, resumeState) {
|
|
737
|
-
let popupHTML = "";
|
|
738
|
-
let contentHTML = "";
|
|
739
|
-
let resumeIframeHTML = "";
|
|
740
|
-
let resumeIframeDocumentNodeId = null;
|
|
741
|
-
|
|
742
|
-
if (resumeState?.popup?.node_id) {
|
|
743
|
-
popupHTML = await getOuterHTML(client, resumeState.popup.node_id);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (resumeState?.content?.node_id && resumeState.content.node_id !== resumeState?.popup?.node_id) {
|
|
747
|
-
contentHTML = await getOuterHTML(client, resumeState.content.node_id);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (resumeState?.resumeIframe?.node_id) {
|
|
751
|
-
resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, resumeState.resumeIframe.node_id);
|
|
752
|
-
resumeIframeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
return {
|
|
756
|
-
popupHTML,
|
|
757
|
-
contentHTML,
|
|
758
|
-
resumeIframeHTML,
|
|
759
|
-
resumeIframeDocumentNodeId,
|
|
760
|
-
popupText: htmlToText(popupHTML),
|
|
761
|
-
contentText: htmlToText(contentHTML),
|
|
762
|
-
resumeIframeText: htmlToText(resumeIframeHTML)
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function emptyChatResumeHtml(readError = null) {
|
|
767
|
-
return {
|
|
768
|
-
popupHTML: "",
|
|
769
|
-
contentHTML: "",
|
|
770
|
-
resumeIframeHTML: "",
|
|
771
|
-
resumeIframeDocumentNodeId: null,
|
|
772
|
-
popupText: "",
|
|
773
|
-
contentText: "",
|
|
774
|
-
resumeIframeText: "",
|
|
775
|
-
readError: readError?.message || null
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
export async function waitForChatResumeContent(client, {
|
|
780
|
-
minTextLength = 120,
|
|
781
|
-
timeoutMs = 15000,
|
|
782
|
-
intervalMs = 250
|
|
783
|
-
} = {}) {
|
|
784
|
-
const started = Date.now();
|
|
785
|
-
let lastState = null;
|
|
786
|
-
let lastHtml = null;
|
|
787
|
-
let lastError = null;
|
|
788
|
-
while (Date.now() - started <= timeoutMs) {
|
|
789
|
-
try {
|
|
790
|
-
lastState = await waitForChatResumeModal(client, {
|
|
791
|
-
timeoutMs: 700,
|
|
792
|
-
intervalMs: 100
|
|
793
|
-
});
|
|
794
|
-
if (lastState?.popup || lastState?.content || lastState?.resumeIframe) {
|
|
795
|
-
lastHtml = await readChatResumeHtml(client, lastState);
|
|
796
|
-
const textLength = [
|
|
797
|
-
lastHtml.popupText,
|
|
798
|
-
lastHtml.contentText,
|
|
799
|
-
lastHtml.resumeIframeText
|
|
800
|
-
].join("\n").length;
|
|
801
|
-
if (textLength >= minTextLength) {
|
|
802
|
-
return {
|
|
803
|
-
ok: true,
|
|
804
|
-
elapsed_ms: Date.now() - started,
|
|
805
|
-
text_length: textLength,
|
|
806
|
-
resume_state: lastState,
|
|
807
|
-
resume_html: lastHtml
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
} catch (error) {
|
|
812
|
-
lastError = error;
|
|
813
|
-
}
|
|
814
|
-
await sleep(intervalMs);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
const textLength = [
|
|
818
|
-
lastHtml?.popupText,
|
|
819
|
-
lastHtml?.contentText,
|
|
820
|
-
lastHtml?.resumeIframeText
|
|
821
|
-
].filter(Boolean).join("\n").length;
|
|
822
|
-
return {
|
|
823
|
-
ok: false,
|
|
824
|
-
elapsed_ms: Date.now() - started,
|
|
825
|
-
text_length: textLength,
|
|
826
|
-
resume_state: lastState,
|
|
827
|
-
resume_html: lastHtml,
|
|
828
|
-
error: lastError?.message || null
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
export async function openChatOnlineResume(client, {
|
|
833
|
-
timeoutMs = 15000,
|
|
834
|
-
attemptsLimit = 3,
|
|
835
|
-
settleMs = 1200
|
|
836
|
-
} = {}) {
|
|
837
|
-
const attempts = [];
|
|
838
|
-
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
839
|
-
if (settleMs > 0) await sleep(settleMs);
|
|
840
|
-
await assertChatShellNotResumeTopLevel(client, {
|
|
841
|
-
context: "openChatOnlineResume:before_existing_modal_check"
|
|
842
|
-
});
|
|
843
|
-
const existingResumeState = await waitForChatResumeModal(client, {
|
|
844
|
-
timeoutMs: 500,
|
|
845
|
-
intervalMs: 100
|
|
846
|
-
});
|
|
847
|
-
if (existingResumeState?.forbidden_top_level_navigation) {
|
|
848
|
-
throw makeForbiddenChatResumeNavigationError(existingResumeState.top_level_state);
|
|
849
|
-
}
|
|
850
|
-
if (
|
|
851
|
-
existingResumeState?.popup
|
|
852
|
-
|| existingResumeState?.content
|
|
853
|
-
|| existingResumeState?.resumeIframe
|
|
854
|
-
) {
|
|
855
|
-
attempts.push({
|
|
856
|
-
attempt: index + 1,
|
|
857
|
-
ok: true,
|
|
858
|
-
reused_existing_modal: true,
|
|
859
|
-
resume_popup_selector: existingResumeState?.popup?.selector || null,
|
|
860
|
-
resume_content_selector: existingResumeState?.content?.selector || null,
|
|
861
|
-
resume_iframe_selector: existingResumeState?.resumeIframe?.selector || null
|
|
862
|
-
});
|
|
863
|
-
return {
|
|
864
|
-
button: null,
|
|
865
|
-
button_html: "",
|
|
866
|
-
resume_state: existingResumeState,
|
|
867
|
-
attempts
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
const buttonState = await waitForChatOnlineResumeButton(client, {
|
|
872
|
-
timeoutMs: Math.min(timeoutMs, 8000)
|
|
873
|
-
});
|
|
874
|
-
if (!buttonState?.target?.node_id) {
|
|
875
|
-
attempts.push({
|
|
876
|
-
attempt: index + 1,
|
|
877
|
-
ok: false,
|
|
878
|
-
error: "ONLINE_RESUME_BUTTON_NOT_FOUND"
|
|
879
|
-
});
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
let buttonHTML = "";
|
|
884
|
-
try {
|
|
885
|
-
buttonHTML = await getOuterHTML(client, buttonState.target.node_id);
|
|
886
|
-
} catch {}
|
|
887
|
-
|
|
888
|
-
if (isUnsafeChatOnlineResumeTarget(buttonState.target, buttonHTML)) {
|
|
889
|
-
const error = makeUnsafeChatOnlineResumeLinkError(buttonState.target, buttonHTML);
|
|
890
|
-
attempts.push({
|
|
891
|
-
attempt: index + 1,
|
|
892
|
-
ok: false,
|
|
893
|
-
error: error.code,
|
|
894
|
-
blocked_pre_click: true,
|
|
895
|
-
button_selector: buttonState.target.selector,
|
|
896
|
-
button_text: error.button_text,
|
|
897
|
-
button_href: error.href,
|
|
898
|
-
button_html_length: buttonHTML.length
|
|
899
|
-
});
|
|
900
|
-
error.attempts = attempts;
|
|
901
|
-
throw error;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
try {
|
|
905
|
-
if (buttonState.target.center) {
|
|
906
|
-
await clickPoint(client, buttonState.target.center.x, buttonState.target.center.y);
|
|
907
|
-
} else {
|
|
908
|
-
await clickNodeCenter(client, buttonState.target.node_id, {
|
|
909
|
-
scrollIntoView: true
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
} catch (error) {
|
|
913
|
-
attempts.push({
|
|
914
|
-
attempt: index + 1,
|
|
915
|
-
ok: false,
|
|
916
|
-
error: error?.message || String(error),
|
|
917
|
-
recoverable_stale_node: isRecoverableNodeError(error),
|
|
918
|
-
button_selector: buttonState.target.selector,
|
|
919
|
-
button_text: htmlToText(buttonHTML).slice(0, 120),
|
|
920
|
-
button_html_length: buttonHTML.length
|
|
921
|
-
});
|
|
922
|
-
if (isRecoverableNodeError(error)) {
|
|
923
|
-
await sleep(350);
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
throw error;
|
|
927
|
-
}
|
|
928
|
-
await assertChatShellNotResumeTopLevel(client, {
|
|
929
|
-
context: "openChatOnlineResume:after_online_resume_click"
|
|
930
|
-
});
|
|
931
|
-
const resumeState = await waitForChatResumeModal(client, {
|
|
932
|
-
timeoutMs: Math.max(2500, Math.floor(timeoutMs / attemptsLimit))
|
|
933
|
-
});
|
|
934
|
-
if (resumeState?.forbidden_top_level_navigation) {
|
|
935
|
-
throw makeForbiddenChatResumeNavigationError(resumeState.top_level_state);
|
|
936
|
-
}
|
|
937
|
-
attempts.push({
|
|
938
|
-
attempt: index + 1,
|
|
939
|
-
ok: Boolean(resumeState?.popup || resumeState?.content || resumeState?.resumeIframe),
|
|
940
|
-
button_selector: buttonState.target.selector,
|
|
941
|
-
button_text: htmlToText(buttonHTML).slice(0, 120),
|
|
942
|
-
button_html_length: buttonHTML.length,
|
|
943
|
-
resume_popup_selector: resumeState?.popup?.selector || null,
|
|
944
|
-
resume_content_selector: resumeState?.content?.selector || null,
|
|
945
|
-
resume_iframe_selector: resumeState?.resumeIframe?.selector || null
|
|
946
|
-
});
|
|
947
|
-
if (resumeState?.popup || resumeState?.content || resumeState?.resumeIframe) {
|
|
948
|
-
return {
|
|
949
|
-
button: buttonState.target,
|
|
950
|
-
button_html: buttonHTML,
|
|
951
|
-
resume_state: resumeState,
|
|
952
|
-
attempts
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
const error = new Error("Chat online resume modal did not open");
|
|
958
|
-
error.attempts = attempts;
|
|
959
|
-
throw error;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
export async function readChatConversationReadyState(client) {
|
|
963
|
-
const rootState = await getChatRoots(client);
|
|
964
|
-
const scopedControlRoots = await resolveScopedRoots(
|
|
965
|
-
client,
|
|
966
|
-
rootState.roots,
|
|
967
|
-
CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS,
|
|
968
|
-
{ fallbackToRoots: false }
|
|
969
|
-
);
|
|
970
|
-
const scopedRequestedRoots = await resolveScopedRoots(
|
|
971
|
-
client,
|
|
972
|
-
rootState.roots,
|
|
973
|
-
CHAT_REQUESTED_RESUME_SCOPE_SELECTORS,
|
|
974
|
-
{ fallbackToRoots: false }
|
|
975
|
-
);
|
|
976
|
-
const controlRoots = scopedControlRoots.length ? scopedControlRoots : rootState.roots;
|
|
977
|
-
const requestedRoots = scopedRequestedRoots.length ? scopedRequestedRoots : rootState.roots;
|
|
978
|
-
const onlineResume = await findVisibleMatchingTarget(
|
|
979
|
-
client,
|
|
980
|
-
controlRoots,
|
|
981
|
-
CHAT_ONLINE_RESUME_BUTTON_SELECTORS,
|
|
982
|
-
(target) => target.label.includes("在线简历") && !target.disabled
|
|
983
|
-
);
|
|
984
|
-
const attachmentResume = await findVisibleMatchingTarget(
|
|
985
|
-
client,
|
|
986
|
-
controlRoots,
|
|
987
|
-
CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
|
|
988
|
-
(target) => isAttachmentResumeText(target.label)
|
|
989
|
-
);
|
|
990
|
-
const askResume = await findVisibleMatchingTarget(
|
|
991
|
-
client,
|
|
992
|
-
controlRoots,
|
|
993
|
-
CHAT_ASK_RESUME_BUTTON_SELECTORS,
|
|
994
|
-
(target) => isAskResumeText(target.label) && !isAttachmentResumeTarget(target)
|
|
995
|
-
);
|
|
996
|
-
const requestedResume = await findVisibleMatchingTarget(
|
|
997
|
-
client,
|
|
998
|
-
requestedRoots,
|
|
999
|
-
CHAT_ASK_RESUME_BUTTON_SELECTORS,
|
|
1000
|
-
(target) => isRequestedResumeControlTarget(target)
|
|
1001
|
-
);
|
|
1002
|
-
const editor = await findVisibleMatchingTarget(
|
|
1003
|
-
client,
|
|
1004
|
-
controlRoots,
|
|
1005
|
-
CHAT_EDITOR_SELECTORS,
|
|
1006
|
-
() => true
|
|
1007
|
-
);
|
|
1008
|
-
const sendButton = await findVisibleMatchingTarget(
|
|
1009
|
-
client,
|
|
1010
|
-
controlRoots,
|
|
1011
|
-
CHAT_SEND_BUTTON_SELECTORS,
|
|
1012
|
-
(target) => isSendText(target.label) || /submit/i.test(String(target.attributes?.class || ""))
|
|
1013
|
-
);
|
|
1014
|
-
const resumeState = await waitForChatResumeModal(client, { timeoutMs: 300 });
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
if (
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
);
|
|
1227
|
-
const
|
|
1228
|
-
client,
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if (
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1290
|
-
const
|
|
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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
765
|
+
let popupHTML = "";
|
|
766
|
+
let contentHTML = "";
|
|
767
|
+
let resumeIframeHTML = "";
|
|
768
|
+
let resumeIframeDocumentNodeId = null;
|
|
769
|
+
|
|
770
|
+
if (resumeState?.popup?.node_id) {
|
|
771
|
+
popupHTML = await getOuterHTML(client, resumeState.popup.node_id);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (resumeState?.content?.node_id && resumeState.content.node_id !== resumeState?.popup?.node_id) {
|
|
775
|
+
contentHTML = await getOuterHTML(client, resumeState.content.node_id);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (resumeState?.resumeIframe?.node_id) {
|
|
779
|
+
resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, resumeState.resumeIframe.node_id);
|
|
780
|
+
resumeIframeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
popupHTML,
|
|
785
|
+
contentHTML,
|
|
786
|
+
resumeIframeHTML,
|
|
787
|
+
resumeIframeDocumentNodeId,
|
|
788
|
+
popupText: htmlToText(popupHTML),
|
|
789
|
+
contentText: htmlToText(contentHTML),
|
|
790
|
+
resumeIframeText: htmlToText(resumeIframeHTML)
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function emptyChatResumeHtml(readError = null) {
|
|
795
|
+
return {
|
|
796
|
+
popupHTML: "",
|
|
797
|
+
contentHTML: "",
|
|
798
|
+
resumeIframeHTML: "",
|
|
799
|
+
resumeIframeDocumentNodeId: null,
|
|
800
|
+
popupText: "",
|
|
801
|
+
contentText: "",
|
|
802
|
+
resumeIframeText: "",
|
|
803
|
+
readError: readError?.message || null
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export async function waitForChatResumeContent(client, {
|
|
808
|
+
minTextLength = 120,
|
|
809
|
+
timeoutMs = 15000,
|
|
810
|
+
intervalMs = 250
|
|
811
|
+
} = {}) {
|
|
812
|
+
const started = Date.now();
|
|
813
|
+
let lastState = null;
|
|
814
|
+
let lastHtml = null;
|
|
815
|
+
let lastError = null;
|
|
816
|
+
while (Date.now() - started <= timeoutMs) {
|
|
817
|
+
try {
|
|
818
|
+
lastState = await waitForChatResumeModal(client, {
|
|
819
|
+
timeoutMs: 700,
|
|
820
|
+
intervalMs: 100
|
|
821
|
+
});
|
|
822
|
+
if (lastState?.popup || lastState?.content || lastState?.resumeIframe) {
|
|
823
|
+
lastHtml = await readChatResumeHtml(client, lastState);
|
|
824
|
+
const textLength = [
|
|
825
|
+
lastHtml.popupText,
|
|
826
|
+
lastHtml.contentText,
|
|
827
|
+
lastHtml.resumeIframeText
|
|
828
|
+
].join("\n").length;
|
|
829
|
+
if (textLength >= minTextLength) {
|
|
830
|
+
return {
|
|
831
|
+
ok: true,
|
|
832
|
+
elapsed_ms: Date.now() - started,
|
|
833
|
+
text_length: textLength,
|
|
834
|
+
resume_state: lastState,
|
|
835
|
+
resume_html: lastHtml
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} catch (error) {
|
|
840
|
+
lastError = error;
|
|
841
|
+
}
|
|
842
|
+
await sleep(intervalMs);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const textLength = [
|
|
846
|
+
lastHtml?.popupText,
|
|
847
|
+
lastHtml?.contentText,
|
|
848
|
+
lastHtml?.resumeIframeText
|
|
849
|
+
].filter(Boolean).join("\n").length;
|
|
850
|
+
return {
|
|
851
|
+
ok: false,
|
|
852
|
+
elapsed_ms: Date.now() - started,
|
|
853
|
+
text_length: textLength,
|
|
854
|
+
resume_state: lastState,
|
|
855
|
+
resume_html: lastHtml,
|
|
856
|
+
error: lastError?.message || null
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export async function openChatOnlineResume(client, {
|
|
861
|
+
timeoutMs = 15000,
|
|
862
|
+
attemptsLimit = 3,
|
|
863
|
+
settleMs = 1200
|
|
864
|
+
} = {}) {
|
|
865
|
+
const attempts = [];
|
|
866
|
+
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
867
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
868
|
+
await assertChatShellNotResumeTopLevel(client, {
|
|
869
|
+
context: "openChatOnlineResume:before_existing_modal_check"
|
|
870
|
+
});
|
|
871
|
+
const existingResumeState = await waitForChatResumeModal(client, {
|
|
872
|
+
timeoutMs: 500,
|
|
873
|
+
intervalMs: 100
|
|
874
|
+
});
|
|
875
|
+
if (existingResumeState?.forbidden_top_level_navigation) {
|
|
876
|
+
throw makeForbiddenChatResumeNavigationError(existingResumeState.top_level_state);
|
|
877
|
+
}
|
|
878
|
+
if (
|
|
879
|
+
existingResumeState?.popup
|
|
880
|
+
|| existingResumeState?.content
|
|
881
|
+
|| existingResumeState?.resumeIframe
|
|
882
|
+
) {
|
|
883
|
+
attempts.push({
|
|
884
|
+
attempt: index + 1,
|
|
885
|
+
ok: true,
|
|
886
|
+
reused_existing_modal: true,
|
|
887
|
+
resume_popup_selector: existingResumeState?.popup?.selector || null,
|
|
888
|
+
resume_content_selector: existingResumeState?.content?.selector || null,
|
|
889
|
+
resume_iframe_selector: existingResumeState?.resumeIframe?.selector || null
|
|
890
|
+
});
|
|
891
|
+
return {
|
|
892
|
+
button: null,
|
|
893
|
+
button_html: "",
|
|
894
|
+
resume_state: existingResumeState,
|
|
895
|
+
attempts
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const buttonState = await waitForChatOnlineResumeButton(client, {
|
|
900
|
+
timeoutMs: Math.min(timeoutMs, 8000)
|
|
901
|
+
});
|
|
902
|
+
if (!buttonState?.target?.node_id) {
|
|
903
|
+
attempts.push({
|
|
904
|
+
attempt: index + 1,
|
|
905
|
+
ok: false,
|
|
906
|
+
error: "ONLINE_RESUME_BUTTON_NOT_FOUND"
|
|
907
|
+
});
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
let buttonHTML = "";
|
|
912
|
+
try {
|
|
913
|
+
buttonHTML = await getOuterHTML(client, buttonState.target.node_id);
|
|
914
|
+
} catch {}
|
|
915
|
+
|
|
916
|
+
if (isUnsafeChatOnlineResumeTarget(buttonState.target, buttonHTML)) {
|
|
917
|
+
const error = makeUnsafeChatOnlineResumeLinkError(buttonState.target, buttonHTML);
|
|
918
|
+
attempts.push({
|
|
919
|
+
attempt: index + 1,
|
|
920
|
+
ok: false,
|
|
921
|
+
error: error.code,
|
|
922
|
+
blocked_pre_click: true,
|
|
923
|
+
button_selector: buttonState.target.selector,
|
|
924
|
+
button_text: error.button_text,
|
|
925
|
+
button_href: error.href,
|
|
926
|
+
button_html_length: buttonHTML.length
|
|
927
|
+
});
|
|
928
|
+
error.attempts = attempts;
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
try {
|
|
933
|
+
if (buttonState.target.center) {
|
|
934
|
+
await clickPoint(client, buttonState.target.center.x, buttonState.target.center.y);
|
|
935
|
+
} else {
|
|
936
|
+
await clickNodeCenter(client, buttonState.target.node_id, {
|
|
937
|
+
scrollIntoView: true
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
} catch (error) {
|
|
941
|
+
attempts.push({
|
|
942
|
+
attempt: index + 1,
|
|
943
|
+
ok: false,
|
|
944
|
+
error: error?.message || String(error),
|
|
945
|
+
recoverable_stale_node: isRecoverableNodeError(error),
|
|
946
|
+
button_selector: buttonState.target.selector,
|
|
947
|
+
button_text: htmlToText(buttonHTML).slice(0, 120),
|
|
948
|
+
button_html_length: buttonHTML.length
|
|
949
|
+
});
|
|
950
|
+
if (isRecoverableNodeError(error)) {
|
|
951
|
+
await sleep(350);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
await assertChatShellNotResumeTopLevel(client, {
|
|
957
|
+
context: "openChatOnlineResume:after_online_resume_click"
|
|
958
|
+
});
|
|
959
|
+
const resumeState = await waitForChatResumeModal(client, {
|
|
960
|
+
timeoutMs: Math.max(2500, Math.floor(timeoutMs / attemptsLimit))
|
|
961
|
+
});
|
|
962
|
+
if (resumeState?.forbidden_top_level_navigation) {
|
|
963
|
+
throw makeForbiddenChatResumeNavigationError(resumeState.top_level_state);
|
|
964
|
+
}
|
|
965
|
+
attempts.push({
|
|
966
|
+
attempt: index + 1,
|
|
967
|
+
ok: Boolean(resumeState?.popup || resumeState?.content || resumeState?.resumeIframe),
|
|
968
|
+
button_selector: buttonState.target.selector,
|
|
969
|
+
button_text: htmlToText(buttonHTML).slice(0, 120),
|
|
970
|
+
button_html_length: buttonHTML.length,
|
|
971
|
+
resume_popup_selector: resumeState?.popup?.selector || null,
|
|
972
|
+
resume_content_selector: resumeState?.content?.selector || null,
|
|
973
|
+
resume_iframe_selector: resumeState?.resumeIframe?.selector || null
|
|
974
|
+
});
|
|
975
|
+
if (resumeState?.popup || resumeState?.content || resumeState?.resumeIframe) {
|
|
976
|
+
return {
|
|
977
|
+
button: buttonState.target,
|
|
978
|
+
button_html: buttonHTML,
|
|
979
|
+
resume_state: resumeState,
|
|
980
|
+
attempts
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const error = new Error("Chat online resume modal did not open");
|
|
986
|
+
error.attempts = attempts;
|
|
987
|
+
throw error;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
export async function readChatConversationReadyState(client) {
|
|
991
|
+
const rootState = await getChatRoots(client);
|
|
992
|
+
const scopedControlRoots = await resolveScopedRoots(
|
|
993
|
+
client,
|
|
994
|
+
rootState.roots,
|
|
995
|
+
CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS,
|
|
996
|
+
{ fallbackToRoots: false }
|
|
997
|
+
);
|
|
998
|
+
const scopedRequestedRoots = await resolveScopedRoots(
|
|
999
|
+
client,
|
|
1000
|
+
rootState.roots,
|
|
1001
|
+
CHAT_REQUESTED_RESUME_SCOPE_SELECTORS,
|
|
1002
|
+
{ fallbackToRoots: false }
|
|
1003
|
+
);
|
|
1004
|
+
const controlRoots = scopedControlRoots.length ? scopedControlRoots : rootState.roots;
|
|
1005
|
+
const requestedRoots = scopedRequestedRoots.length ? scopedRequestedRoots : rootState.roots;
|
|
1006
|
+
const onlineResume = await findVisibleMatchingTarget(
|
|
1007
|
+
client,
|
|
1008
|
+
controlRoots,
|
|
1009
|
+
CHAT_ONLINE_RESUME_BUTTON_SELECTORS,
|
|
1010
|
+
(target) => target.label.includes("在线简历") && !target.disabled
|
|
1011
|
+
);
|
|
1012
|
+
const attachmentResume = await findVisibleMatchingTarget(
|
|
1013
|
+
client,
|
|
1014
|
+
controlRoots,
|
|
1015
|
+
CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
|
|
1016
|
+
(target) => isAttachmentResumeText(target.label)
|
|
1017
|
+
);
|
|
1018
|
+
const askResume = await findVisibleMatchingTarget(
|
|
1019
|
+
client,
|
|
1020
|
+
controlRoots,
|
|
1021
|
+
CHAT_ASK_RESUME_BUTTON_SELECTORS,
|
|
1022
|
+
(target) => isAskResumeText(target.label) && !isAttachmentResumeTarget(target)
|
|
1023
|
+
);
|
|
1024
|
+
const requestedResume = await findVisibleMatchingTarget(
|
|
1025
|
+
client,
|
|
1026
|
+
requestedRoots,
|
|
1027
|
+
CHAT_ASK_RESUME_BUTTON_SELECTORS,
|
|
1028
|
+
(target) => isRequestedResumeControlTarget(target)
|
|
1029
|
+
);
|
|
1030
|
+
const editor = await findVisibleMatchingTarget(
|
|
1031
|
+
client,
|
|
1032
|
+
controlRoots,
|
|
1033
|
+
CHAT_EDITOR_SELECTORS,
|
|
1034
|
+
() => true
|
|
1035
|
+
);
|
|
1036
|
+
const sendButton = await findVisibleMatchingTarget(
|
|
1037
|
+
client,
|
|
1038
|
+
controlRoots,
|
|
1039
|
+
CHAT_SEND_BUTTON_SELECTORS,
|
|
1040
|
+
(target) => isSendText(target.label) || /submit/i.test(String(target.attributes?.class || ""))
|
|
1041
|
+
);
|
|
1042
|
+
const resumeState = await waitForChatResumeModal(client, { timeoutMs: 300 });
|
|
1043
|
+
const blockingPanel = await findChatBlockingPanel(client);
|
|
1044
|
+
const resumeModalOpen = Boolean(resumeState?.popup || resumeState?.content || resumeState?.resumeIframe);
|
|
1045
|
+
const blockingPanelOpen = Boolean(blockingPanel?.open);
|
|
1046
|
+
return {
|
|
1047
|
+
has_online_resume: Boolean(onlineResume),
|
|
1048
|
+
online_resume: onlineResume,
|
|
1049
|
+
has_ask_resume: Boolean(askResume),
|
|
1050
|
+
ask_resume: askResume,
|
|
1051
|
+
already_requested_resume: Boolean(requestedResume),
|
|
1052
|
+
requested_resume: requestedResume,
|
|
1053
|
+
has_attachment_resume: Boolean(attachmentResume),
|
|
1054
|
+
attachment_resume_enabled: Boolean(attachmentResume && !attachmentResume.disabled),
|
|
1055
|
+
attachment_resume: attachmentResume,
|
|
1056
|
+
editor_visible: Boolean(editor),
|
|
1057
|
+
editor,
|
|
1058
|
+
send_button_visible: Boolean(sendButton),
|
|
1059
|
+
send_button: sendButton,
|
|
1060
|
+
resume_modal_open: resumeModalOpen,
|
|
1061
|
+
blocking_panel_open: blockingPanelOpen,
|
|
1062
|
+
blocking_panel: blockingPanelOpen ? blockingPanel : null,
|
|
1063
|
+
panels_closed: !resumeModalOpen && !blockingPanelOpen
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
export async function setChatEditorMessage(client, message, {
|
|
1068
|
+
timeoutMs = 8000
|
|
1069
|
+
} = {}) {
|
|
1070
|
+
const started = Date.now();
|
|
1071
|
+
let lastState = null;
|
|
1072
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1073
|
+
const state = await readChatConversationReadyState(client);
|
|
1074
|
+
lastState = state;
|
|
1075
|
+
if (state.editor?.node_id) {
|
|
1076
|
+
try {
|
|
1077
|
+
if (state.editor.center) {
|
|
1078
|
+
await clickPoint(client, state.editor.center.x, state.editor.center.y);
|
|
1079
|
+
} else {
|
|
1080
|
+
await clickNodeCenter(client, state.editor.node_id, { scrollIntoView: true });
|
|
1081
|
+
}
|
|
1082
|
+
await sleep(120);
|
|
1083
|
+
await clearFocusedInput(client);
|
|
1084
|
+
await sleep(80);
|
|
1085
|
+
await insertText(client, message);
|
|
1086
|
+
await sleep(250);
|
|
1087
|
+
const afterState = await readChatConversationReadyState(client);
|
|
1088
|
+
const editorText = normalizeDetailText(afterState.editor?.label || "");
|
|
1089
|
+
if (editorText.includes(normalizeDetailText(message))) {
|
|
1090
|
+
return {
|
|
1091
|
+
ok: true,
|
|
1092
|
+
value: editorText,
|
|
1093
|
+
editor: afterState.editor || state.editor
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
lastState = {
|
|
1097
|
+
...afterState,
|
|
1098
|
+
editor_message_mismatch: true,
|
|
1099
|
+
editor_text: editorText
|
|
1100
|
+
};
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
1103
|
+
lastState = {
|
|
1104
|
+
...state,
|
|
1105
|
+
recoverable_error: error?.message || String(error),
|
|
1106
|
+
recoverable_phase: "set_editor_message"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
await sleep(250);
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
ok: false,
|
|
1114
|
+
error: "CHAT_EDITOR_NOT_FOUND",
|
|
1115
|
+
state: lastState
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
export async function sendChatMessage(client, expectedText = "", {
|
|
1120
|
+
timeoutMs = 8000,
|
|
1121
|
+
settleMs = 800
|
|
1122
|
+
} = {}) {
|
|
1123
|
+
const started = Date.now();
|
|
1124
|
+
let lastState = null;
|
|
1125
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1126
|
+
const state = await readChatConversationReadyState(client);
|
|
1127
|
+
lastState = state;
|
|
1128
|
+
if (state.send_button?.node_id && !state.send_button.disabled) {
|
|
1129
|
+
try {
|
|
1130
|
+
if (state.send_button.center) {
|
|
1131
|
+
await clickPoint(client, state.send_button.center.x, state.send_button.center.y);
|
|
1132
|
+
} else {
|
|
1133
|
+
await clickNodeCenter(client, state.send_button.node_id, { scrollIntoView: true });
|
|
1134
|
+
}
|
|
1135
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1136
|
+
return {
|
|
1137
|
+
sent: true,
|
|
1138
|
+
method: "send-button",
|
|
1139
|
+
control: state.send_button,
|
|
1140
|
+
expected_text: expectedText
|
|
1141
|
+
};
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
1144
|
+
lastState = {
|
|
1145
|
+
...state,
|
|
1146
|
+
recoverable_error: error?.message || String(error),
|
|
1147
|
+
recoverable_phase: "send_button_click"
|
|
1148
|
+
};
|
|
1149
|
+
await sleep(250);
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (state.editor?.node_id) {
|
|
1154
|
+
await pressKey(client, "Enter", {
|
|
1155
|
+
code: "Enter",
|
|
1156
|
+
windowsVirtualKeyCode: 13,
|
|
1157
|
+
nativeVirtualKeyCode: 13
|
|
1158
|
+
});
|
|
1159
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1160
|
+
return {
|
|
1161
|
+
sent: true,
|
|
1162
|
+
method: "enter",
|
|
1163
|
+
expected_text: expectedText
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
await sleep(250);
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
sent: false,
|
|
1170
|
+
method: "none",
|
|
1171
|
+
error: "CHAT_SEND_CONTROL_NOT_FOUND",
|
|
1172
|
+
state: lastState
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export async function clickChatAskResume(client, {
|
|
1177
|
+
timeoutMs = 8000,
|
|
1178
|
+
settleMs = 700
|
|
1179
|
+
} = {}) {
|
|
1180
|
+
const started = Date.now();
|
|
1181
|
+
let lastState = null;
|
|
1182
|
+
let lastDisabledAskResume = null;
|
|
1183
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1184
|
+
const state = await readChatConversationReadyState(client);
|
|
1185
|
+
lastState = state;
|
|
1186
|
+
if (state.attachment_resume_enabled) {
|
|
1187
|
+
return {
|
|
1188
|
+
ok: true,
|
|
1189
|
+
already_requested: true,
|
|
1190
|
+
attachment_resume_available: true,
|
|
1191
|
+
control: state.attachment_resume
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
if (state.ask_resume?.node_id && !state.ask_resume.disabled) {
|
|
1195
|
+
try {
|
|
1196
|
+
if (state.ask_resume.center) {
|
|
1197
|
+
await clickPoint(client, state.ask_resume.center.x, state.ask_resume.center.y);
|
|
1198
|
+
} else {
|
|
1199
|
+
await clickNodeCenter(client, state.ask_resume.node_id, { scrollIntoView: true });
|
|
1200
|
+
}
|
|
1201
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1202
|
+
return {
|
|
1203
|
+
ok: true,
|
|
1204
|
+
already_requested: false,
|
|
1205
|
+
control: state.ask_resume
|
|
1206
|
+
};
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
1209
|
+
lastState = {
|
|
1210
|
+
...state,
|
|
1211
|
+
recoverable_error: error?.message || String(error),
|
|
1212
|
+
recoverable_phase: "ask_resume_click"
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (state.ask_resume?.node_id && state.ask_resume.disabled) {
|
|
1217
|
+
lastDisabledAskResume = state.ask_resume;
|
|
1218
|
+
}
|
|
1219
|
+
if (state.already_requested_resume) {
|
|
1220
|
+
return {
|
|
1221
|
+
ok: true,
|
|
1222
|
+
already_requested: true,
|
|
1223
|
+
control: state.requested_resume
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
await sleep(250);
|
|
1227
|
+
}
|
|
1228
|
+
if (lastDisabledAskResume) {
|
|
1229
|
+
return {
|
|
1230
|
+
ok: false,
|
|
1231
|
+
already_requested: true,
|
|
1232
|
+
request_pending: true,
|
|
1233
|
+
error: "ASK_RESUME_BUTTON_DISABLED",
|
|
1234
|
+
control: lastDisabledAskResume,
|
|
1235
|
+
state: lastState
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
ok: false,
|
|
1240
|
+
error: "ASK_RESUME_BUTTON_NOT_FOUND",
|
|
1241
|
+
state: lastState
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export async function clickChatConfirmRequestResume(client, {
|
|
1246
|
+
timeoutMs = 8000,
|
|
1247
|
+
settleMs = 900
|
|
1248
|
+
} = {}) {
|
|
1249
|
+
const started = Date.now();
|
|
1250
|
+
let lastTarget = null;
|
|
1251
|
+
let lastState = null;
|
|
1252
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1253
|
+
lastState = await readChatConversationReadyState(client);
|
|
1254
|
+
const rootState = await getChatRoots(client);
|
|
1255
|
+
const confirmRoots = await resolveScopedRoots(
|
|
1256
|
+
client,
|
|
1257
|
+
rootState.roots,
|
|
1258
|
+
CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS
|
|
1259
|
+
);
|
|
1260
|
+
const target = await findVisibleMatchingTarget(
|
|
1261
|
+
client,
|
|
1262
|
+
confirmRoots,
|
|
1263
|
+
CHAT_CONFIRM_REQUEST_RESUME_SELECTORS,
|
|
1264
|
+
(item) => isConfirmText(item.label) && !item.disabled
|
|
1265
|
+
);
|
|
1266
|
+
lastTarget = target;
|
|
1267
|
+
if (target?.node_id) {
|
|
1268
|
+
try {
|
|
1269
|
+
if (target.center) {
|
|
1270
|
+
await clickPoint(client, target.center.x, target.center.y);
|
|
1271
|
+
} else {
|
|
1272
|
+
await clickNodeCenter(client, target.node_id, { scrollIntoView: true });
|
|
1273
|
+
}
|
|
1274
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1275
|
+
const afterState = await readChatConversationReadyState(client);
|
|
1276
|
+
return {
|
|
1277
|
+
confirmed: true,
|
|
1278
|
+
assumed_requested: Boolean(afterState.already_requested_resume),
|
|
1279
|
+
control: target,
|
|
1280
|
+
state: afterState
|
|
1281
|
+
};
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
if (!isRecoverableNodeError(error)) throw error;
|
|
1284
|
+
lastTarget = {
|
|
1285
|
+
...target,
|
|
1286
|
+
recoverable_error: error?.message || String(error),
|
|
1287
|
+
recoverable_phase: "confirm_request_resume_click"
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
await sleep(250);
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
confirmed: false,
|
|
1295
|
+
error: "CONFIRM_BUTTON_NOT_FOUND",
|
|
1296
|
+
control: lastTarget,
|
|
1297
|
+
state: lastState
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
export async function getChatResumeRequestMessageState(client) {
|
|
1302
|
+
const rootState = await getChatRoots(client);
|
|
1303
|
+
let messageRoot = null;
|
|
1304
|
+
for (const root of rootState.roots) {
|
|
1305
|
+
for (const selector of CHAT_MESSAGE_LIST_SELECTORS) {
|
|
1306
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
1307
|
+
if (nodeIds.length) {
|
|
1308
|
+
messageRoot = {
|
|
1309
|
+
root,
|
|
1310
|
+
selector,
|
|
1311
|
+
node_id: nodeIds[0]
|
|
1312
|
+
};
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (messageRoot) break;
|
|
1317
|
+
}
|
|
1318
|
+
const nodeId = messageRoot?.node_id || rootState.rootNodes.top;
|
|
1319
|
+
let text = "";
|
|
1320
|
+
try {
|
|
1321
|
+
text = htmlToText(await getOuterHTML(client, nodeId));
|
|
1322
|
+
} catch {}
|
|
1323
|
+
const lines = text.split(/\r?\n/).map(normalizeDetailText).filter(Boolean);
|
|
1324
|
+
const matching = lines.filter((line) => isResumeRequestSentMessageText(line));
|
|
1325
|
+
const attachmentMatching = lines.filter((line) => isResumeAttachmentMessageText(line));
|
|
1326
|
+
const count = countResumeRequestSentMessageMarkers(lines);
|
|
1327
|
+
const resumeAttachmentCount = countResumeAttachmentMessageMarkers(lines);
|
|
1328
|
+
return {
|
|
1329
|
+
ok: Boolean(text),
|
|
1330
|
+
selector: messageRoot?.selector || "top",
|
|
1331
|
+
count,
|
|
1332
|
+
resume_attachment_count: resumeAttachmentCount,
|
|
1333
|
+
success_count: count + resumeAttachmentCount,
|
|
1334
|
+
last_text: matching[matching.length - 1] || lines[lines.length - 1] || "",
|
|
1335
|
+
last_resume_attachment_text: attachmentMatching[attachmentMatching.length - 1] || "",
|
|
1336
|
+
last_success_text: matching[matching.length - 1] || attachmentMatching[attachmentMatching.length - 1] || "",
|
|
1337
|
+
recent: lines.slice(-12)
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
export async function waitForChatResumeRequestMessage(client, {
|
|
1342
|
+
baselineCount = 0,
|
|
1343
|
+
baselineResumeAttachmentCount = 0,
|
|
1344
|
+
timeoutMs = 6500,
|
|
1345
|
+
intervalMs = 260
|
|
1346
|
+
} = {}) {
|
|
1347
|
+
const started = Date.now();
|
|
1348
|
+
let state = null;
|
|
1349
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1350
|
+
state = await getChatResumeRequestMessageState(client);
|
|
1351
|
+
const observed = (
|
|
1352
|
+
state.count > baselineCount
|
|
1353
|
+
|| state.resume_attachment_count > baselineResumeAttachmentCount
|
|
1354
|
+
);
|
|
1355
|
+
if (observed) {
|
|
1356
|
+
return {
|
|
1357
|
+
observed: true,
|
|
1358
|
+
elapsed_ms: Date.now() - started,
|
|
1359
|
+
state
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
await sleep(intervalMs);
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
observed: false,
|
|
1366
|
+
elapsed_ms: Date.now() - started,
|
|
1367
|
+
state
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
export async function requestChatResumeForPassedCandidate(client, {
|
|
1372
|
+
greetingText = "Hi同学,能麻烦发下简历吗?",
|
|
1373
|
+
maxAttempts = 3,
|
|
1374
|
+
askResumeTimeoutMs = 8000,
|
|
1375
|
+
dryRun = false
|
|
1376
|
+
} = {}) {
|
|
1377
|
+
const effectiveGreetingText = normalizeDetailText(greetingText) || "Hi同学,能麻烦发下简历吗?";
|
|
1378
|
+
const initialState = await readChatConversationReadyState(client);
|
|
1379
|
+
if (initialState.attachment_resume_enabled) {
|
|
1380
|
+
return {
|
|
1381
|
+
requested: false,
|
|
1382
|
+
skipped: true,
|
|
1383
|
+
reason: "attachment_resume_already_available",
|
|
1384
|
+
initial_state: initialState
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
if (dryRun) {
|
|
1388
|
+
return {
|
|
1389
|
+
requested: false,
|
|
1390
|
+
skipped: false,
|
|
1391
|
+
reason: "dry_run",
|
|
1392
|
+
initial_state: initialState,
|
|
1393
|
+
would_send_greeting: true,
|
|
1394
|
+
would_click_ask_resume: true
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const closeBeforeGreeting = await closeChatResumeModal(client, { attemptsLimit: 3 });
|
|
1399
|
+
if (!closeBeforeGreeting.closed) {
|
|
1400
|
+
return {
|
|
1401
|
+
requested: false,
|
|
1402
|
+
skipped: true,
|
|
1403
|
+
reason: "resume_modal_close_failed_before_request",
|
|
1404
|
+
initial_state: initialState,
|
|
1405
|
+
close_before_greeting: closeBeforeGreeting
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
const editorState = await setChatEditorMessage(client, effectiveGreetingText);
|
|
1409
|
+
if (!editorState.ok) {
|
|
1410
|
+
throw new Error("CHAT_EDITOR_MESSAGE_MISMATCH");
|
|
1411
|
+
}
|
|
1412
|
+
const sendResult = await sendChatMessage(client, effectiveGreetingText);
|
|
1413
|
+
if (!sendResult.sent) {
|
|
1414
|
+
throw new Error(`CHAT_GREETING_SEND_FAILED:${sendResult.error || sendResult.method || "unknown"}`);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
const attempts = [];
|
|
1418
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1419
|
+
const before = await getChatResumeRequestMessageState(client);
|
|
1420
|
+
const askResult = await clickChatAskResume(client, {
|
|
1421
|
+
timeoutMs: askResumeTimeoutMs
|
|
1422
|
+
});
|
|
1423
|
+
let confirmResult = {
|
|
1424
|
+
confirmed: false,
|
|
1425
|
+
assumed_requested: Boolean(askResult.already_requested),
|
|
1426
|
+
skipped: true,
|
|
1427
|
+
reason: askResult.attachment_resume_available
|
|
1428
|
+
? "attachment_resume_already_available"
|
|
1429
|
+
: askResult.request_pending
|
|
1430
|
+
? "resume_request_already_pending"
|
|
1431
|
+
: askResult.ok
|
|
1432
|
+
? "already_requested"
|
|
1433
|
+
: (askResult.error || "ask_resume_not_clicked")
|
|
1434
|
+
};
|
|
1435
|
+
if (askResult.attachment_resume_available) {
|
|
1436
|
+
attempts.push({
|
|
1437
|
+
attempt: attempt + 1,
|
|
1438
|
+
ask_result: askResult,
|
|
1439
|
+
confirm_result: confirmResult,
|
|
1440
|
+
message_before_count: before.count,
|
|
1441
|
+
message_after_count: before.count,
|
|
1442
|
+
message_observed: false,
|
|
1443
|
+
message_last_text: before.last_text || ""
|
|
1444
|
+
});
|
|
1445
|
+
return {
|
|
1446
|
+
requested: false,
|
|
1447
|
+
skipped: true,
|
|
1448
|
+
reason: "attachment_resume_already_available",
|
|
1449
|
+
initial_state: initialState,
|
|
1450
|
+
close_before_greeting: closeBeforeGreeting,
|
|
1451
|
+
greeting_sent: true,
|
|
1452
|
+
greeting_send_result: sendResult,
|
|
1453
|
+
attempts
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
if (askResult.request_pending || askResult.already_requested) {
|
|
1457
|
+
attempts.push({
|
|
1458
|
+
attempt: attempt + 1,
|
|
1459
|
+
ask_result: askResult,
|
|
1460
|
+
confirm_result: confirmResult,
|
|
1461
|
+
message_before_count: before.count,
|
|
1462
|
+
message_after_count: before.count,
|
|
1463
|
+
resume_attachment_before_count: before.resume_attachment_count || 0,
|
|
1464
|
+
resume_attachment_after_count: before.resume_attachment_count || 0,
|
|
1465
|
+
message_observed: false,
|
|
1466
|
+
message_last_text: before.last_success_text || before.last_text || ""
|
|
1467
|
+
});
|
|
1468
|
+
return {
|
|
1469
|
+
requested: false,
|
|
1470
|
+
skipped: true,
|
|
1471
|
+
reason: "resume_request_already_pending",
|
|
1472
|
+
initial_state: initialState,
|
|
1473
|
+
close_before_greeting: closeBeforeGreeting,
|
|
1474
|
+
greeting_sent: true,
|
|
1475
|
+
greeting_send_result: sendResult,
|
|
1476
|
+
attempts
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
if (askResult.ok && !askResult.already_requested) {
|
|
1480
|
+
confirmResult = await clickChatConfirmRequestResume(client);
|
|
1481
|
+
}
|
|
1482
|
+
const messageCheck = await waitForChatResumeRequestMessage(client, {
|
|
1483
|
+
baselineCount: before.count,
|
|
1484
|
+
baselineResumeAttachmentCount: before.resume_attachment_count
|
|
1485
|
+
});
|
|
1486
|
+
const messageObserved = Boolean(messageCheck.observed);
|
|
1487
|
+
attempts.push({
|
|
1488
|
+
attempt: attempt + 1,
|
|
1489
|
+
ask_result: askResult,
|
|
1490
|
+
confirm_result: confirmResult,
|
|
1491
|
+
message_before_count: before.count,
|
|
1492
|
+
message_after_count: messageCheck.state?.count || 0,
|
|
1493
|
+
resume_attachment_before_count: before.resume_attachment_count || 0,
|
|
1494
|
+
resume_attachment_after_count: messageCheck.state?.resume_attachment_count || 0,
|
|
1495
|
+
message_observed: messageObserved,
|
|
1496
|
+
message_last_text: messageCheck.state?.last_success_text || messageCheck.state?.last_text || ""
|
|
1497
|
+
});
|
|
1498
|
+
if (messageObserved) {
|
|
1499
|
+
return {
|
|
1500
|
+
requested: true,
|
|
1501
|
+
skipped: false,
|
|
1502
|
+
reason: "requested",
|
|
1503
|
+
initial_state: initialState,
|
|
1504
|
+
close_before_greeting: closeBeforeGreeting,
|
|
1505
|
+
greeting_sent: true,
|
|
1506
|
+
greeting_send_result: sendResult,
|
|
1507
|
+
attempts
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
await sleep(900);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
return {
|
|
1514
|
+
requested: false,
|
|
1515
|
+
skipped: false,
|
|
1516
|
+
reason: "resume_request_message_not_observed",
|
|
1517
|
+
initial_state: initialState,
|
|
1518
|
+
close_before_greeting: closeBeforeGreeting,
|
|
1519
|
+
greeting_sent: true,
|
|
1520
|
+
greeting_send_result: sendResult,
|
|
1521
|
+
attempts
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
export async function closeChatResumeModal(client, {
|
|
1526
|
+
attemptsLimit = 3
|
|
1527
|
+
} = {}) {
|
|
1528
|
+
const attempts = [];
|
|
1529
|
+
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
1530
|
+
const existingState = await waitForChatResumeModal(client, { timeoutMs: 500 });
|
|
1531
|
+
if (!existingState?.popup && !existingState?.content && !existingState?.resumeIframe) {
|
|
1532
|
+
return {
|
|
1533
|
+
closed: true,
|
|
1534
|
+
attempts
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const rootState = await getChatRoots(client);
|
|
1539
|
+
const closeTarget = await findVisibleTarget(client, rootState.roots, CHAT_RESUME_CLOSE_SELECTORS);
|
|
1540
|
+
if (closeTarget) {
|
|
1541
|
+
try {
|
|
1542
|
+
await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
1543
|
+
attempts.push({
|
|
1544
|
+
mode: "close-selector",
|
|
1545
|
+
selector: closeTarget.selector,
|
|
1546
|
+
root: closeTarget.root
|
|
1547
|
+
});
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
attempts.push({
|
|
1550
|
+
mode: "close-selector-error",
|
|
1551
|
+
selector: closeTarget.selector,
|
|
1552
|
+
root: closeTarget.root,
|
|
1553
|
+
error: error?.message || String(error)
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
await sleep(700);
|
|
1557
|
+
} else {
|
|
1558
|
+
await pressEscape(client);
|
|
1559
|
+
attempts.push({ mode: "Escape" });
|
|
1560
|
+
await sleep(700);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
let state = await waitForChatResumeModal(client, { timeoutMs: 1000 });
|
|
1564
|
+
if (!state?.popup && !state?.content && !state?.resumeIframe) {
|
|
1565
|
+
return {
|
|
1566
|
+
closed: true,
|
|
1567
|
+
attempts
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
await pressEscape(client);
|
|
1572
|
+
attempts.push({ mode: "Escape-fallback" });
|
|
1573
|
+
await sleep(700);
|
|
1574
|
+
|
|
1575
|
+
state = await waitForChatResumeModal(client, { timeoutMs: 1000 });
|
|
1576
|
+
if (!state?.popup && !state?.content && !state?.resumeIframe) {
|
|
1577
|
+
return {
|
|
1578
|
+
closed: true,
|
|
1579
|
+
attempts
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
return {
|
|
1585
|
+
closed: false,
|
|
1586
|
+
attempts
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
export async function extractChatProfileCandidate(client, {
|
|
1591
|
+
cardCandidate,
|
|
1592
|
+
cardNodeId,
|
|
1593
|
+
resumeState,
|
|
1594
|
+
resumeHtml: providedResumeHtml = null,
|
|
1595
|
+
networkEvents = [],
|
|
1596
|
+
targetUrl = "",
|
|
1597
|
+
closeResume = true,
|
|
1598
|
+
networkParseRetryMs = 1800,
|
|
1599
|
+
networkParseIntervalMs = 250
|
|
1600
|
+
} = {}) {
|
|
1601
|
+
let resumeHtml = providedResumeHtml || null;
|
|
1602
|
+
if (!resumeHtml) {
|
|
1603
|
+
try {
|
|
1604
|
+
resumeHtml = await readChatResumeHtml(client, resumeState);
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
if (!networkEvents.length) throw error;
|
|
1607
|
+
resumeHtml = emptyChatResumeHtml(error);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const detailText = [
|
|
1611
|
+
resumeHtml.popupText,
|
|
1612
|
+
resumeHtml.contentText,
|
|
1613
|
+
resumeHtml.resumeIframeText
|
|
1614
|
+
].filter(Boolean).join("\n\n");
|
|
1615
|
+
|
|
1616
|
+
const parseStarted = Date.now();
|
|
1617
|
+
let networkBodies = [];
|
|
1618
|
+
let detailCandidateResult = null;
|
|
1619
|
+
do {
|
|
1620
|
+
networkBodies = await readChatProfileNetworkBodies(client, networkEvents);
|
|
1621
|
+
detailCandidateResult = buildScreeningCandidateFromDetail({
|
|
1622
|
+
domain: "chat",
|
|
1623
|
+
source: "chat-live-cdp-profile",
|
|
1624
|
+
cardCandidate,
|
|
1625
|
+
detailText,
|
|
1626
|
+
networkBodies,
|
|
1627
|
+
metadata: {
|
|
1628
|
+
target_url: targetUrl,
|
|
1629
|
+
card_node_id: cardNodeId,
|
|
1630
|
+
resume_popup_selector: resumeState?.popup?.selector || null,
|
|
1631
|
+
resume_content_selector: resumeState?.content?.selector || null,
|
|
1632
|
+
resume_iframe_selector: resumeState?.resumeIframe?.selector || null,
|
|
1633
|
+
resume_iframe_document_node_id: resumeHtml.resumeIframeDocumentNodeId
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
|
|
1637
|
+
if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
|
|
1638
|
+
await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
|
|
1639
|
+
} while (true);
|
|
1640
|
+
|
|
1641
|
+
let closeResult = null;
|
|
1642
|
+
if (closeResume) {
|
|
1643
|
+
closeResult = await closeChatResumeModal(client);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
return {
|
|
1647
|
+
candidate: detailCandidateResult.candidate,
|
|
1648
|
+
parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
|
|
1649
|
+
network_bodies: networkBodies,
|
|
1650
|
+
network_parse_retry_elapsed_ms: Date.now() - parseStarted,
|
|
1651
|
+
network_event_count: networkEvents.length,
|
|
1652
|
+
detail: {
|
|
1653
|
+
popup_text: resumeHtml.popupText,
|
|
1654
|
+
content_text: resumeHtml.contentText,
|
|
1655
|
+
resume_iframe_text: resumeHtml.resumeIframeText,
|
|
1656
|
+
popup_html_length: resumeHtml.popupHTML.length,
|
|
1657
|
+
content_html_length: resumeHtml.contentHTML.length,
|
|
1658
|
+
resume_iframe_html_length: resumeHtml.resumeIframeHTML.length
|
|
1659
|
+
},
|
|
1660
|
+
resume_html_read_error: resumeHtml.readError || null,
|
|
1661
|
+
close_result: closeResult
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
async function findVisibleTarget(client, roots, selectors) {
|
|
1666
|
+
let fallback = null;
|
|
1667
|
+
for (const root of roots) {
|
|
1668
|
+
if (!root?.nodeId) continue;
|
|
1669
|
+
for (const selector of selectors) {
|
|
1670
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
1671
|
+
for (const nodeId of nodeIds) {
|
|
1672
|
+
const target = {
|
|
1673
|
+
root: root.name,
|
|
1674
|
+
root_node_id: root.nodeId,
|
|
1675
|
+
selector,
|
|
1676
|
+
node_id: nodeId
|
|
1677
|
+
};
|
|
1678
|
+
if (!fallback) fallback = target;
|
|
1679
|
+
try {
|
|
1680
|
+
const box = await getNodeBox(client, nodeId);
|
|
1681
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
1682
|
+
return {
|
|
1683
|
+
...target,
|
|
1684
|
+
center: box.center,
|
|
1685
|
+
rect: box.rect
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
} catch {}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return fallback;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
async function pressEscape(client) {
|
|
1696
|
+
await pressKey(client, "Escape", {
|
|
1697
|
+
code: "Escape",
|
|
1698
|
+
windowsVirtualKeyCode: 27,
|
|
1699
|
+
nativeVirtualKeyCode: 27
|
|
1700
|
+
});
|
|
1701
|
+
}
|