@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.1

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