@reconcrap/boss-recommend-mcp 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adapters.js +108 -22
- package/src/parser.js +4 -1
- package/src/test-adapters-runtime.js +54 -1
- package/src/test-parser.js +12 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +36 -11
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +83 -8
package/package.json
CHANGED
package/src/adapters.js
CHANGED
|
@@ -16,6 +16,7 @@ const screenConfigTemplateDefaults = {
|
|
|
16
16
|
apiKey: "replace-with-openai-api-key",
|
|
17
17
|
model: "gpt-4.1-mini"
|
|
18
18
|
};
|
|
19
|
+
const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
19
20
|
|
|
20
21
|
function getCodexHome() {
|
|
21
22
|
return process.env.CODEX_HOME
|
|
@@ -830,7 +831,39 @@ function parseJsonOutput(text) {
|
|
|
830
831
|
return null;
|
|
831
832
|
}
|
|
832
833
|
|
|
833
|
-
function
|
|
834
|
+
function createScreenProgressTracker(currentTracker = {}) {
|
|
835
|
+
const outcome = String(currentTracker.outcome || "").trim();
|
|
836
|
+
return {
|
|
837
|
+
candidate_index: Number.isInteger(currentTracker.candidate_index) ? currentTracker.candidate_index : null,
|
|
838
|
+
outcome: outcome === "pass" || outcome === "skip" ? outcome : null,
|
|
839
|
+
action_failed: currentTracker.action_failed === true
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function finalizeCandidateProgress(progress, tracker) {
|
|
844
|
+
if (!Number.isInteger(tracker.candidate_index)) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
let changed = false;
|
|
849
|
+
if (tracker.action_failed === true) {
|
|
850
|
+
progress.skipped += 1;
|
|
851
|
+
changed = true;
|
|
852
|
+
} else if (tracker.outcome === "pass") {
|
|
853
|
+
progress.passed += 1;
|
|
854
|
+
changed = true;
|
|
855
|
+
} else if (tracker.outcome === "skip") {
|
|
856
|
+
progress.skipped += 1;
|
|
857
|
+
changed = true;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
tracker.candidate_index = null;
|
|
861
|
+
tracker.outcome = null;
|
|
862
|
+
tracker.action_failed = false;
|
|
863
|
+
return changed;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function parseScreenProgressLine(line, currentProgress = {}, currentTracker = {}) {
|
|
834
867
|
const normalizedLine = String(line || "").replace(/\s+/g, " ").trim();
|
|
835
868
|
if (!normalizedLine) return null;
|
|
836
869
|
|
|
@@ -840,23 +873,56 @@ function parseScreenProgressLine(line, currentProgress = {}) {
|
|
|
840
873
|
skipped: Number.isInteger(currentProgress.skipped) ? currentProgress.skipped : 0,
|
|
841
874
|
greet_count: Number.isInteger(currentProgress.greet_count) ? currentProgress.greet_count : 0
|
|
842
875
|
};
|
|
876
|
+
const nextTracker = createScreenProgressTracker(currentTracker);
|
|
843
877
|
|
|
844
878
|
let changed = false;
|
|
845
879
|
const processedMatch = normalizedLine.match(/处理第\s*(\d+)\s*位候选人/u);
|
|
846
880
|
if (processedMatch) {
|
|
881
|
+
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
882
|
+
changed = true;
|
|
883
|
+
}
|
|
847
884
|
const processed = Number.parseInt(processedMatch[1], 10);
|
|
848
885
|
if (Number.isInteger(processed) && processed >= 0 && processed !== nextProgress.processed) {
|
|
849
886
|
nextProgress.processed = processed;
|
|
850
887
|
changed = true;
|
|
851
888
|
}
|
|
889
|
+
nextTracker.candidate_index = processed;
|
|
890
|
+
nextTracker.outcome = null;
|
|
891
|
+
nextTracker.action_failed = false;
|
|
852
892
|
}
|
|
853
893
|
|
|
854
894
|
if (/筛选结果:\s*通过/u.test(normalizedLine)) {
|
|
855
|
-
|
|
856
|
-
|
|
895
|
+
if (nextTracker.outcome !== "pass" || nextTracker.action_failed) {
|
|
896
|
+
changed = true;
|
|
897
|
+
}
|
|
898
|
+
nextTracker.outcome = "pass";
|
|
899
|
+
nextTracker.action_failed = false;
|
|
857
900
|
} else if (/筛选结果:\s*不通过/u.test(normalizedLine)) {
|
|
858
|
-
|
|
859
|
-
|
|
901
|
+
if (nextTracker.outcome !== "skip" || nextTracker.action_failed) {
|
|
902
|
+
changed = true;
|
|
903
|
+
}
|
|
904
|
+
nextTracker.outcome = "skip";
|
|
905
|
+
nextTracker.action_failed = false;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (/候选人处理失败\s*:/u.test(normalizedLine)) {
|
|
909
|
+
if (!nextTracker.action_failed) {
|
|
910
|
+
changed = true;
|
|
911
|
+
}
|
|
912
|
+
nextTracker.action_failed = true;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (/^\[关闭详情\].*成功/u.test(normalizedLine)) {
|
|
916
|
+
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
917
|
+
changed = true;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const finalStateLine = /Process timed out after|status"\s*:\s*"(?:COMPLETED|PAUSED|FAILED)"/iu.test(normalizedLine);
|
|
922
|
+
if (finalStateLine) {
|
|
923
|
+
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
924
|
+
changed = true;
|
|
925
|
+
}
|
|
860
926
|
}
|
|
861
927
|
|
|
862
928
|
const greetMatch = normalizedLine.match(/greet[_\s-]*count\s*[:=]\s*(\d+)/iu);
|
|
@@ -871,7 +937,34 @@ function parseScreenProgressLine(line, currentProgress = {}) {
|
|
|
871
937
|
if (!changed) return null;
|
|
872
938
|
return {
|
|
873
939
|
line: normalizedLine,
|
|
874
|
-
progress: nextProgress
|
|
940
|
+
progress: nextProgress,
|
|
941
|
+
tracker: nextTracker
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function resolveRecommendScreenTimeoutMs(runtime = null) {
|
|
946
|
+
const runtimeTimeoutMs = parsePositiveInteger(runtime?.timeoutMs);
|
|
947
|
+
const envTimeoutMs = parsePositiveInteger(process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS);
|
|
948
|
+
return runtimeTimeoutMs || envTimeoutMs || DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function buildRecommendScreenProcessError(result, screenTimeoutMs) {
|
|
952
|
+
if (result.code === 0) return null;
|
|
953
|
+
if (result.error_code === "TIMEOUT") {
|
|
954
|
+
return {
|
|
955
|
+
code: "TIMEOUT",
|
|
956
|
+
message: `推荐页筛选命令执行超时(${screenTimeoutMs}ms)。`
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (result.error_code === "ABORTED") {
|
|
960
|
+
return {
|
|
961
|
+
code: "PROCESS_ABORTED",
|
|
962
|
+
message: "推荐页筛选命令已取消。"
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
code: "RECOMMEND_SCREEN_FAILED",
|
|
967
|
+
message: "推荐页筛选命令执行失败。"
|
|
875
968
|
};
|
|
876
969
|
}
|
|
877
970
|
|
|
@@ -1683,21 +1776,24 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
|
|
|
1683
1776
|
skipped: 0,
|
|
1684
1777
|
greet_count: 0
|
|
1685
1778
|
};
|
|
1779
|
+
let inferredTracker = createScreenProgressTracker();
|
|
1780
|
+
const screenTimeoutMs = resolveRecommendScreenTimeoutMs(runtime);
|
|
1686
1781
|
|
|
1687
1782
|
const result = await runProcess({
|
|
1688
1783
|
command: "node",
|
|
1689
1784
|
args,
|
|
1690
1785
|
cwd: screenDir,
|
|
1691
|
-
timeoutMs:
|
|
1786
|
+
timeoutMs: screenTimeoutMs,
|
|
1692
1787
|
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
1693
1788
|
signal: runtime?.signal,
|
|
1694
1789
|
onOutput: (event) => {
|
|
1695
1790
|
safeInvokeCallback(runtime?.onOutput, event);
|
|
1696
1791
|
},
|
|
1697
1792
|
onLine: (event) => {
|
|
1698
|
-
const parsed = parseScreenProgressLine(event?.line, inferredProgress);
|
|
1793
|
+
const parsed = parseScreenProgressLine(event?.line, inferredProgress, inferredTracker);
|
|
1699
1794
|
if (!parsed) return;
|
|
1700
1795
|
inferredProgress = parsed.progress;
|
|
1796
|
+
inferredTracker = parsed.tracker;
|
|
1701
1797
|
safeInvokeCallback(runtime?.onProgress, {
|
|
1702
1798
|
...inferredProgress,
|
|
1703
1799
|
line: parsed.line
|
|
@@ -1731,24 +1827,14 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
|
|
|
1731
1827
|
stderr: result.stderr,
|
|
1732
1828
|
structured,
|
|
1733
1829
|
summary,
|
|
1734
|
-
error: structured?.error || missingOutputError || (
|
|
1735
|
-
result.code === 0
|
|
1736
|
-
? null
|
|
1737
|
-
: result.error_code === "ABORTED"
|
|
1738
|
-
? {
|
|
1739
|
-
code: "PROCESS_ABORTED",
|
|
1740
|
-
message: "推荐页筛选命令已取消。"
|
|
1741
|
-
}
|
|
1742
|
-
: {
|
|
1743
|
-
code: "RECOMMEND_SCREEN_FAILED",
|
|
1744
|
-
message: "推荐页筛选命令执行失败。"
|
|
1745
|
-
}
|
|
1746
|
-
)
|
|
1830
|
+
error: structured?.error || missingOutputError || buildRecommendScreenProcessError(result, screenTimeoutMs)
|
|
1747
1831
|
};
|
|
1748
1832
|
}
|
|
1749
1833
|
|
|
1750
1834
|
export const __testables = {
|
|
1751
1835
|
runProcess,
|
|
1752
1836
|
parseJsonOutput,
|
|
1753
|
-
parseScreenProgressLine
|
|
1837
|
+
parseScreenProgressLine,
|
|
1838
|
+
resolveRecommendScreenTimeoutMs,
|
|
1839
|
+
buildRecommendScreenProcessError
|
|
1754
1840
|
};
|
package/src/parser.js
CHANGED
|
@@ -192,7 +192,10 @@ function expandDegreeAtOrAbove(value) {
|
|
|
192
192
|
function parseDegreeSelectionsFromText(text) {
|
|
193
193
|
const normalizedText = normalizeText(text);
|
|
194
194
|
if (!normalizedText) return [];
|
|
195
|
-
if (
|
|
195
|
+
if (
|
|
196
|
+
/(?:学历|学位|教育)(?:要求)?\s*(?:[::]\s*)?(?:不限|不限制|无要求)|(?:不限|不限制)\s*(?:[::]\s*)?(?:学历|学位|教育)(?:要求)?/i
|
|
197
|
+
.test(normalizedText)
|
|
198
|
+
) {
|
|
196
199
|
return ["不限"];
|
|
197
200
|
}
|
|
198
201
|
|
|
@@ -4,7 +4,13 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { runRecommendScreenCli, __testables as adapterTestables } from "./adapters.js";
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
runProcess,
|
|
9
|
+
parseJsonOutput,
|
|
10
|
+
parseScreenProgressLine,
|
|
11
|
+
resolveRecommendScreenTimeoutMs,
|
|
12
|
+
buildRecommendScreenProcessError
|
|
13
|
+
} = adapterTestables;
|
|
8
14
|
|
|
9
15
|
async function testRunProcessHeartbeatAndOutput() {
|
|
10
16
|
const heartbeats = [];
|
|
@@ -57,6 +63,50 @@ function testParsePausedStructuredOutput() {
|
|
|
57
63
|
assert.equal(parsed?.result?.processed_count, 3);
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
function testParseScreenProgressLineShouldCountFavoriteFailureAsSkipped() {
|
|
67
|
+
let progress = { processed: 0, passed: 0, skipped: 0, greet_count: 0 };
|
|
68
|
+
let tracker = {};
|
|
69
|
+
const feed = (line) => {
|
|
70
|
+
const parsed = parseScreenProgressLine(line, progress, tracker);
|
|
71
|
+
if (!parsed) return;
|
|
72
|
+
progress = parsed.progress;
|
|
73
|
+
tracker = parsed.tracker;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
feed("处理第 1 位候选人: 甲");
|
|
77
|
+
feed("筛选结果: 通过");
|
|
78
|
+
feed("[关闭详情] 成功: no popup or detail signal visible");
|
|
79
|
+
feed("处理第 2 位候选人: 乙");
|
|
80
|
+
feed("筛选结果: 通过");
|
|
81
|
+
feed("候选人处理失败: FAVORITE_BUTTON_FAILED");
|
|
82
|
+
feed("[关闭详情] 成功: no popup or detail signal visible");
|
|
83
|
+
|
|
84
|
+
assert.equal(progress.processed, 2);
|
|
85
|
+
assert.equal(progress.passed, 1);
|
|
86
|
+
assert.equal(progress.skipped, 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function testResolveScreenTimeoutDefaultsTo24Hours() {
|
|
90
|
+
const previous = process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS;
|
|
91
|
+
delete process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS;
|
|
92
|
+
try {
|
|
93
|
+
assert.equal(resolveRecommendScreenTimeoutMs(null), 24 * 60 * 60 * 1000);
|
|
94
|
+
assert.equal(resolveRecommendScreenTimeoutMs({ timeoutMs: 1234 }), 1234);
|
|
95
|
+
} finally {
|
|
96
|
+
if (previous === undefined) {
|
|
97
|
+
delete process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS;
|
|
98
|
+
} else {
|
|
99
|
+
process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS = previous;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function testBuildRecommendScreenProcessErrorMapsTimeout() {
|
|
105
|
+
const error = buildRecommendScreenProcessError({ code: -1, error_code: "TIMEOUT" }, 86400000);
|
|
106
|
+
assert.equal(error?.code, "TIMEOUT");
|
|
107
|
+
assert.equal(String(error?.message || "").includes("86400000"), true);
|
|
108
|
+
}
|
|
109
|
+
|
|
60
110
|
async function testResumeRequiresCheckpointFile() {
|
|
61
111
|
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
62
112
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-resume-"));
|
|
@@ -103,6 +153,9 @@ async function main() {
|
|
|
103
153
|
await testRunProcessHeartbeatAndOutput();
|
|
104
154
|
await testRunProcessAbortSignal();
|
|
105
155
|
testParsePausedStructuredOutput();
|
|
156
|
+
testParseScreenProgressLineShouldCountFavoriteFailureAsSkipped();
|
|
157
|
+
testResolveScreenTimeoutDefaultsTo24Hours();
|
|
158
|
+
testBuildRecommendScreenProcessErrorMapsTimeout();
|
|
106
159
|
await testResumeRequiresCheckpointFile();
|
|
107
160
|
console.log("adapters runtime tests passed");
|
|
108
161
|
}
|
package/src/test-parser.js
CHANGED
|
@@ -172,6 +172,17 @@ function testDegreeAtOrAboveExpansion() {
|
|
|
172
172
|
assert.deepEqual(result.searchParams.degree, ["大专", "本科", "硕士", "博士"]);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
function testDegreeShouldNotBeOverwrittenBySchoolTagUnlimitedClause() {
|
|
176
|
+
const result = parseRecommendInstruction({
|
|
177
|
+
instruction: "学校标签不限,学历要求大专及以上,性别不限,过滤近14天已看",
|
|
178
|
+
confirmation: null,
|
|
179
|
+
overrides: null
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
assert.deepEqual(result.searchParams.school_tag, ["不限"]);
|
|
183
|
+
assert.deepEqual(result.searchParams.degree, ["大专", "本科", "硕士", "博士"]);
|
|
184
|
+
}
|
|
185
|
+
|
|
175
186
|
function testDegreeExplicitListOnly() {
|
|
176
187
|
const result = parseRecommendInstruction({
|
|
177
188
|
instruction: "推荐页筛选大专、本科,近14天没有,有Agent经验",
|
|
@@ -420,6 +431,7 @@ function main() {
|
|
|
420
431
|
testMultipleSchoolTagsMarkedSuspicious();
|
|
421
432
|
testDegreeCanBeExtracted();
|
|
422
433
|
testDegreeAtOrAboveExpansion();
|
|
434
|
+
testDegreeShouldNotBeOverwrittenBySchoolTagUnlimitedClause();
|
|
423
435
|
testDegreeExplicitListOnly();
|
|
424
436
|
testDegreeOverrideCanBeArray();
|
|
425
437
|
testSchoolTagOverrideCanBeArray();
|
|
@@ -9,6 +9,9 @@ const { captureFullResumeCanvas } = require("./scripts/capture-full-resume-canva
|
|
|
9
9
|
const DEFAULT_PORT = 9222;
|
|
10
10
|
const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
|
|
11
11
|
const CSV_HEADER = ["姓名", "最高学历学校", "最高学历专业", "最近工作公司", "最近工作职位", "评估通过详细原因"].join(",");
|
|
12
|
+
const RESUME_CAPTURE_WAIT_MS = 60000;
|
|
13
|
+
const RESUME_CAPTURE_MAX_ATTEMPTS = 4;
|
|
14
|
+
const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
|
|
12
15
|
|
|
13
16
|
function log(...args) {
|
|
14
17
|
console.error(...args);
|
|
@@ -1531,18 +1534,40 @@ class RecommendScreenCli {
|
|
|
1531
1534
|
}
|
|
1532
1535
|
|
|
1533
1536
|
async captureResumeImage(candidate) {
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
});
|
|
1543
|
-
|
|
1544
|
-
|
|
1537
|
+
const candidateLabel = normalizeText(candidate?.geek_id || candidate?.name || "unknown");
|
|
1538
|
+
const candidateKey = String(candidate?.geek_id || candidate?.name || "candidate")
|
|
1539
|
+
.replace(/[^\w.-]+/g, "_")
|
|
1540
|
+
.slice(0, 80) || "candidate";
|
|
1541
|
+
const attemptSummaries = [];
|
|
1542
|
+
let lastError = null;
|
|
1543
|
+
|
|
1544
|
+
for (let attempt = 1; attempt <= RESUME_CAPTURE_MAX_ATTEMPTS; attempt += 1) {
|
|
1545
|
+
const outPrefix = path.join(this.debugDir, `${candidateKey}_${Date.now()}_a${attempt}`);
|
|
1546
|
+
try {
|
|
1547
|
+
return await captureFullResumeCanvas({
|
|
1548
|
+
port: this.args.port,
|
|
1549
|
+
outPrefix,
|
|
1550
|
+
targetPattern: RECOMMEND_URL_FRAGMENT,
|
|
1551
|
+
waitResumeMs: RESUME_CAPTURE_WAIT_MS,
|
|
1552
|
+
scrollSettleMs: 500
|
|
1553
|
+
});
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
lastError = error;
|
|
1556
|
+
const message = normalizeText(error?.message || String(error) || "Failed to capture resume image.");
|
|
1557
|
+
attemptSummaries.push(`a${attempt}/${RESUME_CAPTURE_MAX_ATTEMPTS}:${message.slice(0, 320)}`);
|
|
1558
|
+
log(`[简历截图失败] candidate=${candidateLabel} attempt=${attempt}/${RESUME_CAPTURE_MAX_ATTEMPTS} error=${message}`);
|
|
1559
|
+
if (attempt < RESUME_CAPTURE_MAX_ATTEMPTS) {
|
|
1560
|
+
await sleep(RESUME_CAPTURE_RETRY_DELAY_MS);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1545
1563
|
}
|
|
1564
|
+
|
|
1565
|
+
const lastMessage = normalizeText(lastError?.message || "Failed to capture resume image.");
|
|
1566
|
+
const attemptsText = attemptSummaries.join(" | ");
|
|
1567
|
+
throw this.buildError(
|
|
1568
|
+
"RESUME_CAPTURE_FAILED",
|
|
1569
|
+
`Resume capture failed after ${RESUME_CAPTURE_MAX_ATTEMPTS} attempts; last_error=${lastMessage}; attempts=${attemptsText}`
|
|
1570
|
+
);
|
|
1546
1571
|
}
|
|
1547
1572
|
|
|
1548
1573
|
async callVisionModel(imagePath) {
|
|
@@ -39,6 +39,36 @@ function pickTarget(targets, targetPattern) {
|
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function oneLineJson(value, maxLength = 1200) {
|
|
43
|
+
try {
|
|
44
|
+
const text = JSON.stringify(value);
|
|
45
|
+
if (text.length <= maxLength) return text;
|
|
46
|
+
return `${text.slice(0, maxLength)}...`;
|
|
47
|
+
} catch {
|
|
48
|
+
return "\"<unserializable>\"";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function summarizeProbeReason(probe) {
|
|
53
|
+
if (!probe || typeof probe !== "object") return "NO_PROBE";
|
|
54
|
+
if (probe.ok === true) return "INVALID_CLIP";
|
|
55
|
+
return String(probe.reason || "UNKNOWN");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildResumeProbeTimeoutMessage(waitResumeMs, probe) {
|
|
59
|
+
const reason = summarizeProbeReason(probe);
|
|
60
|
+
const payload = {
|
|
61
|
+
reason,
|
|
62
|
+
clip: probe?.clip || null,
|
|
63
|
+
scroll_top: Number.isFinite(Number(probe?.scrollTop)) ? Number(probe.scrollTop) : null,
|
|
64
|
+
client_height: Number.isFinite(Number(probe?.clientHeight)) ? Number(probe.clientHeight) : null,
|
|
65
|
+
scroll_height: Number.isFinite(Number(probe?.scrollHeight)) ? Number(probe.scrollHeight) : null,
|
|
66
|
+
max_scroll: Number.isFinite(Number(probe?.maxScroll)) ? Number(probe.maxScroll) : null,
|
|
67
|
+
debug: probe?.debug || null
|
|
68
|
+
};
|
|
69
|
+
return `Resume canvas not found: wait_resume_ms=${waitResumeMs}; last_reason=${reason}; probe=${oneLineJson(payload)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
function buildResumeProbeExpr({ init, targetScroll }) {
|
|
43
73
|
const initLiteral = init ? "true" : "false";
|
|
44
74
|
const scrollLiteral = typeof targetScroll === "number" && Number.isFinite(targetScroll)
|
|
@@ -110,12 +140,22 @@ function buildResumeProbeExpr({ init, targetScroll }) {
|
|
|
110
140
|
|| document.querySelector('iframe');
|
|
111
141
|
const recommendDoc = recommendFrame && recommendFrame.contentDocument;
|
|
112
142
|
if (!recommendFrame || !recommendDoc) {
|
|
113
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: 'NO_RECOMMEND_IFRAME',
|
|
146
|
+
debug: {
|
|
147
|
+
topIframeCount: document.querySelectorAll('iframe').length
|
|
148
|
+
}
|
|
149
|
+
};
|
|
114
150
|
}
|
|
115
151
|
|
|
116
152
|
const scopes = Array.from(
|
|
117
153
|
recommendDoc.querySelectorAll('.dialog-wrap.active, .boss-popup__wrapper.boss-dialog, .boss-dialog__wrapper')
|
|
118
154
|
).filter(isVisible);
|
|
155
|
+
const allResumeFrames = Array.from(
|
|
156
|
+
recommendDoc.querySelectorAll('iframe[src*="/web/frame/c-resume/"], iframe[name*="resume"]')
|
|
157
|
+
);
|
|
158
|
+
const visibleResumeFrames = allResumeFrames.filter(isVisible);
|
|
119
159
|
|
|
120
160
|
let resumeFrame = null;
|
|
121
161
|
for (const scope of scopes) {
|
|
@@ -127,13 +167,22 @@ function buildResumeProbeExpr({ init, targetScroll }) {
|
|
|
127
167
|
}
|
|
128
168
|
|
|
129
169
|
if (!resumeFrame) {
|
|
130
|
-
resumeFrame =
|
|
131
|
-
recommendDoc.querySelectorAll('iframe[src*="/web/frame/c-resume/"], iframe[name*="resume"]')
|
|
132
|
-
).find(isVisible) || null;
|
|
170
|
+
resumeFrame = visibleResumeFrames[0] || null;
|
|
133
171
|
}
|
|
134
172
|
|
|
135
173
|
if (!resumeFrame) {
|
|
136
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
reason: 'NO_CRESUME_IFRAME',
|
|
177
|
+
debug: {
|
|
178
|
+
activeScopeCount: scopes.length,
|
|
179
|
+
totalResumeIframes: allResumeFrames.length,
|
|
180
|
+
visibleResumeIframes: visibleResumeFrames.length,
|
|
181
|
+
recommendFrameUrl: (() => {
|
|
182
|
+
try { return String(recommendFrame.contentWindow.location.href || ''); } catch { return ''; }
|
|
183
|
+
})()
|
|
184
|
+
}
|
|
185
|
+
};
|
|
137
186
|
}
|
|
138
187
|
|
|
139
188
|
const resumeDoc = resumeFrame.contentDocument;
|
|
@@ -143,7 +192,19 @@ function buildResumeProbeExpr({ init, targetScroll }) {
|
|
|
143
192
|
|| chooseScrollableAncestor(resumeFrame)
|
|
144
193
|
|| null;
|
|
145
194
|
if (!scroller || !isVisible(scroller)) {
|
|
146
|
-
return {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
reason: 'NO_SCROLL_CONTAINER',
|
|
198
|
+
debug: {
|
|
199
|
+
activeScopeCount: scopes.length,
|
|
200
|
+
totalResumeIframes: allResumeFrames.length,
|
|
201
|
+
visibleResumeIframes: visibleResumeFrames.length,
|
|
202
|
+
resumeFrameSrc: String(resumeFrame.src || ''),
|
|
203
|
+
scrollerFound: Boolean(scroller),
|
|
204
|
+
scrollerVisible: Boolean(scroller && isVisible(scroller)),
|
|
205
|
+
scrollerClass: scroller ? String(scroller.className || '') : ''
|
|
206
|
+
}
|
|
207
|
+
};
|
|
147
208
|
}
|
|
148
209
|
|
|
149
210
|
return {
|
|
@@ -285,9 +346,23 @@ async function captureFullResumeCanvas(options = {}) {
|
|
|
285
346
|
await send("Page.bringToFront");
|
|
286
347
|
|
|
287
348
|
let probe = null;
|
|
349
|
+
let lastProbe = null;
|
|
288
350
|
const startTime = Date.now();
|
|
289
351
|
while (Date.now() - startTime < waitResumeMs) {
|
|
290
|
-
|
|
352
|
+
try {
|
|
353
|
+
probe = await evaluate(buildResumeProbeExpr({ init: true, targetScroll: 0 }));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
probe = {
|
|
356
|
+
ok: false,
|
|
357
|
+
reason: "PROBE_EVALUATE_FAILED",
|
|
358
|
+
debug: {
|
|
359
|
+
message: String(error?.message || error || "unknown")
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (probe && typeof probe === "object") {
|
|
364
|
+
lastProbe = probe;
|
|
365
|
+
}
|
|
291
366
|
if (probe?.ok && probe.clip?.height > 80 && probe.clip?.width > 120) {
|
|
292
367
|
break;
|
|
293
368
|
}
|
|
@@ -295,7 +370,7 @@ async function captureFullResumeCanvas(options = {}) {
|
|
|
295
370
|
}
|
|
296
371
|
|
|
297
372
|
if (!probe?.ok) {
|
|
298
|
-
throw new Error(
|
|
373
|
+
throw new Error(buildResumeProbeTimeoutMessage(waitResumeMs, lastProbe || probe));
|
|
299
374
|
}
|
|
300
375
|
|
|
301
376
|
const maxScroll = Math.max(0, Number(probe.maxScroll || 0));
|