@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/bin/boss-recommend-mcp.js +4 -4
- package/package.json +6 -1
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +43 -42
- package/skills/boss-chat/SKILL.md +107 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +47 -47
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +301 -127
- package/src/chat-runtime-config.js +7 -5
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +302 -145
- package/src/core/capture/index.js +2930 -1201
- package/src/core/cv-acquisition/index.js +512 -238
- package/src/core/cv-capture-target/index.js +513 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +31 -31
- package/src/core/reporting/legacy-csv.js +12 -12
- package/src/core/run/detached-launcher.js +305 -0
- package/src/core/run/index.js +109 -55
- package/src/core/run/timing.js +33 -33
- package/src/core/run/windows-detached-worker.ps1 +106 -0
- package/src/core/screening/index.js +2400 -2135
- package/src/core/self-heal/index.js +989 -973
- package/src/core/self-heal/viewport.js +1505 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/action-journal.js +536 -0
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +1684 -401
- package/src/domains/chat/index.js +8 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +157 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1801 -760
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +1219 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +1079 -333
- package/src/domains/recommend/constants.js +92 -92
- package/src/domains/recommend/detail.js +4037 -136
- package/src/domains/recommend/filters.js +608 -590
- package/src/domains/recommend/index.js +9 -9
- package/src/domains/recommend/jobs.js +571 -542
- package/src/domains/recommend/location.js +754 -707
- package/src/domains/recommend/refresh.js +677 -392
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +4048 -1447
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1817 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +16 -1
- package/src/parser.js +1296 -1296
- package/src/recommend-mcp.js +1061 -450
- package/src/recommend-scheduler.js +75 -75
|
@@ -1,333 +1,1079 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clickNodeCenter,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const SECTION_SELECTED_TAB_SELECTOR = ".tab-hd .selected";
|
|
14
|
-
const TAB_CANDIDATE_SELECTOR = ".tab-hd span, .tab-hd div, .tab-hd
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const DETAIL_PANE_SELECTOR = ".resume-item-detail";
|
|
18
|
-
|
|
19
|
-
function normalizeText(value) {
|
|
20
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function dateOnly(value) {
|
|
24
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
25
|
-
if (Number.isNaN(date.getTime())) return null;
|
|
26
|
-
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function daysBetween(left, right) {
|
|
30
|
-
const leftDate = dateOnly(left);
|
|
31
|
-
const rightDate = dateOnly(right);
|
|
32
|
-
if (!leftDate || !rightDate) return null;
|
|
33
|
-
return Math.floor((leftDate.getTime() - rightDate.getTime()) / 86400000);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function formatLocalDate(date) {
|
|
37
|
-
const parsed = dateOnly(date);
|
|
38
|
-
if (!parsed) return null;
|
|
39
|
-
const year = parsed.getFullYear();
|
|
40
|
-
const month = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
41
|
-
const day = String(parsed.getDate()).padStart(2, "0");
|
|
42
|
-
return `${year}-${month}-${day}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function makeDate(year, month, day) {
|
|
46
|
-
const parsed = new Date(year, month - 1, day);
|
|
47
|
-
if (
|
|
48
|
-
parsed.getFullYear() !== year
|
|
49
|
-
|| parsed.getMonth() !== month - 1
|
|
50
|
-
|| parsed.getDate() !== day
|
|
51
|
-
) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
return parsed;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function parseColleagueContactDate(text, {
|
|
58
|
-
referenceDate = new Date()
|
|
59
|
-
} = {}) {
|
|
60
|
-
const raw = normalizeText(text);
|
|
61
|
-
if (!raw) return null;
|
|
62
|
-
const today = dateOnly(referenceDate) || dateOnly(new Date());
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
date
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
date
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
date
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Number.parseInt(full[
|
|
89
|
-
Number.parseInt(full[
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Number.parseInt(partial[
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Number.parseInt(partial[
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
1
|
+
import {
|
|
2
|
+
clickNodeCenter,
|
|
3
|
+
describeNode,
|
|
4
|
+
getFrameDocumentNodeId,
|
|
5
|
+
getNodeBox,
|
|
6
|
+
getOuterHTML,
|
|
7
|
+
querySelectorAll,
|
|
8
|
+
sleep
|
|
9
|
+
} from "../../core/browser/index.js";
|
|
10
|
+
import { htmlToText } from "../../core/screening/index.js";
|
|
11
|
+
|
|
12
|
+
const COLLEAGUE_SECTION_SELECTOR = ".colleague-collaboration";
|
|
13
|
+
const SECTION_SELECTED_TAB_SELECTOR = ".tab-hd .selected";
|
|
14
|
+
const TAB_CANDIDATE_SELECTOR = ".tab-hd > span, .tab-hd > div, .tab-hd > button, .tab-hd > li";
|
|
15
|
+
const SECTION_ROW_SELECTOR = ".record-item.mate-log-item";
|
|
16
|
+
const SECTION_ROW_CONTENT_SELECTOR = ".record-item.mate-log-item .content";
|
|
17
|
+
const DETAIL_PANE_SELECTOR = ".resume-item-detail";
|
|
18
|
+
|
|
19
|
+
function normalizeText(value) {
|
|
20
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function dateOnly(value) {
|
|
24
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
25
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
26
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function daysBetween(left, right) {
|
|
30
|
+
const leftDate = dateOnly(left);
|
|
31
|
+
const rightDate = dateOnly(right);
|
|
32
|
+
if (!leftDate || !rightDate) return null;
|
|
33
|
+
return Math.floor((leftDate.getTime() - rightDate.getTime()) / 86400000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatLocalDate(date) {
|
|
37
|
+
const parsed = dateOnly(date);
|
|
38
|
+
if (!parsed) return null;
|
|
39
|
+
const year = parsed.getFullYear();
|
|
40
|
+
const month = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
41
|
+
const day = String(parsed.getDate()).padStart(2, "0");
|
|
42
|
+
return `${year}-${month}-${day}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeDate(year, month, day) {
|
|
46
|
+
const parsed = new Date(year, month - 1, day);
|
|
47
|
+
if (
|
|
48
|
+
parsed.getFullYear() !== year
|
|
49
|
+
|| parsed.getMonth() !== month - 1
|
|
50
|
+
|| parsed.getDate() !== day
|
|
51
|
+
) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseColleagueContactDate(text, {
|
|
58
|
+
referenceDate = new Date()
|
|
59
|
+
} = {}) {
|
|
60
|
+
const raw = normalizeText(text);
|
|
61
|
+
if (!raw) return null;
|
|
62
|
+
const today = dateOnly(referenceDate) || dateOnly(new Date());
|
|
63
|
+
if (/(?:刚刚|刚才)/.test(raw)) return today;
|
|
64
|
+
const relativeDays = raw.match(/(\d+)\s*天前/);
|
|
65
|
+
if (relativeDays) {
|
|
66
|
+
const days = Number.parseInt(relativeDays[1], 10);
|
|
67
|
+
if (Number.isFinite(days) && days >= 0) {
|
|
68
|
+
const date = new Date(today);
|
|
69
|
+
date.setDate(date.getDate() - days);
|
|
70
|
+
return date;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (/今天/.test(raw)) return today;
|
|
74
|
+
if (/昨天/.test(raw)) {
|
|
75
|
+
const date = new Date(today);
|
|
76
|
+
date.setDate(date.getDate() - 1);
|
|
77
|
+
return date;
|
|
78
|
+
}
|
|
79
|
+
if (/前天/.test(raw)) {
|
|
80
|
+
const date = new Date(today);
|
|
81
|
+
date.setDate(date.getDate() - 2);
|
|
82
|
+
return date;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const full = raw.match(/(20\d{2})[.\-\/](\d{1,2})[.\-\/](\d{1,2})/);
|
|
86
|
+
if (full) {
|
|
87
|
+
return makeDate(
|
|
88
|
+
Number.parseInt(full[1], 10),
|
|
89
|
+
Number.parseInt(full[2], 10),
|
|
90
|
+
Number.parseInt(full[3], 10)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const partial = raw.match(/(?:^|\D)(\d{1,2})[.\-\/](\d{1,2})(?:\D|$)/);
|
|
95
|
+
if (partial) {
|
|
96
|
+
const reference = dateOnly(referenceDate) || new Date();
|
|
97
|
+
let date = makeDate(
|
|
98
|
+
reference.getFullYear(),
|
|
99
|
+
Number.parseInt(partial[1], 10),
|
|
100
|
+
Number.parseInt(partial[2], 10)
|
|
101
|
+
);
|
|
102
|
+
if (date && daysBetween(date, reference) > 7) {
|
|
103
|
+
date = makeDate(
|
|
104
|
+
reference.getFullYear() - 1,
|
|
105
|
+
Number.parseInt(partial[1], 10),
|
|
106
|
+
Number.parseInt(partial[2], 10)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return date;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isDateWithinWindow(date, {
|
|
116
|
+
referenceDate = new Date(),
|
|
117
|
+
windowDays = 14
|
|
118
|
+
} = {}) {
|
|
119
|
+
const diff = daysBetween(referenceDate, date);
|
|
120
|
+
return Number.isFinite(diff) && diff >= 0 && diff <= windowDays;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function textForNode(client, nodeId) {
|
|
124
|
+
return htmlToText(await getOuterHTML(client, nodeId));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function queryAcrossRoots(client, roots, selector) {
|
|
128
|
+
const matches = [];
|
|
129
|
+
for (const root of roots || []) {
|
|
130
|
+
if (!root?.nodeId) continue;
|
|
131
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
132
|
+
for (const nodeId of nodeIds) {
|
|
133
|
+
matches.push({
|
|
134
|
+
root: root.name,
|
|
135
|
+
root_node_id: root.nodeId,
|
|
136
|
+
selector,
|
|
137
|
+
node_id: nodeId
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return matches;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function tabIsColleague(text) {
|
|
145
|
+
return normalizeText(text) === "同事沟通进度";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isUsableVisibleRowBox(box) {
|
|
149
|
+
return Number(box?.rect?.width || 0) > 2 && Number(box?.rect?.height || 0) > 2;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function readVisibleNodeEvidence(client, nodeId) {
|
|
153
|
+
let box = null;
|
|
154
|
+
try {
|
|
155
|
+
box = await getNodeBox(client, nodeId);
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (!isUsableVisibleRowBox(box)) return null;
|
|
160
|
+
const node = await describeNode(client, nodeId, { depth: 0, pierce: true });
|
|
161
|
+
if (!Number.isInteger(node?.backendNodeId) || node.backendNodeId <= 0) return null;
|
|
162
|
+
return {
|
|
163
|
+
node_id: nodeId,
|
|
164
|
+
backend_node_id: node.backendNodeId,
|
|
165
|
+
box: {
|
|
166
|
+
x: box.rect.x,
|
|
167
|
+
y: box.rect.y,
|
|
168
|
+
width: box.rect.width,
|
|
169
|
+
height: box.rect.height
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function readSelectedColleagueTab(client, sectionNodeId) {
|
|
175
|
+
const selectedIds = await querySelectorAll(client, sectionNodeId, SECTION_SELECTED_TAB_SELECTOR);
|
|
176
|
+
const visibleSelected = [];
|
|
177
|
+
for (const nodeId of selectedIds) {
|
|
178
|
+
const evidence = await readVisibleNodeEvidence(client, nodeId);
|
|
179
|
+
if (!evidence) continue;
|
|
180
|
+
visibleSelected.push({
|
|
181
|
+
...evidence,
|
|
182
|
+
text: normalizeText(await textForNode(client, nodeId))
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const selected = visibleSelected.length === 1 && tabIsColleague(visibleSelected[0].text);
|
|
186
|
+
return {
|
|
187
|
+
selected,
|
|
188
|
+
selected_text: visibleSelected[0]?.text || "",
|
|
189
|
+
selected_tab_count: visibleSelected.length,
|
|
190
|
+
node_id: visibleSelected[0]?.node_id || null,
|
|
191
|
+
backend_node_id: visibleSelected[0]?.backend_node_id || null,
|
|
192
|
+
box: visibleSelected[0]?.box || null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function ensureColleagueTabSelected(client, sectionNodeId) {
|
|
197
|
+
const before = await readSelectedColleagueTab(client, sectionNodeId);
|
|
198
|
+
if (before.selected) {
|
|
199
|
+
return {
|
|
200
|
+
...before,
|
|
201
|
+
changed: false
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const candidateIds = await querySelectorAll(client, sectionNodeId, TAB_CANDIDATE_SELECTOR);
|
|
206
|
+
for (const nodeId of candidateIds) {
|
|
207
|
+
const evidence = await readVisibleNodeEvidence(client, nodeId);
|
|
208
|
+
if (!evidence) continue;
|
|
209
|
+
const text = normalizeText(await textForNode(client, nodeId));
|
|
210
|
+
if (!tabIsColleague(text)) continue;
|
|
211
|
+
const box = await clickNodeCenter(client, nodeId, { scrollIntoView: true });
|
|
212
|
+
await sleep(500);
|
|
213
|
+
const after = await readSelectedColleagueTab(client, sectionNodeId);
|
|
214
|
+
return {
|
|
215
|
+
...after,
|
|
216
|
+
changed: true,
|
|
217
|
+
click_box: {
|
|
218
|
+
rect: box.rect,
|
|
219
|
+
center: box.center
|
|
220
|
+
},
|
|
221
|
+
reason: after.selected ? null : "colleague_tab_selection_not_verified"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
...before,
|
|
227
|
+
changed: false,
|
|
228
|
+
reason: "colleague_tab_candidate_not_visible"
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function readContactRows(client, sectionNodeId, sectionBackendNodeId, sectionRoot = "") {
|
|
233
|
+
const contentNodeIds = await querySelectorAll(client, sectionNodeId, SECTION_ROW_CONTENT_SELECTOR);
|
|
234
|
+
const fallbackNodeIds = contentNodeIds.length
|
|
235
|
+
? []
|
|
236
|
+
: await querySelectorAll(client, sectionNodeId, SECTION_ROW_SELECTOR);
|
|
237
|
+
const nodeIds = contentNodeIds.length ? contentNodeIds : fallbackNodeIds;
|
|
238
|
+
const rows = [];
|
|
239
|
+
const unreadableRows = [];
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
for (const nodeId of nodeIds) {
|
|
242
|
+
let box = null;
|
|
243
|
+
try {
|
|
244
|
+
box = await getNodeBox(client, nodeId);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
unreadableRows.push({
|
|
247
|
+
node_id: nodeId,
|
|
248
|
+
stage: "box",
|
|
249
|
+
error: error?.message || String(error)
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!isUsableVisibleRowBox(box)) {
|
|
254
|
+
unreadableRows.push({
|
|
255
|
+
node_id: nodeId,
|
|
256
|
+
stage: "visible_box"
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const node = await describeNode(client, nodeId, { depth: 0, pierce: true });
|
|
261
|
+
if (!Number.isInteger(node?.backendNodeId) || node.backendNodeId <= 0) {
|
|
262
|
+
unreadableRows.push({
|
|
263
|
+
node_id: nodeId,
|
|
264
|
+
stage: "backend_identity"
|
|
265
|
+
});
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const text = normalizeText(await textForNode(client, nodeId));
|
|
269
|
+
if (!text) {
|
|
270
|
+
unreadableRows.push({
|
|
271
|
+
node_id: nodeId,
|
|
272
|
+
backend_node_id: node.backendNodeId,
|
|
273
|
+
stage: "text"
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (seen.has(text)) continue;
|
|
278
|
+
seen.add(text);
|
|
279
|
+
rows.push({
|
|
280
|
+
text,
|
|
281
|
+
root: sectionRoot,
|
|
282
|
+
selector: contentNodeIds.length ? SECTION_ROW_CONTENT_SELECTOR : SECTION_ROW_SELECTOR,
|
|
283
|
+
node_id: nodeId,
|
|
284
|
+
backend_node_id: node.backendNodeId,
|
|
285
|
+
section_node_id: sectionNodeId,
|
|
286
|
+
section_backend_node_id: sectionBackendNodeId,
|
|
287
|
+
visible: true,
|
|
288
|
+
box: {
|
|
289
|
+
x: box.rect.x,
|
|
290
|
+
y: box.rect.y,
|
|
291
|
+
width: box.rect.width,
|
|
292
|
+
height: box.rect.height
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
rows,
|
|
298
|
+
unreadable_rows: unreadableRows
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function paneBindingMatches(sectionEvidence, selectedEvidence, section, tab) {
|
|
303
|
+
return Boolean(
|
|
304
|
+
sectionEvidence
|
|
305
|
+
&& sectionEvidence.backend_node_id === section.backend_node_id
|
|
306
|
+
&& selectedEvidence?.selected === true
|
|
307
|
+
&& selectedEvidence.selected_tab_count === 1
|
|
308
|
+
&& selectedEvidence.node_id === tab.node_id
|
|
309
|
+
&& selectedEvidence.backend_node_id === tab.backend_node_id
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function readPaneBindingAtPosition(client, section, tab) {
|
|
314
|
+
const sectionEvidence = await readVisibleNodeEvidence(client, section.node_id);
|
|
315
|
+
const selectedEvidence = await readSelectedColleagueTab(client, section.node_id);
|
|
316
|
+
return {
|
|
317
|
+
verified: paneBindingMatches(sectionEvidence, selectedEvidence, section, tab),
|
|
318
|
+
section_visible: Boolean(sectionEvidence),
|
|
319
|
+
section_node_id: section.node_id,
|
|
320
|
+
section_backend_node_id: sectionEvidence?.backend_node_id || null,
|
|
321
|
+
selected_tab_node_id: selectedEvidence.node_id || null,
|
|
322
|
+
selected_tab_backend_node_id: selectedEvidence.backend_node_id || null,
|
|
323
|
+
selected_tab_text: selectedEvidence.selected_text,
|
|
324
|
+
selected_tab_count: selectedEvidence.selected_tab_count
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function resolveBoundScrollTarget(client, scopes, section) {
|
|
329
|
+
const sectionScope = (scopes || []).find((scope) => (
|
|
330
|
+
scope?.name === section.root
|
|
331
|
+
&& scope?.nodeId === section.root_node_id
|
|
332
|
+
));
|
|
333
|
+
const detailPanes = sectionScope
|
|
334
|
+
? await queryAcrossRoots(client, [sectionScope], DETAIL_PANE_SELECTOR)
|
|
335
|
+
: [];
|
|
336
|
+
const visiblePanes = [];
|
|
337
|
+
for (const pane of detailPanes) {
|
|
338
|
+
const evidence = await readVisibleNodeEvidence(client, pane.node_id);
|
|
339
|
+
if (!evidence) continue;
|
|
340
|
+
visiblePanes.push({
|
|
341
|
+
...pane,
|
|
342
|
+
...evidence
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (visiblePanes.length > 1) {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
reason: "scroll_target_ambiguous",
|
|
349
|
+
visible_target_count: visiblePanes.length
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (visiblePanes.length === 1) {
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
node_id: visiblePanes[0].node_id,
|
|
356
|
+
backend_node_id: visiblePanes[0].backend_node_id,
|
|
357
|
+
selector: visiblePanes[0].selector,
|
|
358
|
+
box: visiblePanes[0].box
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const sectionEvidence = await readVisibleNodeEvidence(client, section.node_id);
|
|
362
|
+
if (!sectionEvidence || sectionEvidence.backend_node_id !== section.backend_node_id) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
reason: "scroll_target_box_unavailable",
|
|
366
|
+
visible_target_count: 0
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
node_id: section.node_id,
|
|
372
|
+
backend_node_id: section.backend_node_id,
|
|
373
|
+
selector: COLLEAGUE_SECTION_SELECTOR,
|
|
374
|
+
box: sectionEvidence.box
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function addRowsFromPosition(byText, positionRows, positionIndex) {
|
|
379
|
+
for (const row of positionRows) {
|
|
380
|
+
const existing = byText.get(row.text);
|
|
381
|
+
if (existing) {
|
|
382
|
+
if (!existing.observed_at_positions.includes(positionIndex)) {
|
|
383
|
+
existing.observed_at_positions.push(positionIndex);
|
|
384
|
+
}
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
byText.set(row.text, {
|
|
388
|
+
...row,
|
|
389
|
+
observed_at_positions: [positionIndex]
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function scanContactRowsAcrossScrollPositions(client, scopes, section, tab, {
|
|
395
|
+
enabled = true,
|
|
396
|
+
maxScrolls = 24,
|
|
397
|
+
settleMs = 350
|
|
398
|
+
} = {}) {
|
|
399
|
+
const stableEndSamplesRequired = 2;
|
|
400
|
+
const boundedMaxScrolls = enabled
|
|
401
|
+
? Math.max(
|
|
402
|
+
stableEndSamplesRequired,
|
|
403
|
+
Math.min(48, Number.isFinite(Number(maxScrolls)) ? Math.floor(Number(maxScrolls)) : 24)
|
|
404
|
+
)
|
|
405
|
+
: 0;
|
|
406
|
+
const scrollTarget = enabled
|
|
407
|
+
? await resolveBoundScrollTarget(client, scopes, section)
|
|
408
|
+
: null;
|
|
409
|
+
if (enabled && !scrollTarget?.ok) {
|
|
410
|
+
return {
|
|
411
|
+
completed: false,
|
|
412
|
+
coverage_verified: false,
|
|
413
|
+
reason: scrollTarget?.reason || "scroll_target_box_unavailable",
|
|
414
|
+
scrolls_requested: boundedMaxScrolls,
|
|
415
|
+
scrolls_completed: 0,
|
|
416
|
+
position_count: 0,
|
|
417
|
+
positions: [],
|
|
418
|
+
rows: []
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const rowsByText = new Map();
|
|
423
|
+
const positions = [];
|
|
424
|
+
let scrollsCompleted = 0;
|
|
425
|
+
let previousRowSignature = null;
|
|
426
|
+
let previousRowLayoutSignature = null;
|
|
427
|
+
let stableSignatureCount = 0;
|
|
428
|
+
let effectiveScrollCount = 0;
|
|
429
|
+
let endProof = null;
|
|
430
|
+
const targetHeight = Number(scrollTarget?.box?.height || 0);
|
|
431
|
+
const stepDeltaY = enabled
|
|
432
|
+
? Math.max(1, Math.min(480, Math.floor(targetHeight * 0.65)))
|
|
433
|
+
: 0;
|
|
434
|
+
|
|
435
|
+
for (let positionIndex = 0; positionIndex <= boundedMaxScrolls; positionIndex += 1) {
|
|
436
|
+
const bindingBefore = await readPaneBindingAtPosition(client, section, tab);
|
|
437
|
+
if (!bindingBefore.verified) {
|
|
438
|
+
return {
|
|
439
|
+
completed: false,
|
|
440
|
+
coverage_verified: false,
|
|
441
|
+
reason: "colleague_binding_lost",
|
|
442
|
+
scrolls_requested: boundedMaxScrolls,
|
|
443
|
+
scrolls_completed: scrollsCompleted,
|
|
444
|
+
position_count: positions.length,
|
|
445
|
+
positions,
|
|
446
|
+
rows: [],
|
|
447
|
+
failed_position: positionIndex,
|
|
448
|
+
failed_binding_phase: "before_rows",
|
|
449
|
+
binding: bindingBefore
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const positionRead = await readContactRows(
|
|
454
|
+
client,
|
|
455
|
+
section.node_id,
|
|
456
|
+
section.backend_node_id,
|
|
457
|
+
section.root
|
|
458
|
+
);
|
|
459
|
+
const positionRows = positionRead.rows;
|
|
460
|
+
const bindingAfter = await readPaneBindingAtPosition(client, section, tab);
|
|
461
|
+
const rowIdentityKeys = positionRows
|
|
462
|
+
.map((row) => `${row.backend_node_id}:${row.text}`)
|
|
463
|
+
.sort();
|
|
464
|
+
const rowTexts = positionRows.map((row) => row.text).sort();
|
|
465
|
+
const rowSignature = JSON.stringify(rowIdentityKeys);
|
|
466
|
+
const orderedRows = positionRows
|
|
467
|
+
.map((row) => ({
|
|
468
|
+
backend_node_id: row.backend_node_id,
|
|
469
|
+
text: row.text,
|
|
470
|
+
x: Math.round(Number(row?.box?.x || 0)),
|
|
471
|
+
y: Math.round(Number(row?.box?.y || 0)),
|
|
472
|
+
width: Math.round(Number(row?.box?.width || 0)),
|
|
473
|
+
height: Math.round(Number(row?.box?.height || 0))
|
|
474
|
+
}))
|
|
475
|
+
.sort((left, right) => (
|
|
476
|
+
left.y - right.y
|
|
477
|
+
|| left.x - right.x
|
|
478
|
+
|| left.backend_node_id - right.backend_node_id
|
|
479
|
+
));
|
|
480
|
+
const rowLayoutKeys = orderedRows.map((row) => (
|
|
481
|
+
`${row.text}:${row.x}:${row.y}:${row.width}:${row.height}`
|
|
482
|
+
));
|
|
483
|
+
const rowLayoutSignature = JSON.stringify(rowLayoutKeys);
|
|
484
|
+
const newRowTexts = rowTexts.filter((text) => !rowsByText.has(text));
|
|
485
|
+
const scrollEffectObserved = Boolean(
|
|
486
|
+
scrollsCompleted > 0
|
|
487
|
+
&& rowLayoutSignature !== previousRowLayoutSignature
|
|
488
|
+
);
|
|
489
|
+
if (scrollEffectObserved) effectiveScrollCount += 1;
|
|
490
|
+
if (
|
|
491
|
+
scrollsCompleted > 0
|
|
492
|
+
&& rowSignature === previousRowSignature
|
|
493
|
+
&& newRowTexts.length === 0
|
|
494
|
+
) {
|
|
495
|
+
stableSignatureCount += 1;
|
|
496
|
+
} else {
|
|
497
|
+
stableSignatureCount = 0;
|
|
498
|
+
}
|
|
499
|
+
const positionEvidence = {
|
|
500
|
+
position_index: positionIndex,
|
|
501
|
+
sampled_after_scroll_count: scrollsCompleted,
|
|
502
|
+
row_count: positionRows.length,
|
|
503
|
+
unreadable_row_count: positionRead.unreadable_rows.length,
|
|
504
|
+
row_backend_node_ids: positionRows.map((row) => row.backend_node_id),
|
|
505
|
+
row_texts: rowTexts,
|
|
506
|
+
row_identity_keys: rowIdentityKeys,
|
|
507
|
+
row_signature: rowSignature,
|
|
508
|
+
ordered_row_layout: orderedRows,
|
|
509
|
+
ordered_row_layout_keys: rowLayoutKeys,
|
|
510
|
+
row_layout_signature: rowLayoutSignature,
|
|
511
|
+
scroll_effect_observed: scrollEffectObserved,
|
|
512
|
+
cumulative_effective_scroll_count: effectiveScrollCount,
|
|
513
|
+
new_row_count: newRowTexts.length,
|
|
514
|
+
new_row_texts: newRowTexts,
|
|
515
|
+
stable_signature_count: stableSignatureCount,
|
|
516
|
+
binding_before_verified: bindingBefore.verified,
|
|
517
|
+
binding_after_verified: bindingAfter.verified
|
|
518
|
+
};
|
|
519
|
+
positions.push(positionEvidence);
|
|
520
|
+
if (!bindingAfter.verified) {
|
|
521
|
+
return {
|
|
522
|
+
completed: false,
|
|
523
|
+
coverage_verified: false,
|
|
524
|
+
reason: "colleague_binding_lost",
|
|
525
|
+
scrolls_requested: boundedMaxScrolls,
|
|
526
|
+
scrolls_completed: scrollsCompleted,
|
|
527
|
+
position_count: positions.length,
|
|
528
|
+
positions,
|
|
529
|
+
rows: [],
|
|
530
|
+
failed_position: positionIndex,
|
|
531
|
+
failed_binding_phase: "after_rows",
|
|
532
|
+
binding: bindingAfter
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (positionRead.unreadable_rows.length) {
|
|
536
|
+
return {
|
|
537
|
+
completed: false,
|
|
538
|
+
coverage_verified: false,
|
|
539
|
+
reason: "colleague_row_evidence_unavailable",
|
|
540
|
+
scrolls_requested: boundedMaxScrolls,
|
|
541
|
+
scrolls_completed: scrollsCompleted,
|
|
542
|
+
position_count: positions.length,
|
|
543
|
+
positions,
|
|
544
|
+
rows: [],
|
|
545
|
+
failed_position: positionIndex,
|
|
546
|
+
unreadable_rows: positionRead.unreadable_rows
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
addRowsFromPosition(rowsByText, positionRows, positionIndex);
|
|
550
|
+
previousRowSignature = rowSignature;
|
|
551
|
+
previousRowLayoutSignature = rowLayoutSignature;
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
enabled
|
|
555
|
+
&& stableSignatureCount >= stableEndSamplesRequired
|
|
556
|
+
&& effectiveScrollCount > 0
|
|
557
|
+
) {
|
|
558
|
+
endProof = {
|
|
559
|
+
verified: true,
|
|
560
|
+
method: "effective_scroll_then_repeated_identical_rows",
|
|
561
|
+
stable_samples_required: stableEndSamplesRequired,
|
|
562
|
+
stable_samples_observed: stableSignatureCount,
|
|
563
|
+
effective_scroll_observed: true,
|
|
564
|
+
effective_scroll_count: effectiveScrollCount,
|
|
565
|
+
end_position_index: positionIndex,
|
|
566
|
+
end_scroll_count: scrollsCompleted,
|
|
567
|
+
row_signature: rowSignature,
|
|
568
|
+
additional_wheel_attempts_without_change: stableSignatureCount
|
|
569
|
+
};
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (positionIndex === boundedMaxScrolls) break;
|
|
574
|
+
|
|
575
|
+
const freshTarget = await readVisibleNodeEvidence(client, scrollTarget.node_id);
|
|
576
|
+
if (!freshTarget || freshTarget.backend_node_id !== scrollTarget.backend_node_id) {
|
|
577
|
+
return {
|
|
578
|
+
completed: false,
|
|
579
|
+
coverage_verified: false,
|
|
580
|
+
reason: "scroll_target_binding_lost",
|
|
581
|
+
scrolls_requested: boundedMaxScrolls,
|
|
582
|
+
scrolls_completed: scrollsCompleted,
|
|
583
|
+
position_count: positions.length,
|
|
584
|
+
positions,
|
|
585
|
+
rows: [],
|
|
586
|
+
failed_position: positionIndex,
|
|
587
|
+
target_selector: scrollTarget.selector
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
await client.Input.dispatchMouseEvent({
|
|
592
|
+
type: "mouseWheel",
|
|
593
|
+
x: freshTarget.box.x + (freshTarget.box.width / 2),
|
|
594
|
+
y: freshTarget.box.y + (freshTarget.box.height / 2),
|
|
595
|
+
deltaY: stepDeltaY,
|
|
596
|
+
deltaX: 0
|
|
597
|
+
});
|
|
598
|
+
} catch (error) {
|
|
599
|
+
return {
|
|
600
|
+
completed: false,
|
|
601
|
+
coverage_verified: false,
|
|
602
|
+
reason: "scroll_dispatch_failed",
|
|
603
|
+
error: error?.message || String(error),
|
|
604
|
+
scrolls_requested: boundedMaxScrolls,
|
|
605
|
+
scrolls_completed: scrollsCompleted,
|
|
606
|
+
position_count: positions.length,
|
|
607
|
+
positions,
|
|
608
|
+
rows: [],
|
|
609
|
+
failed_position: positionIndex,
|
|
610
|
+
target_selector: scrollTarget.selector
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
scrollsCompleted += 1;
|
|
614
|
+
await sleep(Math.max(0, Number(settleMs) || 0));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const allPositionEvidenceVerified = positions.every((position) => (
|
|
618
|
+
position.binding_before_verified === true
|
|
619
|
+
&& position.binding_after_verified === true
|
|
620
|
+
));
|
|
621
|
+
const completed = enabled
|
|
622
|
+
? Boolean(endProof?.verified && allPositionEvidenceVerified)
|
|
623
|
+
: Boolean(positions.length === 1 && scrollsCompleted === 0 && allPositionEvidenceVerified);
|
|
624
|
+
const coverageVerified = Boolean(
|
|
625
|
+
enabled
|
|
626
|
+
&& completed
|
|
627
|
+
&& endProof?.verified === true
|
|
628
|
+
&& positions.every((position) => position.row_count > 0)
|
|
629
|
+
);
|
|
630
|
+
const capReachedWithoutEnd = Boolean(
|
|
631
|
+
enabled
|
|
632
|
+
&& scrollsCompleted >= boundedMaxScrolls
|
|
633
|
+
&& endProof?.verified !== true
|
|
634
|
+
);
|
|
635
|
+
return {
|
|
636
|
+
completed,
|
|
637
|
+
coverage_verified: coverageVerified,
|
|
638
|
+
reason: completed
|
|
639
|
+
? !enabled
|
|
640
|
+
? "scroll_scan_disabled"
|
|
641
|
+
: coverageVerified
|
|
642
|
+
? null
|
|
643
|
+
: "scroll_position_rows_missing"
|
|
644
|
+
: capReachedWithoutEnd
|
|
645
|
+
? "scroll_end_not_verified_before_cap"
|
|
646
|
+
: "scroll_scan_incomplete",
|
|
647
|
+
scrolls_requested: boundedMaxScrolls,
|
|
648
|
+
scrolls_completed: scrollsCompleted,
|
|
649
|
+
position_count: positions.length,
|
|
650
|
+
positions,
|
|
651
|
+
target_selector: scrollTarget?.selector || null,
|
|
652
|
+
target_node_id: scrollTarget?.node_id || null,
|
|
653
|
+
target_backend_node_id: scrollTarget?.backend_node_id || null,
|
|
654
|
+
step_delta_y: stepDeltaY,
|
|
655
|
+
overlap_ratio: 0.35,
|
|
656
|
+
effective_scroll_count: effectiveScrollCount,
|
|
657
|
+
end_proof: endProof || {
|
|
658
|
+
verified: false,
|
|
659
|
+
method: "effective_scroll_then_repeated_identical_rows",
|
|
660
|
+
stable_samples_required: stableEndSamplesRequired,
|
|
661
|
+
stable_samples_observed: stableSignatureCount,
|
|
662
|
+
effective_scroll_observed: effectiveScrollCount > 0,
|
|
663
|
+
effective_scroll_count: effectiveScrollCount,
|
|
664
|
+
end_position_index: null,
|
|
665
|
+
end_scroll_count: null,
|
|
666
|
+
row_signature: null,
|
|
667
|
+
additional_wheel_attempts_without_change: stableSignatureCount
|
|
668
|
+
},
|
|
669
|
+
cap_reached_without_end: capReachedWithoutEnd,
|
|
670
|
+
coverage_gap_positions: positions
|
|
671
|
+
.filter((position) => position.row_count <= 0)
|
|
672
|
+
.map((position) => position.position_index),
|
|
673
|
+
rows: Array.from(rowsByText.values())
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function resolveColleagueDetailScopes(client, detailState) {
|
|
678
|
+
const scopes = [];
|
|
679
|
+
if (Number.isInteger(detailState?.popup?.node_id) && detailState.popup.node_id > 0) {
|
|
680
|
+
const popupEvidence = await readVisibleNodeEvidence(client, detailState.popup.node_id);
|
|
681
|
+
if (popupEvidence) {
|
|
682
|
+
scopes.push({
|
|
683
|
+
name: "popup",
|
|
684
|
+
nodeId: detailState.popup.node_id,
|
|
685
|
+
backendNodeId: popupEvidence.backend_node_id
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (Number.isInteger(detailState?.resumeIframe?.node_id) && detailState.resumeIframe.node_id > 0) {
|
|
690
|
+
try {
|
|
691
|
+
const documentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
|
|
692
|
+
scopes.push({
|
|
693
|
+
name: "resume_iframe",
|
|
694
|
+
nodeId: documentNodeId,
|
|
695
|
+
backendNodeId: null
|
|
696
|
+
});
|
|
697
|
+
} catch {
|
|
698
|
+
// A stale or inaccessible iframe cannot be used as colleague-contact evidence.
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return scopes;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function waitForColleagueSections(client, scopes, {
|
|
705
|
+
timeoutMs = 1000,
|
|
706
|
+
intervalMs = 150
|
|
707
|
+
} = {}) {
|
|
708
|
+
const started = Date.now();
|
|
709
|
+
let sections = [];
|
|
710
|
+
let pollCount = 0;
|
|
711
|
+
let stableScopeCount = 0;
|
|
712
|
+
let scopeBindingLost = false;
|
|
713
|
+
do {
|
|
714
|
+
pollCount += 1;
|
|
715
|
+
stableScopeCount = 0;
|
|
716
|
+
for (const scope of scopes) {
|
|
717
|
+
if (!Number.isInteger(scope?.backendNodeId) || scope.backendNodeId <= 0) continue;
|
|
718
|
+
const evidence = await readVisibleNodeEvidence(client, scope.nodeId);
|
|
719
|
+
if (!evidence || evidence.backend_node_id !== scope.backendNodeId) {
|
|
720
|
+
scopeBindingLost = true;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
stableScopeCount += 1;
|
|
724
|
+
}
|
|
725
|
+
if (scopeBindingLost) break;
|
|
726
|
+
const matches = await queryAcrossRoots(client, scopes, COLLEAGUE_SECTION_SELECTOR);
|
|
727
|
+
sections = [];
|
|
728
|
+
for (const match of matches) {
|
|
729
|
+
const evidence = await readVisibleNodeEvidence(client, match.node_id);
|
|
730
|
+
if (!evidence) continue;
|
|
731
|
+
sections.push({
|
|
732
|
+
...match,
|
|
733
|
+
backend_node_id: evidence.backend_node_id,
|
|
734
|
+
visible: true,
|
|
735
|
+
box: evidence.box
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (sections.length) {
|
|
739
|
+
return {
|
|
740
|
+
sections,
|
|
741
|
+
absence_probe: {
|
|
742
|
+
verified: false,
|
|
743
|
+
selector: COLLEAGUE_SECTION_SELECTOR,
|
|
744
|
+
scope_count: scopes.length,
|
|
745
|
+
stable_scope_count: stableScopeCount,
|
|
746
|
+
poll_count: pollCount,
|
|
747
|
+
elapsed_ms: Date.now() - started,
|
|
748
|
+
timeout_ms: Math.max(0, Number(timeoutMs) || 0),
|
|
749
|
+
full_window_elapsed: false,
|
|
750
|
+
query_error_count: 0,
|
|
751
|
+
scope_binding_lost: false,
|
|
752
|
+
scope_backend_node_ids: scopes
|
|
753
|
+
.map((scope) => Number(scope?.backendNodeId))
|
|
754
|
+
.filter((backendNodeId) => Number.isInteger(backendNodeId) && backendNodeId > 0)
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
const elapsedMs = Date.now() - started;
|
|
759
|
+
const normalizedTimeoutMs = Math.max(0, Number(timeoutMs) || 0);
|
|
760
|
+
if (
|
|
761
|
+
elapsedMs >= normalizedTimeoutMs
|
|
762
|
+
&& (normalizedTimeoutMs === 0 || pollCount >= 2)
|
|
763
|
+
) break;
|
|
764
|
+
await sleep(Math.max(1, Number(intervalMs) || 0));
|
|
765
|
+
} while (true);
|
|
766
|
+
const elapsedMs = Date.now() - started;
|
|
767
|
+
const normalizedTimeoutMs = Math.max(0, Number(timeoutMs) || 0);
|
|
768
|
+
const stableBackendNodeIds = scopes
|
|
769
|
+
.map((scope) => Number(scope?.backendNodeId))
|
|
770
|
+
.filter((backendNodeId) => Number.isInteger(backendNodeId) && backendNodeId > 0);
|
|
771
|
+
return {
|
|
772
|
+
sections,
|
|
773
|
+
absence_probe: {
|
|
774
|
+
verified: Boolean(
|
|
775
|
+
!scopeBindingLost
|
|
776
|
+
&& scopes.length > 0
|
|
777
|
+
&& stableScopeCount > 0
|
|
778
|
+
&& stableBackendNodeIds.length === stableScopeCount
|
|
779
|
+
&& pollCount >= 2
|
|
780
|
+
&& elapsedMs >= normalizedTimeoutMs
|
|
781
|
+
),
|
|
782
|
+
selector: COLLEAGUE_SECTION_SELECTOR,
|
|
783
|
+
scope_count: scopes.length,
|
|
784
|
+
stable_scope_count: stableScopeCount,
|
|
785
|
+
poll_count: pollCount,
|
|
786
|
+
elapsed_ms: elapsedMs,
|
|
787
|
+
timeout_ms: normalizedTimeoutMs,
|
|
788
|
+
full_window_elapsed: elapsedMs >= normalizedTimeoutMs,
|
|
789
|
+
query_error_count: 0,
|
|
790
|
+
scope_binding_lost: scopeBindingLost,
|
|
791
|
+
scope_backend_node_ids: stableBackendNodeIds
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export async function inspectRecentColleagueContact(client, detailState, {
|
|
797
|
+
referenceDate = new Date(),
|
|
798
|
+
windowDays = 14,
|
|
799
|
+
scroll = true,
|
|
800
|
+
scrollMaxSteps = 24,
|
|
801
|
+
scrollSettleMs = 350,
|
|
802
|
+
sectionWaitMs = 1000,
|
|
803
|
+
sectionPollMs = 150
|
|
804
|
+
} = {}) {
|
|
805
|
+
const scopes = await resolveColleagueDetailScopes(client, detailState);
|
|
806
|
+
if (!scopes.length) {
|
|
807
|
+
return {
|
|
808
|
+
checked: false,
|
|
809
|
+
panel_found: false,
|
|
810
|
+
recent: null,
|
|
811
|
+
indeterminate: true,
|
|
812
|
+
reason: "detail_scope_unavailable",
|
|
813
|
+
window_days: windowDays,
|
|
814
|
+
rows: []
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
const sectionWait = await waitForColleagueSections(client, scopes, {
|
|
818
|
+
timeoutMs: sectionWaitMs,
|
|
819
|
+
intervalMs: sectionPollMs
|
|
820
|
+
});
|
|
821
|
+
const sections = sectionWait.sections;
|
|
822
|
+
if (!sections.length) {
|
|
823
|
+
const absenceVerified = sectionWait.absence_probe?.verified === true;
|
|
824
|
+
return {
|
|
825
|
+
checked: absenceVerified,
|
|
826
|
+
panel_found: false,
|
|
827
|
+
recent: absenceVerified ? false : null,
|
|
828
|
+
indeterminate: !absenceVerified,
|
|
829
|
+
reason: "panel_missing",
|
|
830
|
+
window_days: windowDays,
|
|
831
|
+
absence_probe: sectionWait.absence_probe,
|
|
832
|
+
rows: []
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
if (sections.length !== 1) {
|
|
836
|
+
return {
|
|
837
|
+
checked: false,
|
|
838
|
+
panel_found: true,
|
|
839
|
+
recent: null,
|
|
840
|
+
indeterminate: true,
|
|
841
|
+
reason: "panel_ambiguous",
|
|
842
|
+
window_days: windowDays,
|
|
843
|
+
visible_section_count: sections.length,
|
|
844
|
+
rows: []
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const section = sections[0];
|
|
849
|
+
const tabHeaderIds = await querySelectorAll(client, section.node_id, ".tab-hd");
|
|
850
|
+
let visibleTabHeaderCount = 0;
|
|
851
|
+
for (const nodeId of tabHeaderIds) {
|
|
852
|
+
if (await readVisibleNodeEvidence(client, nodeId)) visibleTabHeaderCount += 1;
|
|
853
|
+
}
|
|
854
|
+
const tab = await ensureColleagueTabSelected(client, section.node_id);
|
|
855
|
+
if (!tab.selected) {
|
|
856
|
+
return {
|
|
857
|
+
checked: false,
|
|
858
|
+
panel_found: true,
|
|
859
|
+
recent: null,
|
|
860
|
+
indeterminate: true,
|
|
861
|
+
reason: "colleague_tab_unavailable",
|
|
862
|
+
window_days: windowDays,
|
|
863
|
+
section_root: section.root,
|
|
864
|
+
section_node_id: section.node_id,
|
|
865
|
+
section_backend_node_id: section.backend_node_id,
|
|
866
|
+
tab_header_found: visibleTabHeaderCount === 1,
|
|
867
|
+
selected_tab_text: tab.selected_text,
|
|
868
|
+
selected_tab_count: tab.selected_tab_count,
|
|
869
|
+
pane_binding_verified: false,
|
|
870
|
+
rows: []
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const scrollScan = await scanContactRowsAcrossScrollPositions(
|
|
875
|
+
client,
|
|
876
|
+
scopes,
|
|
877
|
+
section,
|
|
878
|
+
tab,
|
|
879
|
+
{
|
|
880
|
+
enabled: scroll,
|
|
881
|
+
maxScrolls: scrollMaxSteps,
|
|
882
|
+
settleMs: scrollSettleMs
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
const {
|
|
886
|
+
rows = [],
|
|
887
|
+
...scroll_probe
|
|
888
|
+
} = scrollScan;
|
|
889
|
+
|
|
890
|
+
const sectionAfter = await readVisibleNodeEvidence(client, section.node_id);
|
|
891
|
+
const selectedAfter = await readSelectedColleagueTab(client, section.node_id);
|
|
892
|
+
const binding = {
|
|
893
|
+
verified: Boolean(
|
|
894
|
+
sectionAfter
|
|
895
|
+
&& sectionAfter.backend_node_id === section.backend_node_id
|
|
896
|
+
&& tab.selected === true
|
|
897
|
+
&& selectedAfter.selected === true
|
|
898
|
+
&& selectedAfter.backend_node_id === tab.backend_node_id
|
|
899
|
+
&& selectedAfter.selected_tab_count === 1
|
|
900
|
+
),
|
|
901
|
+
detail_root_node_id: scopes[0]?.nodeId || null,
|
|
902
|
+
section_node_id: section.node_id,
|
|
903
|
+
section_backend_node_id: section.backend_node_id,
|
|
904
|
+
section_visible: Boolean(sectionAfter),
|
|
905
|
+
selected_tab_node_id: tab.node_id || null,
|
|
906
|
+
selected_tab_backend_node_id: tab.backend_node_id || null,
|
|
907
|
+
selected_tab_text: tab.selected_text,
|
|
908
|
+
selected_tab_visible: tab.selected === true,
|
|
909
|
+
selected_tab_count: tab.selected_tab_count,
|
|
910
|
+
selection_reverified_after_rows: selectedAfter.selected === true,
|
|
911
|
+
selected_tab_backend_node_id_after_rows: selectedAfter.backend_node_id || null,
|
|
912
|
+
row_scope: "selected_section_descendants"
|
|
913
|
+
};
|
|
914
|
+
if (!binding.verified) {
|
|
915
|
+
return {
|
|
916
|
+
checked: false,
|
|
917
|
+
panel_found: true,
|
|
918
|
+
recent: null,
|
|
919
|
+
indeterminate: true,
|
|
920
|
+
reason: "colleague_binding_lost",
|
|
921
|
+
window_days: windowDays,
|
|
922
|
+
section_root: section.root,
|
|
923
|
+
section_node_id: section.node_id,
|
|
924
|
+
section_backend_node_id: section.backend_node_id,
|
|
925
|
+
tab_header_found: visibleTabHeaderCount === 1,
|
|
926
|
+
selected_tab_text: selectedAfter.selected_text || tab.selected_text,
|
|
927
|
+
selected_tab_count: selectedAfter.selected_tab_count,
|
|
928
|
+
pane_binding_verified: false,
|
|
929
|
+
binding,
|
|
930
|
+
rows: [],
|
|
931
|
+
scroll_probe
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const parsedRows = rows.map((row) => {
|
|
936
|
+
const parsedDate = parseColleagueContactDate(row.text, { referenceDate });
|
|
937
|
+
return {
|
|
938
|
+
...row,
|
|
939
|
+
parsed_date: parsedDate ? formatLocalDate(parsedDate) : null,
|
|
940
|
+
within_window: parsedDate
|
|
941
|
+
? isDateWithinWindow(parsedDate, { referenceDate, windowDays })
|
|
942
|
+
: false
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
const matched = parsedRows.find((row) => row.within_window) || null;
|
|
946
|
+
if (matched) {
|
|
947
|
+
return {
|
|
948
|
+
checked: true,
|
|
949
|
+
panel_found: true,
|
|
950
|
+
recent: true,
|
|
951
|
+
indeterminate: false,
|
|
952
|
+
reason: "recent_colleague_contact_found",
|
|
953
|
+
window_days: windowDays,
|
|
954
|
+
section_root: section.root,
|
|
955
|
+
section_node_id: section.node_id,
|
|
956
|
+
section_backend_node_id: section.backend_node_id,
|
|
957
|
+
tab_header_found: visibleTabHeaderCount === 1,
|
|
958
|
+
selected_tab_text: tab.selected_text,
|
|
959
|
+
selected_tab_count: tab.selected_tab_count,
|
|
960
|
+
pane_binding_verified: true,
|
|
961
|
+
binding,
|
|
962
|
+
tab_changed: tab.changed,
|
|
963
|
+
matched_row: matched,
|
|
964
|
+
row_count: parsedRows.length,
|
|
965
|
+
rows: parsedRows,
|
|
966
|
+
scroll_probe
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
if (!scroll_probe.completed) {
|
|
970
|
+
return {
|
|
971
|
+
checked: false,
|
|
972
|
+
panel_found: true,
|
|
973
|
+
recent: null,
|
|
974
|
+
indeterminate: true,
|
|
975
|
+
reason: scroll_probe.reason || "colleague_scroll_scan_incomplete",
|
|
976
|
+
window_days: windowDays,
|
|
977
|
+
section_root: section.root,
|
|
978
|
+
section_node_id: section.node_id,
|
|
979
|
+
section_backend_node_id: section.backend_node_id,
|
|
980
|
+
tab_header_found: visibleTabHeaderCount === 1,
|
|
981
|
+
selected_tab_text: tab.selected_text,
|
|
982
|
+
selected_tab_count: tab.selected_tab_count,
|
|
983
|
+
pane_binding_verified: true,
|
|
984
|
+
binding,
|
|
985
|
+
rows: parsedRows,
|
|
986
|
+
scroll_probe
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
if (!parsedRows.length) {
|
|
990
|
+
return {
|
|
991
|
+
checked: false,
|
|
992
|
+
panel_found: true,
|
|
993
|
+
recent: null,
|
|
994
|
+
indeterminate: true,
|
|
995
|
+
reason: "contact_rows_missing",
|
|
996
|
+
window_days: windowDays,
|
|
997
|
+
section_root: section.root,
|
|
998
|
+
section_node_id: section.node_id,
|
|
999
|
+
section_backend_node_id: section.backend_node_id,
|
|
1000
|
+
tab_header_found: visibleTabHeaderCount === 1,
|
|
1001
|
+
selected_tab_text: tab.selected_text,
|
|
1002
|
+
selected_tab_count: tab.selected_tab_count,
|
|
1003
|
+
pane_binding_verified: true,
|
|
1004
|
+
binding,
|
|
1005
|
+
tab_changed: tab.changed,
|
|
1006
|
+
row_count: 0,
|
|
1007
|
+
rows: [],
|
|
1008
|
+
scroll_probe
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
const unparsedRows = parsedRows.filter((row) => !row.parsed_date);
|
|
1012
|
+
if (unparsedRows.length) {
|
|
1013
|
+
return {
|
|
1014
|
+
checked: false,
|
|
1015
|
+
panel_found: true,
|
|
1016
|
+
recent: null,
|
|
1017
|
+
indeterminate: true,
|
|
1018
|
+
reason: "contact_date_unparseable",
|
|
1019
|
+
window_days: windowDays,
|
|
1020
|
+
section_root: section.root,
|
|
1021
|
+
section_node_id: section.node_id,
|
|
1022
|
+
section_backend_node_id: section.backend_node_id,
|
|
1023
|
+
tab_header_found: visibleTabHeaderCount === 1,
|
|
1024
|
+
selected_tab_text: tab.selected_text,
|
|
1025
|
+
selected_tab_count: tab.selected_tab_count,
|
|
1026
|
+
pane_binding_verified: true,
|
|
1027
|
+
binding,
|
|
1028
|
+
tab_changed: tab.changed,
|
|
1029
|
+
row_count: parsedRows.length,
|
|
1030
|
+
unparsed_row_count: unparsedRows.length,
|
|
1031
|
+
unparsed_rows: unparsedRows,
|
|
1032
|
+
rows: parsedRows,
|
|
1033
|
+
scroll_probe
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (!scroll_probe.coverage_verified) {
|
|
1037
|
+
return {
|
|
1038
|
+
checked: false,
|
|
1039
|
+
panel_found: true,
|
|
1040
|
+
recent: null,
|
|
1041
|
+
indeterminate: true,
|
|
1042
|
+
reason: scroll_probe.reason || "colleague_scroll_scan_incomplete",
|
|
1043
|
+
window_days: windowDays,
|
|
1044
|
+
section_root: section.root,
|
|
1045
|
+
section_node_id: section.node_id,
|
|
1046
|
+
section_backend_node_id: section.backend_node_id,
|
|
1047
|
+
tab_header_found: visibleTabHeaderCount === 1,
|
|
1048
|
+
selected_tab_text: tab.selected_text,
|
|
1049
|
+
selected_tab_count: tab.selected_tab_count,
|
|
1050
|
+
pane_binding_verified: true,
|
|
1051
|
+
binding,
|
|
1052
|
+
tab_changed: tab.changed,
|
|
1053
|
+
row_count: parsedRows.length,
|
|
1054
|
+
rows: parsedRows,
|
|
1055
|
+
scroll_probe
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
checked: true,
|
|
1060
|
+
panel_found: true,
|
|
1061
|
+
recent: false,
|
|
1062
|
+
indeterminate: false,
|
|
1063
|
+
reason: "no_recent_colleague_contact",
|
|
1064
|
+
window_days: windowDays,
|
|
1065
|
+
section_root: section.root,
|
|
1066
|
+
section_node_id: section.node_id,
|
|
1067
|
+
section_backend_node_id: section.backend_node_id,
|
|
1068
|
+
tab_header_found: visibleTabHeaderCount === 1,
|
|
1069
|
+
selected_tab_text: tab.selected_text,
|
|
1070
|
+
selected_tab_count: tab.selected_tab_count,
|
|
1071
|
+
pane_binding_verified: true,
|
|
1072
|
+
binding,
|
|
1073
|
+
tab_changed: tab.changed,
|
|
1074
|
+
matched_row: null,
|
|
1075
|
+
row_count: parsedRows.length,
|
|
1076
|
+
rows: parsedRows,
|
|
1077
|
+
scroll_probe
|
|
1078
|
+
};
|
|
1079
|
+
}
|