@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -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
- }