@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -33
- package/package.json +61 -9
- package/skills/boss-recommend-pipeline/SKILL.md +4 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1095 -196
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +67 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +68 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
|
@@ -1,817 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const fs = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
const http = require("node:http");
|
|
5
|
-
const { spawnSync } = require("node:child_process");
|
|
6
|
-
const WebSocket = require("ws");
|
|
7
|
-
let sharpFactory = null;
|
|
8
|
-
|
|
9
|
-
function sleep(ms) {
|
|
10
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function shouldBringChromeToFront() {
|
|
14
|
-
const envValue = String(process.env.BOSS_RECOMMEND_BRING_TO_FRONT || "").trim().toLowerCase();
|
|
15
|
-
if (envValue) {
|
|
16
|
-
if (["1", "true", "yes", "y", "on"].includes(envValue)) return true;
|
|
17
|
-
if (["0", "false", "no", "n", "off"].includes(envValue)) return false;
|
|
18
|
-
}
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const SHOULD_BRING_TO_FRONT = shouldBringChromeToFront();
|
|
23
|
-
|
|
24
|
-
const EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS = 5000;
|
|
25
|
-
const EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS = 4;
|
|
26
|
-
const RESUME_VIEWPORT_STABILITY_POLL_MS = 80;
|
|
27
|
-
const RESUME_VIEWPORT_STABLE_POLLS = 2;
|
|
28
|
-
|
|
29
|
-
function clampInteger(value, low, high) {
|
|
30
|
-
return Math.max(low, Math.min(high, value));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function loadSharp() {
|
|
34
|
-
if (!sharpFactory) {
|
|
35
|
-
sharpFactory = require("sharp");
|
|
36
|
-
}
|
|
37
|
-
return sharpFactory;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getJson(url) {
|
|
41
|
-
return new Promise((resolve, reject) => {
|
|
42
|
-
http
|
|
43
|
-
.get(url, (res) => {
|
|
44
|
-
let data = "";
|
|
45
|
-
res.on("data", (chunk) => {
|
|
46
|
-
data += chunk;
|
|
47
|
-
});
|
|
48
|
-
res.on("end", () => {
|
|
49
|
-
try {
|
|
50
|
-
resolve(JSON.parse(data));
|
|
51
|
-
} catch (error) {
|
|
52
|
-
reject(new Error(`Parse JSON failed for ${url}: ${error.message}`));
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
})
|
|
56
|
-
.on("error", reject);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function pickTarget(targets, targetPattern) {
|
|
61
|
-
const pages = targets.filter((item) => item.type === "page");
|
|
62
|
-
if (!pages.length) return null;
|
|
63
|
-
return (
|
|
64
|
-
pages.find((item) => typeof item.url === "string" && item.url.includes(targetPattern))
|
|
65
|
-
|| pages.find((item) => /zhipin\.com/i.test(item.url || ""))
|
|
66
|
-
|| pages[0]
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function oneLineJson(value, maxLength = 1200) {
|
|
71
|
-
try {
|
|
72
|
-
const text = JSON.stringify(value);
|
|
73
|
-
if (text.length <= maxLength) return text;
|
|
74
|
-
return `${text.slice(0, maxLength)}...`;
|
|
75
|
-
} catch {
|
|
76
|
-
return "\"<unserializable>\"";
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function summarizeProbeReason(probe) {
|
|
81
|
-
if (!probe || typeof probe !== "object") return "NO_PROBE";
|
|
82
|
-
if (probe.ok === true) return "INVALID_CLIP";
|
|
83
|
-
return String(probe.reason || "UNKNOWN");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildResumeProbeTimeoutMessage(waitResumeMs, probe) {
|
|
87
|
-
const reason = summarizeProbeReason(probe);
|
|
88
|
-
const payload = {
|
|
89
|
-
reason,
|
|
90
|
-
clip: probe?.clip || null,
|
|
91
|
-
scroll_top: Number.isFinite(Number(probe?.scrollTop)) ? Number(probe.scrollTop) : null,
|
|
92
|
-
client_height: Number.isFinite(Number(probe?.clientHeight)) ? Number(probe.clientHeight) : null,
|
|
93
|
-
scroll_height: Number.isFinite(Number(probe?.scrollHeight)) ? Number(probe.scrollHeight) : null,
|
|
94
|
-
max_scroll: Number.isFinite(Number(probe?.maxScroll)) ? Number(probe.maxScroll) : null,
|
|
95
|
-
debug: probe?.debug || null
|
|
96
|
-
};
|
|
97
|
-
return `Resume canvas not found: wait_resume_ms=${waitResumeMs}; last_reason=${reason}; probe=${oneLineJson(payload)}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function isStableNoResumeIframeProbe(probe) {
|
|
101
|
-
if (!probe || probe.ok === true || probe.reason !== "NO_CRESUME_IFRAME") {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
const activeScopeCount = Number(probe?.debug?.activeScopeCount ?? -1);
|
|
105
|
-
const totalResumeIframes = Number(probe?.debug?.totalResumeIframes ?? -1);
|
|
106
|
-
const visibleResumeIframes = Number(probe?.debug?.visibleResumeIframes ?? -1);
|
|
107
|
-
return activeScopeCount === 0 && totalResumeIframes === 0 && visibleResumeIframes === 0;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function shouldAbortResumeProbeEarly({ probe, stableNoResumeIframePolls, elapsedMs, waitResumeMs }) {
|
|
111
|
-
if (!isStableNoResumeIframeProbe(probe)) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
const minWaitMs = Math.min(waitResumeMs, EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS);
|
|
115
|
-
return stableNoResumeIframePolls >= EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS
|
|
116
|
-
&& elapsedMs >= minWaitMs;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function parseBooleanOption(value, fallback = false) {
|
|
120
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
121
|
-
if (typeof value === "boolean") return value;
|
|
122
|
-
const normalized = String(value).trim().toLowerCase();
|
|
123
|
-
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
124
|
-
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
125
|
-
return fallback;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function numbersClose(left, right, tolerance = 1) {
|
|
129
|
-
return Math.abs(Number(left || 0) - Number(right || 0)) <= tolerance;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function isStableResumeViewport(previous, current, targetScroll) {
|
|
133
|
-
if (!previous?.ok || !current?.ok) return false;
|
|
134
|
-
const target = Number(targetScroll || 0);
|
|
135
|
-
const scrollTop = Number(current.scrollTop || 0);
|
|
136
|
-
const maxScroll = Number(current.maxScroll || 0);
|
|
137
|
-
const targetReached = numbersClose(scrollTop, target, 2)
|
|
138
|
-
|| (target >= maxScroll && numbersClose(scrollTop, maxScroll, 2));
|
|
139
|
-
if (!targetReached) return false;
|
|
140
|
-
const prevClip = previous.clip || {};
|
|
141
|
-
const currentClip = current.clip || {};
|
|
142
|
-
return numbersClose(previous.scrollTop, current.scrollTop, 1)
|
|
143
|
-
&& numbersClose(previous.scrollHeight, current.scrollHeight, 1)
|
|
144
|
-
&& numbersClose(previous.clientHeight, current.clientHeight, 1)
|
|
145
|
-
&& numbersClose(prevClip.x, currentClip.x, 1)
|
|
146
|
-
&& numbersClose(prevClip.y, currentClip.y, 1)
|
|
147
|
-
&& numbersClose(prevClip.width, currentClip.width, 1)
|
|
148
|
-
&& numbersClose(prevClip.height, currentClip.height, 1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function waitForStableResumeViewport(evaluate, targetScroll, maxWaitMs) {
|
|
152
|
-
const maxWait = Math.max(160, Number(maxWaitMs || 0));
|
|
153
|
-
const start = Date.now();
|
|
154
|
-
let previous = null;
|
|
155
|
-
let latest = null;
|
|
156
|
-
let stablePolls = 0;
|
|
157
|
-
while (Date.now() - start < maxWait) {
|
|
158
|
-
await sleep(RESUME_VIEWPORT_STABILITY_POLL_MS);
|
|
159
|
-
const current = await evaluate(buildResumeProbeExpr({ init: false, targetScroll: null }));
|
|
160
|
-
if (current?.ok) {
|
|
161
|
-
latest = current;
|
|
162
|
-
if (isStableResumeViewport(previous, current, targetScroll)) {
|
|
163
|
-
stablePolls += 1;
|
|
164
|
-
if (stablePolls >= RESUME_VIEWPORT_STABLE_POLLS) {
|
|
165
|
-
return current;
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
stablePolls = 0;
|
|
169
|
-
}
|
|
170
|
-
previous = current;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return latest;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function stitchWithSharp(metadataFile, stitchedImage) {
|
|
177
|
-
const sharp = loadSharp();
|
|
178
|
-
let metadata;
|
|
179
|
-
try {
|
|
180
|
-
metadata = JSON.parse(fs.readFileSync(metadataFile, "utf8"));
|
|
181
|
-
} catch (error) {
|
|
182
|
-
throw new Error(`Invalid stitch metadata: ${error.message || error}`);
|
|
183
|
-
}
|
|
184
|
-
const rawChunks = Array.isArray(metadata?.chunks) ? metadata.chunks : [];
|
|
185
|
-
if (rawChunks.length === 0) {
|
|
186
|
-
throw new Error("No chunks found in metadata.");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const chunks = rawChunks.map((chunk, index) => {
|
|
190
|
-
const file = path.resolve(String(chunk?.file || ""));
|
|
191
|
-
if (!file || !fs.existsSync(file)) {
|
|
192
|
-
throw new Error(`Chunk image missing: ${file || "<empty>"}`);
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
index: Number.isInteger(chunk?.index) ? chunk.index : index,
|
|
196
|
-
file,
|
|
197
|
-
scrollTop: Number(chunk?.scrollTop || 0),
|
|
198
|
-
clipHeightCss: Number(chunk?.clipHeightCss || 0)
|
|
199
|
-
};
|
|
200
|
-
}).sort((a, b) => {
|
|
201
|
-
if (a.scrollTop !== b.scrollTop) return a.scrollTop - b.scrollTop;
|
|
202
|
-
return a.index - b.index;
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const composites = [];
|
|
206
|
-
const used = [];
|
|
207
|
-
let outWidth = 1;
|
|
208
|
-
let outHeight = 0;
|
|
209
|
-
let prevChunk = null;
|
|
210
|
-
|
|
211
|
-
for (const chunk of chunks) {
|
|
212
|
-
const info = await sharp(chunk.file).metadata();
|
|
213
|
-
const width = Number(info?.width || 0);
|
|
214
|
-
const height = Number(info?.height || 0);
|
|
215
|
-
if (width <= 0 || height <= 0) {
|
|
216
|
-
throw new Error(`Invalid chunk image size: ${chunk.file}`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (prevChunk) {
|
|
220
|
-
const deltaCss = chunk.scrollTop - prevChunk.scrollTop;
|
|
221
|
-
if (!(deltaCss > 0.5)) {
|
|
222
|
-
prevChunk = chunk;
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
const clipHeightCss = chunk.clipHeightCss > 1 ? chunk.clipHeightCss : prevChunk.clipHeightCss;
|
|
226
|
-
const ratio = clipHeightCss > 1 ? (height / clipHeightCss) : 1;
|
|
227
|
-
const newPixels = clampInteger(Math.round(deltaCss * ratio), 1, height);
|
|
228
|
-
const cropTop = clampInteger(height - newPixels, 0, height - 1);
|
|
229
|
-
const segHeight = height - cropTop;
|
|
230
|
-
const segment = await sharp(chunk.file)
|
|
231
|
-
.removeAlpha()
|
|
232
|
-
.extract({
|
|
233
|
-
left: 0,
|
|
234
|
-
top: cropTop,
|
|
235
|
-
width,
|
|
236
|
-
height: segHeight
|
|
237
|
-
})
|
|
238
|
-
.png()
|
|
239
|
-
.toBuffer();
|
|
240
|
-
composites.push({
|
|
241
|
-
input: segment,
|
|
242
|
-
top: outHeight,
|
|
243
|
-
left: 0
|
|
244
|
-
});
|
|
245
|
-
used.push({
|
|
246
|
-
file: chunk.file,
|
|
247
|
-
scrollTop: chunk.scrollTop,
|
|
248
|
-
cropTopPx: cropTop,
|
|
249
|
-
keptHeightPx: segHeight
|
|
250
|
-
});
|
|
251
|
-
outWidth = Math.max(outWidth, width);
|
|
252
|
-
outHeight += segHeight;
|
|
253
|
-
prevChunk = chunk;
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const segment = await sharp(chunk.file)
|
|
258
|
-
.removeAlpha()
|
|
259
|
-
.png()
|
|
260
|
-
.toBuffer();
|
|
261
|
-
composites.push({
|
|
262
|
-
input: segment,
|
|
263
|
-
top: outHeight,
|
|
264
|
-
left: 0
|
|
265
|
-
});
|
|
266
|
-
used.push({
|
|
267
|
-
file: chunk.file,
|
|
268
|
-
scrollTop: chunk.scrollTop,
|
|
269
|
-
cropTopPx: 0,
|
|
270
|
-
keptHeightPx: height
|
|
271
|
-
});
|
|
272
|
-
outWidth = Math.max(outWidth, width);
|
|
273
|
-
outHeight += height;
|
|
274
|
-
prevChunk = chunk;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (composites.length === 0 || outHeight <= 0 || outWidth <= 0) {
|
|
278
|
-
throw new Error("No valid segments to stitch with sharp.");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
await sharp({
|
|
282
|
-
create: {
|
|
283
|
-
width: outWidth,
|
|
284
|
-
height: outHeight,
|
|
285
|
-
channels: 3,
|
|
286
|
-
background: { r: 255, g: 255, b: 255 }
|
|
287
|
-
}
|
|
288
|
-
})
|
|
289
|
-
.composite(composites)
|
|
290
|
-
.png()
|
|
291
|
-
.toFile(stitchedImage);
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
ok: true,
|
|
295
|
-
engine: "sharp",
|
|
296
|
-
output: path.resolve(stitchedImage),
|
|
297
|
-
segments: composites.length,
|
|
298
|
-
size: {
|
|
299
|
-
width: outWidth,
|
|
300
|
-
height: outHeight
|
|
301
|
-
},
|
|
302
|
-
used
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function stitchWithAvailablePython(stitchScript, metadataFile, stitchedImage, spawnSyncImpl = spawnSync) {
|
|
307
|
-
const candidates = ["python3", "python"];
|
|
308
|
-
const attempts = [];
|
|
309
|
-
if (!fs.existsSync(stitchScript)) {
|
|
310
|
-
return {
|
|
311
|
-
ok: false,
|
|
312
|
-
attempts: candidates.map((command) => ({
|
|
313
|
-
command,
|
|
314
|
-
status: null,
|
|
315
|
-
signal: null,
|
|
316
|
-
error: `Missing stitch script: ${stitchScript}`,
|
|
317
|
-
stderr: "",
|
|
318
|
-
stdout: ""
|
|
319
|
-
}))
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
for (const command of candidates) {
|
|
323
|
-
const result = spawnSyncImpl(command, [stitchScript, metadataFile, stitchedImage], {
|
|
324
|
-
encoding: "utf8"
|
|
325
|
-
});
|
|
326
|
-
attempts.push({
|
|
327
|
-
command,
|
|
328
|
-
status: Number.isInteger(result.status) ? result.status : null,
|
|
329
|
-
signal: result.signal || null,
|
|
330
|
-
error: result.error ? String(result.error.message || result.error) : null,
|
|
331
|
-
stderr: result.stderr || "",
|
|
332
|
-
stdout: result.stdout || ""
|
|
333
|
-
});
|
|
334
|
-
if (result.status === 0) {
|
|
335
|
-
return {
|
|
336
|
-
ok: true,
|
|
337
|
-
engine: "python",
|
|
338
|
-
command,
|
|
339
|
-
result,
|
|
340
|
-
attempts
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return {
|
|
345
|
-
ok: false,
|
|
346
|
-
attempts
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function buildResumeProbeExpr({ init, targetScroll }) {
|
|
351
|
-
const initLiteral = init ? "true" : "false";
|
|
352
|
-
const scrollLiteral = typeof targetScroll === "number" && Number.isFinite(targetScroll)
|
|
353
|
-
? String(targetScroll)
|
|
354
|
-
: "null";
|
|
355
|
-
|
|
356
|
-
return `(() => {
|
|
357
|
-
const INIT = ${initLiteral};
|
|
358
|
-
const TARGET_SCROLL = ${scrollLiteral};
|
|
359
|
-
|
|
360
|
-
function absRect(el) {
|
|
361
|
-
const rect = el.getBoundingClientRect();
|
|
362
|
-
let x = rect.left;
|
|
363
|
-
let y = rect.top;
|
|
364
|
-
let win = el.ownerDocument.defaultView;
|
|
365
|
-
while (win && win !== win.parent) {
|
|
366
|
-
const frameEl = win.frameElement;
|
|
367
|
-
if (!frameEl) break;
|
|
368
|
-
const frameRect = frameEl.getBoundingClientRect();
|
|
369
|
-
x += frameRect.left;
|
|
370
|
-
y += frameRect.top;
|
|
371
|
-
win = win.parent;
|
|
372
|
-
}
|
|
373
|
-
return { x, y, width: rect.width, height: rect.height };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function canScroll(el) {
|
|
377
|
-
return Boolean(el && (el.scrollHeight || 0) > (el.clientHeight || 0) + 8);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function chooseScrollableAncestor(startEl) {
|
|
381
|
-
const candidates = [];
|
|
382
|
-
let cur = startEl;
|
|
383
|
-
let depth = 0;
|
|
384
|
-
while (cur && depth < 24) {
|
|
385
|
-
if ((cur.clientHeight || 0) > 40 && canScroll(cur)) {
|
|
386
|
-
const style = getComputedStyle(cur);
|
|
387
|
-
const overflowY = String(style.overflowY || '').toLowerCase();
|
|
388
|
-
const key = (((cur.id || '') + ' ' + (cur.className || '')).toLowerCase());
|
|
389
|
-
let score = 0;
|
|
390
|
-
if (/auto|scroll|overlay/.test(overflowY)) score += 1000;
|
|
391
|
-
if (key.includes('resume')) score += 600;
|
|
392
|
-
if (key.includes('detail')) score += 300;
|
|
393
|
-
score += Math.min(250, Math.floor((cur.scrollHeight - cur.clientHeight) / 2));
|
|
394
|
-
score -= depth * 10;
|
|
395
|
-
candidates.push({ el: cur, score });
|
|
396
|
-
}
|
|
397
|
-
cur = cur.parentElement;
|
|
398
|
-
depth += 1;
|
|
399
|
-
}
|
|
400
|
-
if (!candidates.length) return null;
|
|
401
|
-
candidates.sort((a, b) => b.score - a.score);
|
|
402
|
-
return candidates[0].el;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function isVisible(el) {
|
|
406
|
-
if (!el) return false;
|
|
407
|
-
const style = getComputedStyle(el);
|
|
408
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
const rect = el.getBoundingClientRect();
|
|
412
|
-
return rect.width > 80 && rect.height > 80;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function locateContext() {
|
|
416
|
-
const recommendFrame = document.querySelector('iframe[name="recommendFrame"]')
|
|
417
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
418
|
-
|| document.querySelector('iframe');
|
|
419
|
-
const recommendDoc = recommendFrame && recommendFrame.contentDocument;
|
|
420
|
-
if (!recommendFrame || !recommendDoc) {
|
|
421
|
-
return {
|
|
422
|
-
ok: false,
|
|
423
|
-
reason: 'NO_RECOMMEND_IFRAME',
|
|
424
|
-
debug: {
|
|
425
|
-
topIframeCount: document.querySelectorAll('iframe').length
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const scopes = Array.from(
|
|
431
|
-
recommendDoc.querySelectorAll('.dialog-wrap.active, .boss-popup__wrapper.boss-dialog, .boss-dialog__wrapper')
|
|
432
|
-
).filter(isVisible);
|
|
433
|
-
const allResumeFrames = Array.from(
|
|
434
|
-
recommendDoc.querySelectorAll('iframe[src*="/web/frame/c-resume/"], iframe[name*="resume"]')
|
|
435
|
-
);
|
|
436
|
-
const visibleResumeFrames = allResumeFrames.filter(isVisible);
|
|
437
|
-
|
|
438
|
-
let resumeFrame = null;
|
|
439
|
-
for (const scope of scopes) {
|
|
440
|
-
const found = scope.querySelector('iframe[src*="/web/frame/c-resume/"], iframe[name*="resume"]');
|
|
441
|
-
if (found && isVisible(found)) {
|
|
442
|
-
resumeFrame = found;
|
|
443
|
-
break;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (!resumeFrame) {
|
|
448
|
-
resumeFrame = visibleResumeFrames[0] || null;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (!resumeFrame) {
|
|
452
|
-
return {
|
|
453
|
-
ok: false,
|
|
454
|
-
reason: 'NO_CRESUME_IFRAME',
|
|
455
|
-
debug: {
|
|
456
|
-
activeScopeCount: scopes.length,
|
|
457
|
-
totalResumeIframes: allResumeFrames.length,
|
|
458
|
-
visibleResumeIframes: visibleResumeFrames.length,
|
|
459
|
-
recommendFrameUrl: (() => {
|
|
460
|
-
try { return String(recommendFrame.contentWindow.location.href || ''); } catch { return ''; }
|
|
461
|
-
})()
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const resumeDoc = resumeFrame.contentDocument;
|
|
467
|
-
const canvas = resumeDoc ? (resumeDoc.querySelector('canvas#resume') || resumeDoc.querySelector('canvas')) : null;
|
|
468
|
-
const scroller = chooseScrollableAncestor(resumeFrame.parentElement || resumeFrame)
|
|
469
|
-
|| recommendDoc.querySelector('.resume-detail-wrap')
|
|
470
|
-
|| chooseScrollableAncestor(resumeFrame)
|
|
471
|
-
|| null;
|
|
472
|
-
if (!scroller || !isVisible(scroller)) {
|
|
473
|
-
return {
|
|
474
|
-
ok: false,
|
|
475
|
-
reason: 'NO_SCROLL_CONTAINER',
|
|
476
|
-
debug: {
|
|
477
|
-
activeScopeCount: scopes.length,
|
|
478
|
-
totalResumeIframes: allResumeFrames.length,
|
|
479
|
-
visibleResumeIframes: visibleResumeFrames.length,
|
|
480
|
-
resumeFrameSrc: String(resumeFrame.src || ''),
|
|
481
|
-
scrollerFound: Boolean(scroller),
|
|
482
|
-
scrollerVisible: Boolean(scroller && isVisible(scroller)),
|
|
483
|
-
scrollerClass: scroller ? String(scroller.className || '') : ''
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return {
|
|
489
|
-
ok: true,
|
|
490
|
-
frame: resumeFrame,
|
|
491
|
-
canvas,
|
|
492
|
-
scroller,
|
|
493
|
-
clipEl: scroller,
|
|
494
|
-
debug: {
|
|
495
|
-
recommendFrameUrl: (() => {
|
|
496
|
-
try { return String(recommendFrame.contentWindow.location.href || ''); } catch { return ''; }
|
|
497
|
-
})(),
|
|
498
|
-
resumeFrameSrc: String(resumeFrame.src || ''),
|
|
499
|
-
scrollerClass: String(scroller.className || '')
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (INIT || !window.__bossRecommendResumeCtx || !window.__bossRecommendResumeCtx.scroller || !window.__bossRecommendResumeCtx.scroller.isConnected) {
|
|
505
|
-
const located = locateContext();
|
|
506
|
-
if (!located.ok) {
|
|
507
|
-
return located;
|
|
508
|
-
}
|
|
509
|
-
window.__bossRecommendResumeCtx = located;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const ctx = window.__bossRecommendResumeCtx;
|
|
513
|
-
if (typeof TARGET_SCROLL === 'number' && Number.isFinite(TARGET_SCROLL)) {
|
|
514
|
-
try {
|
|
515
|
-
ctx.scroller.scrollTop = TARGET_SCROLL;
|
|
516
|
-
if (typeof ctx.scroller.scrollTo === 'function') {
|
|
517
|
-
ctx.scroller.scrollTo({ top: TARGET_SCROLL, left: 0, behavior: 'instant' });
|
|
518
|
-
}
|
|
519
|
-
ctx.scroller.dispatchEvent(new Event('scroll', { bubbles: true }));
|
|
520
|
-
} catch {}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const scrollTop = Number(ctx.scroller.scrollTop || 0);
|
|
524
|
-
const scrollHeight = Number(ctx.scroller.scrollHeight || 0);
|
|
525
|
-
const clientHeight = Number(ctx.scroller.clientHeight || 0);
|
|
526
|
-
const maxScroll = Math.max(0, scrollHeight - clientHeight);
|
|
527
|
-
const clipRaw = absRect(ctx.clipEl);
|
|
528
|
-
|
|
529
|
-
return {
|
|
530
|
-
ok: true,
|
|
531
|
-
scrollTop,
|
|
532
|
-
scrollHeight,
|
|
533
|
-
clientHeight,
|
|
534
|
-
maxScroll,
|
|
535
|
-
clip: {
|
|
536
|
-
x: clipRaw.x,
|
|
537
|
-
y: clipRaw.y,
|
|
538
|
-
width: Math.max(1, Math.min(clipRaw.width, Number(ctx.scroller.clientWidth || clipRaw.width))),
|
|
539
|
-
height: Math.max(1, Math.min(clipRaw.height, Number(ctx.scroller.clientHeight || clipRaw.height)))
|
|
540
|
-
},
|
|
541
|
-
canvas: ctx.canvas ? {
|
|
542
|
-
width: Number(ctx.canvas.width || 0),
|
|
543
|
-
height: Number(ctx.canvas.height || 0)
|
|
544
|
-
} : null,
|
|
545
|
-
debug: ctx.debug || {}
|
|
546
|
-
};
|
|
547
|
-
})()`;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async function captureFullResumeCanvas(options = {}) {
|
|
551
|
-
const host = options.host || process.env.CDP_HOST || "127.0.0.1";
|
|
552
|
-
const port = Number(options.port || process.env.CDP_PORT || 9222);
|
|
553
|
-
const waitResumeMs = Number(options.waitResumeMs || process.env.WAIT_RESUME_MS || 30000);
|
|
554
|
-
const scrollSettleMs = Number(options.scrollSettleMs || process.env.SCROLL_SETTLE_MS || 500);
|
|
555
|
-
const stitchFullImage = parseBooleanOption(
|
|
556
|
-
options.stitchFullImage,
|
|
557
|
-
parseBooleanOption(process.env.BOSS_RECOMMEND_STITCH_FULL_IMAGE, true)
|
|
558
|
-
);
|
|
559
|
-
const outPrefix = options.outPrefix || process.env.OUT_PREFIX || path.resolve(process.cwd(), "recommend_resume_full");
|
|
560
|
-
const targetPattern = options.targetPattern || process.env.TARGET_PATTERN || "/web/chat/recommend";
|
|
561
|
-
const stitchScript = path.resolve(__dirname, "stitch_resume_chunks.py");
|
|
562
|
-
const chunkDir = `${outPrefix}_chunks`;
|
|
563
|
-
const metadataFile = `${outPrefix}_chunks.json`;
|
|
564
|
-
const stitchedImage = `${outPrefix}.png`;
|
|
565
|
-
|
|
566
|
-
fs.mkdirSync(chunkDir, { recursive: true });
|
|
567
|
-
|
|
568
|
-
const targets = await getJson(`http://${host}:${port}/json/list`);
|
|
569
|
-
const target = pickTarget(targets, targetPattern);
|
|
570
|
-
if (!target?.webSocketDebuggerUrl) {
|
|
571
|
-
throw new Error("No debuggable zhipin page target found.");
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
575
|
-
let seq = 0;
|
|
576
|
-
const pending = new Map();
|
|
577
|
-
|
|
578
|
-
function send(method, params = {}) {
|
|
579
|
-
const id = ++seq;
|
|
580
|
-
ws.send(JSON.stringify({ id, method, params }));
|
|
581
|
-
return new Promise((resolve, reject) => {
|
|
582
|
-
pending.set(id, { resolve, reject, method });
|
|
583
|
-
setTimeout(() => {
|
|
584
|
-
if (!pending.has(id)) return;
|
|
585
|
-
pending.delete(id);
|
|
586
|
-
reject(new Error(`Timeout: ${method}`));
|
|
587
|
-
}, 30000);
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function evaluate(expression) {
|
|
592
|
-
return send("Runtime.evaluate", {
|
|
593
|
-
expression,
|
|
594
|
-
returnByValue: true,
|
|
595
|
-
awaitPromise: true
|
|
596
|
-
}).then((response) => response.result?.value);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
ws.on("message", (data) => {
|
|
600
|
-
let message;
|
|
601
|
-
try {
|
|
602
|
-
message = JSON.parse(String(data));
|
|
603
|
-
} catch {
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
if (!message.id) return;
|
|
607
|
-
const promise = pending.get(message.id);
|
|
608
|
-
if (!promise) return;
|
|
609
|
-
pending.delete(message.id);
|
|
610
|
-
if (message.error) {
|
|
611
|
-
promise.reject(new Error(JSON.stringify(message.error)));
|
|
612
|
-
} else {
|
|
613
|
-
promise.resolve(message.result);
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
await new Promise((resolve, reject) => {
|
|
618
|
-
ws.once("open", resolve);
|
|
619
|
-
ws.once("error", reject);
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
try {
|
|
623
|
-
await send("Page.enable");
|
|
624
|
-
await send("Runtime.enable");
|
|
625
|
-
if (SHOULD_BRING_TO_FRONT) {
|
|
626
|
-
await send("Page.bringToFront");
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
let probe = null;
|
|
630
|
-
let lastProbe = null;
|
|
631
|
-
let stableNoResumeIframePolls = 0;
|
|
632
|
-
const startTime = Date.now();
|
|
633
|
-
while (Date.now() - startTime < waitResumeMs) {
|
|
634
|
-
try {
|
|
635
|
-
probe = await evaluate(buildResumeProbeExpr({ init: true, targetScroll: 0 }));
|
|
636
|
-
} catch (error) {
|
|
637
|
-
probe = {
|
|
638
|
-
ok: false,
|
|
639
|
-
reason: "PROBE_EVALUATE_FAILED",
|
|
640
|
-
debug: {
|
|
641
|
-
message: String(error?.message || error || "unknown")
|
|
642
|
-
}
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
if (probe && typeof probe === "object") {
|
|
646
|
-
lastProbe = probe;
|
|
647
|
-
}
|
|
648
|
-
if (probe?.ok && probe.clip?.height > 80 && probe.clip?.width > 120) {
|
|
649
|
-
break;
|
|
650
|
-
}
|
|
651
|
-
if (isStableNoResumeIframeProbe(probe)) {
|
|
652
|
-
stableNoResumeIframePolls += 1;
|
|
653
|
-
} else {
|
|
654
|
-
stableNoResumeIframePolls = 0;
|
|
655
|
-
}
|
|
656
|
-
const elapsedMs = Date.now() - startTime;
|
|
657
|
-
if (shouldAbortResumeProbeEarly({
|
|
658
|
-
probe,
|
|
659
|
-
stableNoResumeIframePolls,
|
|
660
|
-
elapsedMs,
|
|
661
|
-
waitResumeMs
|
|
662
|
-
})) {
|
|
663
|
-
if (probe && typeof probe === "object") {
|
|
664
|
-
probe = {
|
|
665
|
-
...probe,
|
|
666
|
-
debug: {
|
|
667
|
-
...(probe.debug && typeof probe.debug === "object" ? probe.debug : {}),
|
|
668
|
-
earlyAbort: true,
|
|
669
|
-
stableNoResumeIframePolls,
|
|
670
|
-
elapsedMs
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
lastProbe = probe;
|
|
674
|
-
}
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
await sleep(700);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
if (!probe?.ok) {
|
|
681
|
-
const elapsedMs = Math.max(0, Date.now() - startTime);
|
|
682
|
-
throw new Error(buildResumeProbeTimeoutMessage(Math.min(waitResumeMs, elapsedMs), lastProbe || probe));
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const maxScroll = Math.max(0, Number(probe.maxScroll || 0));
|
|
686
|
-
const step = Math.max(120, Math.floor(Number(probe.clientHeight || probe.clip.height || 800)));
|
|
687
|
-
const positions = [];
|
|
688
|
-
for (let pos = 0; pos <= maxScroll; pos += step) {
|
|
689
|
-
positions.push(Math.min(pos, maxScroll));
|
|
690
|
-
}
|
|
691
|
-
if (!positions.length || positions[positions.length - 1] !== maxScroll) {
|
|
692
|
-
positions.push(maxScroll);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
const uniquePositions = [...new Set(positions.map((value) => Math.round(value)))].sort((a, b) => a - b);
|
|
696
|
-
const chunks = [];
|
|
697
|
-
const seenScroll = [];
|
|
698
|
-
|
|
699
|
-
for (let index = 0; index < uniquePositions.length; index += 1) {
|
|
700
|
-
const targetScroll = uniquePositions[index];
|
|
701
|
-
await evaluate(buildResumeProbeExpr({ init: false, targetScroll }));
|
|
702
|
-
const current = await waitForStableResumeViewport(evaluate, targetScroll, scrollSettleMs);
|
|
703
|
-
if (!current?.ok) continue;
|
|
704
|
-
|
|
705
|
-
const actualScroll = Number(current.scrollTop || 0);
|
|
706
|
-
if (seenScroll.some((value) => Math.abs(value - actualScroll) < 1)) {
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
const clip = current.clip || {};
|
|
711
|
-
const width = Number(clip.width || 0);
|
|
712
|
-
const height = Number(clip.height || 0);
|
|
713
|
-
if (width < 50 || height < 50) {
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const shot = await send("Page.captureScreenshot", {
|
|
718
|
-
format: "png",
|
|
719
|
-
captureBeyondViewport: true,
|
|
720
|
-
clip: {
|
|
721
|
-
x: Number(clip.x.toFixed(2)),
|
|
722
|
-
y: Number(clip.y.toFixed(2)),
|
|
723
|
-
width: Number(width.toFixed(2)),
|
|
724
|
-
height: Number(height.toFixed(2)),
|
|
725
|
-
scale: 1
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
const file = path.resolve(chunkDir, `chunk_${String(chunks.length).padStart(3, "0")}.png`);
|
|
730
|
-
fs.writeFileSync(file, Buffer.from(shot.data, "base64"));
|
|
731
|
-
seenScroll.push(actualScroll);
|
|
732
|
-
chunks.push({
|
|
733
|
-
index: chunks.length,
|
|
734
|
-
file,
|
|
735
|
-
scrollTop: actualScroll,
|
|
736
|
-
clipHeightCss: height,
|
|
737
|
-
clipWidthCss: width
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (!chunks.length) {
|
|
742
|
-
throw new Error("No screenshot chunks captured.");
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const metadata = {
|
|
746
|
-
createdAt: new Date().toISOString(),
|
|
747
|
-
target: { title: target.title, url: target.url },
|
|
748
|
-
probe,
|
|
749
|
-
chunks
|
|
750
|
-
};
|
|
751
|
-
fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2), "utf8");
|
|
752
|
-
|
|
753
|
-
let stitchEngine = "skipped";
|
|
754
|
-
if (stitchFullImage) {
|
|
755
|
-
stitchEngine = "sharp";
|
|
756
|
-
try {
|
|
757
|
-
await stitchWithSharp(metadataFile, stitchedImage);
|
|
758
|
-
} catch (sharpError) {
|
|
759
|
-
const fallback = stitchWithAvailablePython(stitchScript, metadataFile, stitchedImage);
|
|
760
|
-
if (!fallback.ok) {
|
|
761
|
-
const fallbackSummary = fallback.attempts
|
|
762
|
-
.map((item) => {
|
|
763
|
-
const message = item.stderr || item.stdout || item.error || "unknown error";
|
|
764
|
-
return `${item.command}(status=${item.status ?? "null"}): ${message}`;
|
|
765
|
-
})
|
|
766
|
-
.join(" | ");
|
|
767
|
-
throw new Error(
|
|
768
|
-
`Stitch failed (sharp + python fallback). sharp=${sharpError?.message || sharpError}; fallback=${fallbackSummary}`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
stitchEngine = fallback.command || "python";
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
return {
|
|
776
|
-
stitchedImage: stitchFullImage ? stitchedImage : "",
|
|
777
|
-
metadataFile,
|
|
778
|
-
chunkDir,
|
|
779
|
-
chunkCount: chunks.length,
|
|
780
|
-
chunkFiles: chunks.map((chunk) => chunk.file),
|
|
781
|
-
modelImagePaths: chunks.map((chunk) => chunk.file),
|
|
782
|
-
stitch_engine: stitchEngine,
|
|
783
|
-
target: {
|
|
784
|
-
title: target.title,
|
|
785
|
-
url: target.url
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
} finally {
|
|
789
|
-
try {
|
|
790
|
-
ws.close();
|
|
791
|
-
} catch {}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
module.exports = {
|
|
796
|
-
captureFullResumeCanvas,
|
|
797
|
-
__testables: {
|
|
798
|
-
EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS,
|
|
799
|
-
EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
|
|
800
|
-
isStableNoResumeIframeProbe,
|
|
801
|
-
isStableResumeViewport,
|
|
802
|
-
shouldAbortResumeProbeEarly,
|
|
803
|
-
stitchWithAvailablePython,
|
|
804
|
-
stitchWithSharp
|
|
805
|
-
}
|
|
806
|
-
};
|
|
807
|
-
|
|
808
|
-
if (require.main === module) {
|
|
809
|
-
captureFullResumeCanvas()
|
|
810
|
-
.then((result) => {
|
|
811
|
-
console.log(JSON.stringify(result, null, 2));
|
|
812
|
-
})
|
|
813
|
-
.catch((error) => {
|
|
814
|
-
console.error(String(error?.message || error));
|
|
815
|
-
process.exit(1);
|
|
816
|
-
});
|
|
817
|
-
}
|