@reconcrap/boss-recommend-mcp 2.0.31 → 2.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/chat-mcp.js +2 -1
- package/src/core/cv-acquisition/index.js +1 -0
- package/src/core/cv-capture-target/index.js +299 -0
- package/src/domains/chat/run-service.js +20 -6
- package/src/domains/recommend/detail.js +28 -4
- package/src/domains/recommend/run-service.js +22 -10
- package/src/domains/recruit/detail.js +28 -4
- package/src/domains/recruit/run-service.js +21 -9
- package/src/index.js +1 -1
- package/src/recommend-mcp.js +2 -1
- package/src/recruit-mcp.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reconcrap/boss-recommend-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.32",
|
|
4
4
|
"description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"boss",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"test:run-state": "node src/test-run-state.js",
|
|
25
25
|
"test:cdp-browser": "node src/test-cdp-browser.js",
|
|
26
26
|
"test:core-capture": "node src/test-core-capture.js",
|
|
27
|
+
"test:core-cv-capture-target": "node src/test-core-cv-capture-target.js",
|
|
27
28
|
"test:core-cv-acquisition": "node src/test-core-cv-acquisition.js",
|
|
28
29
|
"test:core-greet-quota": "node src/test-core-greet-quota.js",
|
|
29
30
|
"test:core-infinite-list": "node src/test-core-infinite-list.js",
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"live:chat-domain": "node scripts/live-chat-domain-smoke.js",
|
|
73
74
|
"live:chat-run-service": "node scripts/live-chat-run-service-smoke.js",
|
|
74
75
|
"live:chat-mcp": "node scripts/live-chat-mcp-smoke.js",
|
|
76
|
+
"live:cv-capture-target": "node scripts/live-cv-capture-target-smoke.js",
|
|
75
77
|
"live:chat-phase10-full": "node scripts/live-chat-phase10-full.js",
|
|
76
78
|
"live:chat-image-screening": "node scripts/live-chat-image-screening-smoke.js"
|
|
77
79
|
},
|
package/src/chat-mcp.js
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
resolveBossChatRuntimeLayout,
|
|
51
51
|
resolveBossScreeningConfig
|
|
52
52
|
} from "./chat-runtime-config.js";
|
|
53
|
+
import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
|
|
53
54
|
|
|
54
55
|
const DEFAULT_CHAT_HOST = "127.0.0.1";
|
|
55
56
|
const DEFAULT_CHAT_PORT = 9222;
|
|
@@ -1100,7 +1101,7 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "", configRe
|
|
|
1100
1101
|
slowLive ? 30000 : 15000
|
|
1101
1102
|
),
|
|
1102
1103
|
resumeDomTimeoutMs: slowLive ? 120000 : 60000,
|
|
1103
|
-
maxImagePages: parsePositiveInteger(args.max_image_pages,
|
|
1104
|
+
maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
|
|
1104
1105
|
imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
|
|
1105
1106
|
llmConfig: resolvedConfig.ok ? {
|
|
1106
1107
|
...resolvedConfig.config
|
|
@@ -5,6 +5,7 @@ export const CV_ACQUISITION_MODE_IMAGE = "image";
|
|
|
5
5
|
export const NETWORK_RESUME_WAIT_MS = 4200;
|
|
6
6
|
export const NETWORK_RESUME_RETRY_WAIT_MS = 2000;
|
|
7
7
|
export const NETWORK_RESUME_IMAGE_MODE_GRACE_MS = 1000;
|
|
8
|
+
export const DEFAULT_MAX_IMAGE_PAGES = 24;
|
|
8
9
|
|
|
9
10
|
const VALID_MODES = new Set([
|
|
10
11
|
CV_ACQUISITION_MODE_UNKNOWN,
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getFrameDocumentNodeId,
|
|
3
|
+
getNodeBox,
|
|
4
|
+
querySelector,
|
|
5
|
+
querySelectorAll,
|
|
6
|
+
sleep
|
|
7
|
+
} from "../browser/index.js";
|
|
8
|
+
|
|
9
|
+
export const CV_CAPTURE_TARGET_SELECTORS = Object.freeze([
|
|
10
|
+
".resume-center-side .resume-detail-wrap",
|
|
11
|
+
".resume-container .resume-detail-wrap",
|
|
12
|
+
".resume-container .resume-content-wrap",
|
|
13
|
+
".resume-item-detail",
|
|
14
|
+
".resume-detail-wrap",
|
|
15
|
+
".resume-content-wrap",
|
|
16
|
+
".resume-common-wrap",
|
|
17
|
+
".new-resume-online-main-ui",
|
|
18
|
+
".resume-detail",
|
|
19
|
+
".resume-recommend",
|
|
20
|
+
"canvas#resume",
|
|
21
|
+
".resume-container"
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const IFRAME_BODY_SELECTORS = Object.freeze(["body", "html"]);
|
|
25
|
+
|
|
26
|
+
function slotNodeId(slot = null) {
|
|
27
|
+
return Number(slot?.node_id || slot?.nodeId || 0) || 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rootNodeId(root = null) {
|
|
31
|
+
return Number(root?.nodeId || root?.node_id || root?.root_node_id || 0) || 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeRootName(root = null, fallback = "") {
|
|
35
|
+
return String(root?.name || root?.root || fallback || "").trim() || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function uniqueRoots(roots = []) {
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const root of roots) {
|
|
42
|
+
const nodeId = rootNodeId(root);
|
|
43
|
+
if (!nodeId || seen.has(nodeId)) continue;
|
|
44
|
+
seen.add(nodeId);
|
|
45
|
+
result.push({
|
|
46
|
+
...root,
|
|
47
|
+
name: normalizeRootName(root),
|
|
48
|
+
nodeId
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function slotAsRoot(slot = null, fallbackName = "") {
|
|
55
|
+
const nodeId = slotNodeId(slot);
|
|
56
|
+
if (!nodeId) return null;
|
|
57
|
+
return {
|
|
58
|
+
name: normalizeRootName(slot, fallbackName),
|
|
59
|
+
nodeId,
|
|
60
|
+
selector: slot?.selector || null,
|
|
61
|
+
root_node_id: slot?.root_node_id || null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isVisibleBox(box = null) {
|
|
66
|
+
return box?.rect?.width > 2 && box?.rect?.height > 2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readVisibleBox(client, nodeId) {
|
|
70
|
+
if (!nodeId) return null;
|
|
71
|
+
try {
|
|
72
|
+
const box = await getNodeBox(client, nodeId);
|
|
73
|
+
return isVisibleBox(box) ? box : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isCvScopedSelector(selector = "") {
|
|
80
|
+
const normalized = String(selector || "").trim();
|
|
81
|
+
if (!normalized) return false;
|
|
82
|
+
if (/boss-popup|boss-dialog|dialog-wrap|geek-detail-modal|\bmodal\b|new-chat-resume-dialog-main-ui/i.test(normalized)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return /resume-item-detail|resume-detail-wrap|resume-content-wrap|resume-common-wrap|new-resume-online-main-ui|resume-detail(?:\b|[.#:])|resume-recommend|canvas#resume|resume-container/i.test(normalized);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildTarget({
|
|
89
|
+
domain = "",
|
|
90
|
+
nodeId,
|
|
91
|
+
source,
|
|
92
|
+
selector = null,
|
|
93
|
+
root = null,
|
|
94
|
+
rootNodeId = null,
|
|
95
|
+
box = null,
|
|
96
|
+
iframeNodeId = null,
|
|
97
|
+
iframeDocumentNodeId = null,
|
|
98
|
+
fallback = false
|
|
99
|
+
} = {}) {
|
|
100
|
+
return {
|
|
101
|
+
schema_version: 1,
|
|
102
|
+
domain: domain || null,
|
|
103
|
+
node_id: nodeId,
|
|
104
|
+
source,
|
|
105
|
+
selector,
|
|
106
|
+
root,
|
|
107
|
+
root_node_id: rootNodeId || null,
|
|
108
|
+
iframe_node_id: iframeNodeId || null,
|
|
109
|
+
iframe_document_node_id: iframeDocumentNodeId || null,
|
|
110
|
+
cv_only: !fallback,
|
|
111
|
+
fallback: Boolean(fallback),
|
|
112
|
+
rect: box?.rect || null,
|
|
113
|
+
center: box?.center || null
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function firstVisibleSelectorTarget(client, roots = [], selectors = CV_CAPTURE_TARGET_SELECTORS, {
|
|
118
|
+
domain = "",
|
|
119
|
+
source = "cv_selector",
|
|
120
|
+
iframeNodeId = null,
|
|
121
|
+
iframeDocumentNodeId = null
|
|
122
|
+
} = {}) {
|
|
123
|
+
for (const root of uniqueRoots(roots)) {
|
|
124
|
+
for (const selector of selectors) {
|
|
125
|
+
let nodeIds = [];
|
|
126
|
+
try {
|
|
127
|
+
nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
128
|
+
} catch {
|
|
129
|
+
nodeIds = [];
|
|
130
|
+
}
|
|
131
|
+
for (const nodeId of nodeIds) {
|
|
132
|
+
const box = await readVisibleBox(client, nodeId);
|
|
133
|
+
if (!box) continue;
|
|
134
|
+
return buildTarget({
|
|
135
|
+
domain,
|
|
136
|
+
nodeId,
|
|
137
|
+
source,
|
|
138
|
+
selector,
|
|
139
|
+
root: root.name,
|
|
140
|
+
rootNodeId: root.nodeId,
|
|
141
|
+
box,
|
|
142
|
+
iframeNodeId,
|
|
143
|
+
iframeDocumentNodeId
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function visibleSlotTarget(client, slot = null, {
|
|
152
|
+
domain = "",
|
|
153
|
+
source = "cv_slot",
|
|
154
|
+
fallback = false
|
|
155
|
+
} = {}) {
|
|
156
|
+
const nodeId = slotNodeId(slot);
|
|
157
|
+
if (!nodeId) return null;
|
|
158
|
+
if (!fallback && !isCvScopedSelector(slot?.selector)) return null;
|
|
159
|
+
const box = await readVisibleBox(client, nodeId);
|
|
160
|
+
if (!box) return null;
|
|
161
|
+
return buildTarget({
|
|
162
|
+
domain,
|
|
163
|
+
nodeId,
|
|
164
|
+
source,
|
|
165
|
+
selector: slot?.selector || null,
|
|
166
|
+
root: slot?.root || null,
|
|
167
|
+
rootNodeId: slot?.root_node_id || null,
|
|
168
|
+
box,
|
|
169
|
+
fallback
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function resolveIframeCaptureTarget(client, resumeIframe = null, {
|
|
174
|
+
domain = "",
|
|
175
|
+
selectors = CV_CAPTURE_TARGET_SELECTORS
|
|
176
|
+
} = {}) {
|
|
177
|
+
const iframeNodeId = slotNodeId(resumeIframe);
|
|
178
|
+
if (!iframeNodeId) return null;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const documentNodeId = await getFrameDocumentNodeId(client, iframeNodeId);
|
|
182
|
+
const selectorTarget = await firstVisibleSelectorTarget(client, [{
|
|
183
|
+
name: "resume-iframe-document",
|
|
184
|
+
nodeId: documentNodeId
|
|
185
|
+
}], selectors, {
|
|
186
|
+
domain,
|
|
187
|
+
source: "resume_iframe_cv_selector",
|
|
188
|
+
iframeNodeId,
|
|
189
|
+
iframeDocumentNodeId: documentNodeId
|
|
190
|
+
});
|
|
191
|
+
if (selectorTarget) return selectorTarget;
|
|
192
|
+
|
|
193
|
+
for (const selector of IFRAME_BODY_SELECTORS) {
|
|
194
|
+
const nodeId = await querySelector(client, documentNodeId, selector).catch(() => 0);
|
|
195
|
+
const box = await readVisibleBox(client, nodeId);
|
|
196
|
+
if (!box) continue;
|
|
197
|
+
return buildTarget({
|
|
198
|
+
domain,
|
|
199
|
+
nodeId,
|
|
200
|
+
source: "resume_iframe_body",
|
|
201
|
+
selector,
|
|
202
|
+
root: "resume-iframe-document",
|
|
203
|
+
rootNodeId: documentNodeId,
|
|
204
|
+
box,
|
|
205
|
+
iframeNodeId,
|
|
206
|
+
iframeDocumentNodeId: documentNodeId
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
|
|
211
|
+
const iframeBox = await readVisibleBox(client, iframeNodeId);
|
|
212
|
+
if (!iframeBox) return null;
|
|
213
|
+
return buildTarget({
|
|
214
|
+
domain,
|
|
215
|
+
nodeId: iframeNodeId,
|
|
216
|
+
source: "resume_iframe_element",
|
|
217
|
+
selector: resumeIframe?.selector || null,
|
|
218
|
+
root: resumeIframe?.root || null,
|
|
219
|
+
rootNodeId: resumeIframe?.root_node_id || null,
|
|
220
|
+
box: iframeBox,
|
|
221
|
+
iframeNodeId,
|
|
222
|
+
fallback: false
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function resolveSlotCaptureTarget(client, slot = null, {
|
|
227
|
+
domain = "",
|
|
228
|
+
slotName = "content",
|
|
229
|
+
selectors = CV_CAPTURE_TARGET_SELECTORS
|
|
230
|
+
} = {}) {
|
|
231
|
+
const root = slotAsRoot(slot, slotName);
|
|
232
|
+
const selectorTarget = root
|
|
233
|
+
? await firstVisibleSelectorTarget(client, [root], selectors, {
|
|
234
|
+
domain,
|
|
235
|
+
source: `${slotName}_cv_selector`
|
|
236
|
+
})
|
|
237
|
+
: null;
|
|
238
|
+
if (selectorTarget) return selectorTarget;
|
|
239
|
+
return visibleSlotTarget(client, slot, {
|
|
240
|
+
domain,
|
|
241
|
+
source: `${slotName}_cv_slot`
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function resolveCvCaptureTarget(client, detailState = null, {
|
|
246
|
+
domain = "",
|
|
247
|
+
selectors = CV_CAPTURE_TARGET_SELECTORS
|
|
248
|
+
} = {}) {
|
|
249
|
+
const iframeTarget = await resolveIframeCaptureTarget(client, detailState?.resumeIframe, {
|
|
250
|
+
domain,
|
|
251
|
+
selectors
|
|
252
|
+
});
|
|
253
|
+
if (iframeTarget) return iframeTarget;
|
|
254
|
+
|
|
255
|
+
const contentTarget = await resolveSlotCaptureTarget(client, detailState?.content, {
|
|
256
|
+
domain,
|
|
257
|
+
slotName: "content",
|
|
258
|
+
selectors
|
|
259
|
+
});
|
|
260
|
+
if (contentTarget) return contentTarget;
|
|
261
|
+
|
|
262
|
+
const popupTarget = await resolveSlotCaptureTarget(client, detailState?.popup, {
|
|
263
|
+
domain,
|
|
264
|
+
slotName: "popup",
|
|
265
|
+
selectors
|
|
266
|
+
});
|
|
267
|
+
if (popupTarget) return popupTarget;
|
|
268
|
+
|
|
269
|
+
return firstVisibleSelectorTarget(client, detailState?.roots || [], selectors, {
|
|
270
|
+
domain,
|
|
271
|
+
source: "root_cv_selector"
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function waitForCvCaptureTarget(client, detailState = null, {
|
|
276
|
+
timeoutMs = 6000,
|
|
277
|
+
intervalMs = 250,
|
|
278
|
+
...options
|
|
279
|
+
} = {}) {
|
|
280
|
+
const started = Date.now();
|
|
281
|
+
let target = null;
|
|
282
|
+
while (Date.now() - started <= Math.max(0, Number(timeoutMs) || 0)) {
|
|
283
|
+
target = await resolveCvCaptureTarget(client, detailState, options);
|
|
284
|
+
if (target?.node_id) {
|
|
285
|
+
return {
|
|
286
|
+
ok: true,
|
|
287
|
+
elapsed_ms: Date.now() - started,
|
|
288
|
+
target
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
await sleep(Math.max(50, Number(intervalMs) || 250));
|
|
292
|
+
}
|
|
293
|
+
target = await resolveCvCaptureTarget(client, detailState, options);
|
|
294
|
+
return {
|
|
295
|
+
ok: Boolean(target?.node_id),
|
|
296
|
+
elapsed_ms: Date.now() - started,
|
|
297
|
+
target: target || null
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
2
|
+
import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
|
|
2
3
|
import {
|
|
3
4
|
clickPoint,
|
|
4
5
|
getNodeBox,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
compactCvAcquisitionState,
|
|
10
11
|
countParsedNetworkProfiles,
|
|
11
12
|
createCvAcquisitionState,
|
|
13
|
+
DEFAULT_MAX_IMAGE_PAGES,
|
|
12
14
|
getCvNetworkWaitPlan,
|
|
13
15
|
recordCvImageFallback,
|
|
14
16
|
recordCvNetworkHit,
|
|
@@ -205,9 +207,9 @@ function llmToScreening(llmResult, candidate) {
|
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
export function captureNodeIdFromResumeState(resumeState) {
|
|
208
|
-
return resumeState?.
|
|
209
|
-
|| resumeState?.content?.node_id
|
|
210
|
+
return resumeState?.content?.node_id
|
|
210
211
|
|| resumeState?.resumeIframe?.node_id
|
|
212
|
+
|| resumeState?.popup?.node_id
|
|
211
213
|
|| null;
|
|
212
214
|
}
|
|
213
215
|
|
|
@@ -641,7 +643,7 @@ export async function runChatWorkflow({
|
|
|
641
643
|
readyTimeoutMs = 60000,
|
|
642
644
|
onlineResumeButtonTimeoutMs = 30000,
|
|
643
645
|
resumeDomTimeoutMs = 60000,
|
|
644
|
-
maxImagePages =
|
|
646
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
645
647
|
imageWheelDeltaY = 650,
|
|
646
648
|
cvAcquisitionMode = "unknown",
|
|
647
649
|
callLlmOnImage = false,
|
|
@@ -1231,11 +1233,19 @@ export async function runChatWorkflow({
|
|
|
1231
1233
|
let source = normalizedDetailSource === "dom" ? "dom" : "network";
|
|
1232
1234
|
let imageEvidence = null;
|
|
1233
1235
|
let llmResult = null;
|
|
1234
|
-
|
|
1236
|
+
let captureTarget = null;
|
|
1237
|
+
let captureTargetWait = null;
|
|
1235
1238
|
let fullCvEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
|
|
1236
1239
|
const shouldCaptureImage = normalizedDetailSource === "image"
|
|
1237
1240
|
|| (normalizedDetailSource === "cascade" && !fullCvEvidence.full_cv_acquired);
|
|
1238
1241
|
if (shouldCaptureImage) {
|
|
1242
|
+
captureTargetWait = await waitForCvCaptureTarget(client, resumeState, {
|
|
1243
|
+
domain: "chat",
|
|
1244
|
+
timeoutMs: 6000,
|
|
1245
|
+
intervalMs: 250
|
|
1246
|
+
});
|
|
1247
|
+
captureTarget = captureTargetWait.target || null;
|
|
1248
|
+
const captureNodeId = captureTarget?.node_id || null;
|
|
1239
1249
|
if (captureNodeId) {
|
|
1240
1250
|
detailStep = "capture_image_fallback";
|
|
1241
1251
|
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
@@ -1277,7 +1287,9 @@ export async function runChatWorkflow({
|
|
|
1277
1287
|
? "forced_image"
|
|
1278
1288
|
: "network_miss_image_fallback",
|
|
1279
1289
|
run_candidate_index: index,
|
|
1280
|
-
candidate_key: candidateKey
|
|
1290
|
+
candidate_key: candidateKey,
|
|
1291
|
+
capture_target: captureTarget,
|
|
1292
|
+
capture_target_wait: captureTargetWait
|
|
1281
1293
|
}
|
|
1282
1294
|
}));
|
|
1283
1295
|
source = "image";
|
|
@@ -1421,6 +1433,8 @@ export async function runChatWorkflow({
|
|
|
1421
1433
|
},
|
|
1422
1434
|
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
1423
1435
|
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
1436
|
+
capture_target: captureTarget || null,
|
|
1437
|
+
capture_target_wait: captureTargetWait,
|
|
1424
1438
|
full_cv_evidence: fullCvEvidence
|
|
1425
1439
|
};
|
|
1426
1440
|
}
|
|
@@ -1605,7 +1619,7 @@ export function createChatRunService({
|
|
|
1605
1619
|
readyTimeoutMs = 60000,
|
|
1606
1620
|
onlineResumeButtonTimeoutMs = 30000,
|
|
1607
1621
|
resumeDomTimeoutMs = 60000,
|
|
1608
|
-
maxImagePages =
|
|
1622
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
1609
1623
|
imageWheelDeltaY = 650,
|
|
1610
1624
|
cvAcquisitionMode = "unknown",
|
|
1611
1625
|
callLlmOnImage = false,
|
|
@@ -20,8 +20,7 @@ import {
|
|
|
20
20
|
DETAIL_RESUME_IFRAME_SELECTORS
|
|
21
21
|
} from "./constants.js";
|
|
22
22
|
import {
|
|
23
|
-
getRecommendRoots
|
|
24
|
-
queryFirstAcrossRoots
|
|
23
|
+
getRecommendRoots
|
|
25
24
|
} from "./roots.js";
|
|
26
25
|
import {
|
|
27
26
|
findRecommendCardNodeIds,
|
|
@@ -133,8 +132,8 @@ export async function waitForRecommendDetail(client, {
|
|
|
133
132
|
let lastState = null;
|
|
134
133
|
while (Date.now() - started <= timeoutMs) {
|
|
135
134
|
const rootState = await getRecommendRoots(client);
|
|
136
|
-
const popup = await
|
|
137
|
-
const resumeIframe = await
|
|
135
|
+
const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
|
|
136
|
+
const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
|
|
138
137
|
lastState = {
|
|
139
138
|
iframe: rootState.iframe,
|
|
140
139
|
roots: rootState.roots,
|
|
@@ -147,6 +146,31 @@ export async function waitForRecommendDetail(client, {
|
|
|
147
146
|
return lastState;
|
|
148
147
|
}
|
|
149
148
|
|
|
149
|
+
async function findVisibleDetailTarget(client, roots, selectors) {
|
|
150
|
+
for (const root of roots) {
|
|
151
|
+
if (!root?.nodeId) continue;
|
|
152
|
+
for (const selector of selectors) {
|
|
153
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
154
|
+
for (const nodeId of nodeIds) {
|
|
155
|
+
try {
|
|
156
|
+
const box = await getNodeBox(client, nodeId);
|
|
157
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
158
|
+
return {
|
|
159
|
+
root: root.name,
|
|
160
|
+
root_node_id: root.nodeId,
|
|
161
|
+
selector,
|
|
162
|
+
node_id: nodeId,
|
|
163
|
+
center: box.center,
|
|
164
|
+
rect: box.rect
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
150
174
|
export async function readRecommendDetailHtml(client, detailState) {
|
|
151
175
|
let popupHTML = "";
|
|
152
176
|
let resumeHTML = "";
|
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
measureTiming
|
|
8
8
|
} from "../../core/run/timing.js";
|
|
9
9
|
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
10
|
+
import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
|
|
10
11
|
import { sleep } from "../../core/browser/index.js";
|
|
11
12
|
import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
|
|
12
13
|
import {
|
|
13
14
|
compactCvAcquisitionState,
|
|
14
15
|
countParsedNetworkProfiles,
|
|
15
16
|
createCvAcquisitionState,
|
|
17
|
+
DEFAULT_MAX_IMAGE_PAGES,
|
|
16
18
|
getCvNetworkWaitPlan,
|
|
17
19
|
recordCvImageFallback,
|
|
18
20
|
recordCvNetworkHit,
|
|
@@ -404,7 +406,7 @@ export function createRecoverableImageCaptureEvidence(error, {
|
|
|
404
406
|
elapsedMs = 0,
|
|
405
407
|
filePath = "",
|
|
406
408
|
extension = "jpg",
|
|
407
|
-
maxScreenshots =
|
|
409
|
+
maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
|
|
408
410
|
} = {}) {
|
|
409
411
|
const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
|
|
410
412
|
return {
|
|
@@ -474,7 +476,7 @@ export async function runRecommendWorkflow({
|
|
|
474
476
|
closeDetail = true,
|
|
475
477
|
delayMs = 0,
|
|
476
478
|
cardTimeoutMs = 10000,
|
|
477
|
-
maxImagePages =
|
|
479
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
478
480
|
imageWheelDeltaY = 650,
|
|
479
481
|
cvAcquisitionMode = "unknown",
|
|
480
482
|
listMaxScrolls = 20,
|
|
@@ -820,15 +822,21 @@ export async function runRecommendWorkflow({
|
|
|
820
822
|
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
821
823
|
let source = "network";
|
|
822
824
|
let imageEvidence = null;
|
|
825
|
+
let captureTarget = null;
|
|
826
|
+
let captureTargetWait = null;
|
|
823
827
|
if (parsedNetworkProfileCount > 0) {
|
|
824
828
|
recordCvNetworkHit(cvAcquisitionState, {
|
|
825
829
|
parsedNetworkProfileCount,
|
|
826
830
|
waitResult: networkWait
|
|
827
831
|
});
|
|
828
832
|
} else {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
833
|
+
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
834
|
+
domain: "recommend",
|
|
835
|
+
timeoutMs: 6000,
|
|
836
|
+
intervalMs: 250
|
|
837
|
+
});
|
|
838
|
+
captureTarget = captureTargetWait.target || null;
|
|
839
|
+
const captureNodeId = captureTarget?.node_id || null;
|
|
832
840
|
if (captureNodeId) {
|
|
833
841
|
const imageEvidencePath = imageEvidenceFilePath({
|
|
834
842
|
imageOutputDir,
|
|
@@ -844,8 +852,8 @@ export async function runRecommendWorkflow({
|
|
|
844
852
|
quality: 72,
|
|
845
853
|
optimize: true,
|
|
846
854
|
resizeMaxWidth: 1100,
|
|
847
|
-
captureViewport:
|
|
848
|
-
padding:
|
|
855
|
+
captureViewport: false,
|
|
856
|
+
padding: 0,
|
|
849
857
|
maxScreenshots: maxImagePages,
|
|
850
858
|
wheelDeltaY: imageWheelDeltaY,
|
|
851
859
|
settleMs: 350,
|
|
@@ -863,7 +871,9 @@ export async function runRecommendWorkflow({
|
|
|
863
871
|
capture_mode: "scroll_sequence",
|
|
864
872
|
acquisition_reason: "network_miss_image_fallback",
|
|
865
873
|
run_candidate_index: index,
|
|
866
|
-
candidate_key: candidateKey
|
|
874
|
+
candidate_key: candidateKey,
|
|
875
|
+
capture_target: captureTarget,
|
|
876
|
+
capture_target_wait: captureTargetWait
|
|
867
877
|
}
|
|
868
878
|
}));
|
|
869
879
|
source = "image";
|
|
@@ -902,7 +912,9 @@ export async function runRecommendWorkflow({
|
|
|
902
912
|
wait_plan: waitPlan,
|
|
903
913
|
network_wait: networkWait,
|
|
904
914
|
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
905
|
-
image_evidence: summarizeImageEvidence(imageEvidence)
|
|
915
|
+
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
916
|
+
capture_target: captureTarget || null,
|
|
917
|
+
capture_target_wait: captureTargetWait
|
|
906
918
|
};
|
|
907
919
|
screeningCandidate = detailResult.candidate;
|
|
908
920
|
} catch (error) {
|
|
@@ -1120,7 +1132,7 @@ export function createRecommendRunService({
|
|
|
1120
1132
|
closeDetail = true,
|
|
1121
1133
|
delayMs = 0,
|
|
1122
1134
|
cardTimeoutMs = 10000,
|
|
1123
|
-
maxImagePages =
|
|
1135
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
1124
1136
|
imageWheelDeltaY = 650,
|
|
1125
1137
|
cvAcquisitionMode = "unknown",
|
|
1126
1138
|
listMaxScrolls = 20,
|
|
@@ -19,8 +19,7 @@ import {
|
|
|
19
19
|
RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS
|
|
20
20
|
} from "./constants.js";
|
|
21
21
|
import {
|
|
22
|
-
getRecruitRoots
|
|
23
|
-
queryFirstAcrossRoots
|
|
22
|
+
getRecruitRoots
|
|
24
23
|
} from "./roots.js";
|
|
25
24
|
|
|
26
25
|
export function matchesRecruitDetailNetwork(url) {
|
|
@@ -128,8 +127,8 @@ export async function waitForRecruitDetail(client, {
|
|
|
128
127
|
let lastState = null;
|
|
129
128
|
while (Date.now() - started <= timeoutMs) {
|
|
130
129
|
const rootState = await getRecruitRoots(client);
|
|
131
|
-
const popup = await
|
|
132
|
-
const resumeIframe = await
|
|
130
|
+
const popup = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_POPUP_SELECTORS);
|
|
131
|
+
const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS);
|
|
133
132
|
lastState = {
|
|
134
133
|
iframe: rootState.iframe,
|
|
135
134
|
roots: rootState.roots,
|
|
@@ -142,6 +141,31 @@ export async function waitForRecruitDetail(client, {
|
|
|
142
141
|
return lastState;
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
async function findVisibleDetailTarget(client, roots, selectors) {
|
|
145
|
+
for (const root of roots) {
|
|
146
|
+
if (!root?.nodeId) continue;
|
|
147
|
+
for (const selector of selectors) {
|
|
148
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
149
|
+
for (const nodeId of nodeIds) {
|
|
150
|
+
try {
|
|
151
|
+
const box = await getNodeBox(client, nodeId);
|
|
152
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
153
|
+
return {
|
|
154
|
+
root: root.name,
|
|
155
|
+
root_node_id: root.nodeId,
|
|
156
|
+
selector,
|
|
157
|
+
node_id: nodeId,
|
|
158
|
+
center: box.center,
|
|
159
|
+
rect: box.rect
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
145
169
|
export async function readRecruitDetailHtml(client, detailState) {
|
|
146
170
|
let popupHTML = "";
|
|
147
171
|
let resumeHTML = "";
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
measureTiming
|
|
6
6
|
} from "../../core/run/timing.js";
|
|
7
7
|
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
8
|
+
import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
|
|
8
9
|
import {
|
|
9
10
|
compactCvAcquisitionState,
|
|
10
11
|
countParsedNetworkProfiles,
|
|
11
12
|
createCvAcquisitionState,
|
|
13
|
+
DEFAULT_MAX_IMAGE_PAGES,
|
|
12
14
|
getCvNetworkWaitPlan,
|
|
13
15
|
recordCvImageFallback,
|
|
14
16
|
recordCvNetworkHit,
|
|
@@ -148,7 +150,7 @@ export async function runRecruitWorkflow({
|
|
|
148
150
|
resetBeforeSearch = true,
|
|
149
151
|
resetTimeoutMs = 180000,
|
|
150
152
|
cityOptionTimeoutMs = 30000,
|
|
151
|
-
maxImagePages =
|
|
153
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
152
154
|
imageWheelDeltaY = 650,
|
|
153
155
|
cvAcquisitionMode = "unknown",
|
|
154
156
|
listMaxScrolls = 20,
|
|
@@ -434,15 +436,21 @@ export async function runRecruitWorkflow({
|
|
|
434
436
|
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
435
437
|
let source = "network";
|
|
436
438
|
let imageEvidence = null;
|
|
439
|
+
let captureTarget = null;
|
|
440
|
+
let captureTargetWait = null;
|
|
437
441
|
if (parsedNetworkProfileCount > 0) {
|
|
438
442
|
recordCvNetworkHit(cvAcquisitionState, {
|
|
439
443
|
parsedNetworkProfileCount,
|
|
440
444
|
waitResult: networkWait
|
|
441
445
|
});
|
|
442
446
|
} else {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
447
|
+
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
448
|
+
domain: "recruit",
|
|
449
|
+
timeoutMs: 6000,
|
|
450
|
+
intervalMs: 250
|
|
451
|
+
});
|
|
452
|
+
captureTarget = captureTargetWait.target || null;
|
|
453
|
+
const captureNodeId = captureTarget?.node_id || null;
|
|
446
454
|
if (captureNodeId) {
|
|
447
455
|
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
448
456
|
filePath: imageEvidenceFilePath({
|
|
@@ -456,8 +464,8 @@ export async function runRecruitWorkflow({
|
|
|
456
464
|
quality: 72,
|
|
457
465
|
optimize: true,
|
|
458
466
|
resizeMaxWidth: 1100,
|
|
459
|
-
captureViewport:
|
|
460
|
-
padding:
|
|
467
|
+
captureViewport: false,
|
|
468
|
+
padding: 0,
|
|
461
469
|
maxScreenshots: maxImagePages,
|
|
462
470
|
wheelDeltaY: imageWheelDeltaY,
|
|
463
471
|
settleMs: 350,
|
|
@@ -475,7 +483,9 @@ export async function runRecruitWorkflow({
|
|
|
475
483
|
capture_mode: "scroll_sequence",
|
|
476
484
|
acquisition_reason: "network_miss_image_fallback",
|
|
477
485
|
run_candidate_index: index,
|
|
478
|
-
candidate_key: candidateKey
|
|
486
|
+
candidate_key: candidateKey,
|
|
487
|
+
capture_target: captureTarget,
|
|
488
|
+
capture_target_wait: captureTargetWait
|
|
479
489
|
}
|
|
480
490
|
}));
|
|
481
491
|
source = "image";
|
|
@@ -506,7 +516,9 @@ export async function runRecruitWorkflow({
|
|
|
506
516
|
wait_plan: waitPlan,
|
|
507
517
|
network_wait: networkWait,
|
|
508
518
|
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
509
|
-
image_evidence: summarizeImageEvidence(imageEvidence)
|
|
519
|
+
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
520
|
+
capture_target: captureTarget || null,
|
|
521
|
+
capture_target_wait: captureTargetWait
|
|
510
522
|
};
|
|
511
523
|
screeningCandidate = detailResult.candidate;
|
|
512
524
|
}
|
|
@@ -648,7 +660,7 @@ export function createRecruitRunService({
|
|
|
648
660
|
resetBeforeSearch = true,
|
|
649
661
|
resetTimeoutMs = 180000,
|
|
650
662
|
cityOptionTimeoutMs = 30000,
|
|
651
|
-
maxImagePages =
|
|
663
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
652
664
|
imageWheelDeltaY = 650,
|
|
653
665
|
cvAcquisitionMode = "unknown",
|
|
654
666
|
listMaxScrolls = 20,
|
package/src/index.js
CHANGED
|
@@ -756,7 +756,7 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
|
|
|
756
756
|
max_image_pages: {
|
|
757
757
|
type: "integer",
|
|
758
758
|
minimum: 1,
|
|
759
|
-
description: "可选,图片简历 fallback
|
|
759
|
+
description: "可选,图片简历 fallback 的滚动截图页数上限,默认 24"
|
|
760
760
|
},
|
|
761
761
|
list_max_scrolls: {
|
|
762
762
|
type: "integer",
|
package/src/recommend-mcp.js
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
resolveBossConfiguredOutputDir,
|
|
49
49
|
resolveBossScreeningConfig
|
|
50
50
|
} from "./chat-runtime-config.js";
|
|
51
|
+
import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
|
|
51
52
|
|
|
52
53
|
const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
|
|
53
54
|
const DEFAULT_RECOMMEND_PORT = 9222;
|
|
@@ -1114,7 +1115,7 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
|
|
|
1114
1115
|
closeDetail: true,
|
|
1115
1116
|
delayMs: parseNonNegativeInteger(args.delay_ms, 0),
|
|
1116
1117
|
cardTimeoutMs: slowLive ? 180000 : 90000,
|
|
1117
|
-
maxImagePages: parsePositiveInteger(args.max_image_pages,
|
|
1118
|
+
maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
|
|
1118
1119
|
imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
|
|
1119
1120
|
cvAcquisitionMode: normalizeText(args.cv_acquisition_mode) || "unknown",
|
|
1120
1121
|
listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 20),
|
package/src/recruit-mcp.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
resolveBossConfiguredOutputDir,
|
|
37
37
|
resolveBossScreeningConfig
|
|
38
38
|
} from "./chat-runtime-config.js";
|
|
39
|
+
import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
|
|
39
40
|
|
|
40
41
|
const RUN_MODE_ASYNC = "async";
|
|
41
42
|
const RUN_MODE_SYNC = "sync";
|
|
@@ -855,6 +856,7 @@ function getRunOptions(args, parsed, session, configResolution = null) {
|
|
|
855
856
|
resetBeforeSearch: args.reset_search !== false,
|
|
856
857
|
resetTimeoutMs: slowLive ? 300000 : 180000,
|
|
857
858
|
cityOptionTimeoutMs: slowLive ? 60000 : 30000,
|
|
859
|
+
maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
|
|
858
860
|
screeningMode,
|
|
859
861
|
llmConfig: screeningMode === "llm" && configResolution?.ok ? {
|
|
860
862
|
...configResolution.config
|