@reconcrap/boss-recommend-mcp 1.3.32 → 1.3.33
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/config/screening-config.example.json +11 -11
- package/package.json +64 -64
- package/src/boss-chat.js +769 -769
- package/src/test-adapters-runtime.js +628 -628
- package/src/test-boss-chat.js +2716 -2227
- package/vendor/boss-chat-cli/src/app.js +1435 -1268
- package/vendor/boss-chat-cli/src/browser/chat-page.js +412 -242
- package/vendor/boss-chat-cli/src/cli.js +1580 -1580
- package/vendor/boss-chat-cli/src/services/chrome-client.js +103 -103
- package/vendor/boss-chat-cli/src/services/llm.js +1146 -810
- package/vendor/boss-chat-cli/src/services/llm.test.js +326 -0
- package/vendor/boss-chat-cli/src/services/profile-store.js +168 -168
- package/vendor/boss-chat-cli/src/services/report-store.js +317 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +469 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +727 -727
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +6660 -6272
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +429 -31
|
@@ -1,1329 +1,1496 @@
|
|
|
1
|
-
import { mkdir } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { isDomProfileConsistentWithCard, NETWORK_RESUME_RETRY_WAIT_MS } from './services/resume-network.js';
|
|
5
|
-
import { createCustomerAliases, createCustomerKey } from './utils/customer-key.js';
|
|
6
|
-
|
|
7
|
-
function runToken(date = new Date()) {
|
|
8
|
-
return date.toISOString().replace(/[:.]/g, '-');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function safePathToken(value) {
|
|
12
|
-
return String(value || 'unknown')
|
|
13
|
-
.replace(/[^\w.-]+/g, '_')
|
|
14
|
-
.slice(0, 80);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeText(value) {
|
|
18
|
-
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function toStringArray(value, maxItems = 8) {
|
|
22
|
-
if (!Array.isArray(value)) return [];
|
|
23
|
-
const normalized = [];
|
|
24
|
-
for (const item of value) {
|
|
25
|
-
const text = normalizeText(item);
|
|
26
|
-
if (!text) continue;
|
|
27
|
-
normalized.push(text);
|
|
28
|
-
if (normalized.length >= maxItems) break;
|
|
29
|
-
}
|
|
30
|
-
return normalized;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function shouldContinue(summary, targetCount) {
|
|
34
|
-
if (!targetCount || !Number.isFinite(targetCount) || targetCount <= 0) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
return summary.inspected < targetCount;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function hasResumeRequestSentMessage(state = {}) {
|
|
41
|
-
const lastText = normalizeText(state?.lastText || '');
|
|
42
|
-
const recent = Array.isArray(state?.recent) ? state.recent : [];
|
|
43
|
-
if (lastText.includes('简历请求已发送')) return true;
|
|
44
|
-
return recent.some((item) => normalizeText(item).includes('简历请求已发送'));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS = 5000;
|
|
48
|
-
const CANDIDATE_LIST_WAIT_POLL_MS = 500;
|
|
49
|
-
|
|
50
|
-
export class BossChatApp {
|
|
51
|
-
constructor({
|
|
52
|
-
page,
|
|
53
|
-
llmClient,
|
|
54
|
-
interaction,
|
|
55
|
-
resumeCaptureService,
|
|
56
|
-
stateStore,
|
|
57
|
-
reportStore,
|
|
58
|
-
resumeNetworkTracker = null,
|
|
59
|
-
runControl = null,
|
|
60
|
-
logger = console,
|
|
61
|
-
dryRun = false,
|
|
62
|
-
artifactRootDir = '',
|
|
63
|
-
resumeOpenCooldownMs = 3000,
|
|
64
|
-
onProgress = null,
|
|
65
|
-
}) {
|
|
66
|
-
this.page = page;
|
|
67
|
-
this.llmClient = llmClient;
|
|
68
|
-
this.interaction = interaction;
|
|
69
|
-
this.resumeCaptureService = resumeCaptureService;
|
|
70
|
-
this.stateStore = stateStore;
|
|
71
|
-
this.reportStore = reportStore;
|
|
72
|
-
this.resumeNetworkTracker = resumeNetworkTracker;
|
|
73
|
-
this.runControl = runControl;
|
|
74
|
-
this.logger = logger;
|
|
75
|
-
this.dryRun = dryRun;
|
|
76
|
-
this.artifactRootDir = artifactRootDir;
|
|
77
|
-
this.lastResumeOpenAt = 0;
|
|
78
|
-
this.resumeOpenBlockedUntil = 0;
|
|
79
|
-
this.resumeOpenCooldownMs = Number.isFinite(Number(resumeOpenCooldownMs))
|
|
80
|
-
? Math.max(0, Number(resumeOpenCooldownMs))
|
|
81
|
-
: 3000;
|
|
82
|
-
this.onProgress = typeof onProgress === 'function' ? onProgress : null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
formatProgress(summary) {
|
|
86
|
-
const targetText = summary.profile.targetCount || '∞';
|
|
87
|
-
return `进度: 已处理 ${summary.inspected}/${targetText},通过 ${summary.passed},已求简历 ${summary.requested},跳过 ${summary.skipped},错误 ${summary.errors}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async checkpoint() {
|
|
91
|
-
if (this.runControl) {
|
|
92
|
-
await this.runControl.checkpoint();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async waitResumeOpenCooldown(minGapMs = this.resumeOpenCooldownMs) {
|
|
97
|
-
const now = Date.now();
|
|
98
|
-
const waitFromLast = Math.max(0, minGapMs - (now - this.lastResumeOpenAt));
|
|
99
|
-
const waitFromBlock = Math.max(0, this.resumeOpenBlockedUntil - now);
|
|
100
|
-
const waitMs = Math.max(waitFromLast, waitFromBlock);
|
|
101
|
-
if (waitMs <= 0) return;
|
|
102
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
setResumeOpenBlocked(ms = 90000) {
|
|
106
|
-
const until = Date.now() + ms;
|
|
107
|
-
this.resumeOpenBlockedUntil = Math.max(this.resumeOpenBlockedUntil, until);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
emitProgress(summary, meta = {}) {
|
|
111
|
-
if (!this.onProgress) return;
|
|
112
|
-
try {
|
|
113
|
-
this.onProgress(
|
|
114
|
-
{
|
|
115
|
-
inspected: Number(summary?.inspected || 0),
|
|
116
|
-
passed: Number(summary?.passed || 0),
|
|
117
|
-
requested: Number(summary?.requested || 0),
|
|
118
|
-
skipped: Number(summary?.skipped || 0),
|
|
119
|
-
errors: Number(summary?.errors || 0),
|
|
120
|
-
exhausted: Boolean(summary?.exhausted),
|
|
121
|
-
stopped: Boolean(summary?.stopped),
|
|
122
|
-
stopReason: String(summary?.stopReason || ''),
|
|
123
|
-
reportPath: String(summary?.reportPath || ''),
|
|
124
|
-
},
|
|
125
|
-
meta,
|
|
126
|
-
);
|
|
127
|
-
} catch {}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
buildCardProfile(customer = {}) {
|
|
131
|
-
return {
|
|
132
|
-
name: normalizeText(customer.name || ''),
|
|
133
|
-
school: normalizeText(customer.school || ''),
|
|
134
|
-
major: normalizeText(customer.major || ''),
|
|
135
|
-
company: normalizeText(customer.company || customer.lastCompany || customer.last_company || ''),
|
|
136
|
-
position: normalizeText(customer.position || customer.lastPosition || customer.last_position || ''),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { isDomProfileConsistentWithCard, NETWORK_RESUME_RETRY_WAIT_MS } from './services/resume-network.js';
|
|
5
|
+
import { createCustomerAliases, createCustomerKey } from './utils/customer-key.js';
|
|
6
|
+
|
|
7
|
+
function runToken(date = new Date()) {
|
|
8
|
+
return date.toISOString().replace(/[:.]/g, '-');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function safePathToken(value) {
|
|
12
|
+
return String(value || 'unknown')
|
|
13
|
+
.replace(/[^\w.-]+/g, '_')
|
|
14
|
+
.slice(0, 80);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeText(value) {
|
|
18
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toStringArray(value, maxItems = 8) {
|
|
22
|
+
if (!Array.isArray(value)) return [];
|
|
23
|
+
const normalized = [];
|
|
24
|
+
for (const item of value) {
|
|
25
|
+
const text = normalizeText(item);
|
|
26
|
+
if (!text) continue;
|
|
27
|
+
normalized.push(text);
|
|
28
|
+
if (normalized.length >= maxItems) break;
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shouldContinue(summary, targetCount) {
|
|
34
|
+
if (!targetCount || !Number.isFinite(targetCount) || targetCount <= 0) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return summary.inspected < targetCount;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasResumeRequestSentMessage(state = {}) {
|
|
41
|
+
const lastText = normalizeText(state?.lastText || '');
|
|
42
|
+
const recent = Array.isArray(state?.recent) ? state.recent : [];
|
|
43
|
+
if (lastText.includes('简历请求已发送')) return true;
|
|
44
|
+
return recent.some((item) => normalizeText(item).includes('简历请求已发送'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS = 5000;
|
|
48
|
+
const CANDIDATE_LIST_WAIT_POLL_MS = 500;
|
|
49
|
+
|
|
50
|
+
export class BossChatApp {
|
|
51
|
+
constructor({
|
|
52
|
+
page,
|
|
53
|
+
llmClient,
|
|
54
|
+
interaction,
|
|
55
|
+
resumeCaptureService,
|
|
56
|
+
stateStore,
|
|
57
|
+
reportStore,
|
|
58
|
+
resumeNetworkTracker = null,
|
|
59
|
+
runControl = null,
|
|
60
|
+
logger = console,
|
|
61
|
+
dryRun = false,
|
|
62
|
+
artifactRootDir = '',
|
|
63
|
+
resumeOpenCooldownMs = 3000,
|
|
64
|
+
onProgress = null,
|
|
65
|
+
}) {
|
|
66
|
+
this.page = page;
|
|
67
|
+
this.llmClient = llmClient;
|
|
68
|
+
this.interaction = interaction;
|
|
69
|
+
this.resumeCaptureService = resumeCaptureService;
|
|
70
|
+
this.stateStore = stateStore;
|
|
71
|
+
this.reportStore = reportStore;
|
|
72
|
+
this.resumeNetworkTracker = resumeNetworkTracker;
|
|
73
|
+
this.runControl = runControl;
|
|
74
|
+
this.logger = logger;
|
|
75
|
+
this.dryRun = dryRun;
|
|
76
|
+
this.artifactRootDir = artifactRootDir;
|
|
77
|
+
this.lastResumeOpenAt = 0;
|
|
78
|
+
this.resumeOpenBlockedUntil = 0;
|
|
79
|
+
this.resumeOpenCooldownMs = Number.isFinite(Number(resumeOpenCooldownMs))
|
|
80
|
+
? Math.max(0, Number(resumeOpenCooldownMs))
|
|
81
|
+
: 3000;
|
|
82
|
+
this.onProgress = typeof onProgress === 'function' ? onProgress : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
formatProgress(summary) {
|
|
86
|
+
const targetText = summary.profile.targetCount || '∞';
|
|
87
|
+
return `进度: 已处理 ${summary.inspected}/${targetText},通过 ${summary.passed},已求简历 ${summary.requested},跳过 ${summary.skipped},错误 ${summary.errors}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async checkpoint() {
|
|
91
|
+
if (this.runControl) {
|
|
92
|
+
await this.runControl.checkpoint();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async waitResumeOpenCooldown(minGapMs = this.resumeOpenCooldownMs) {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const waitFromLast = Math.max(0, minGapMs - (now - this.lastResumeOpenAt));
|
|
99
|
+
const waitFromBlock = Math.max(0, this.resumeOpenBlockedUntil - now);
|
|
100
|
+
const waitMs = Math.max(waitFromLast, waitFromBlock);
|
|
101
|
+
if (waitMs <= 0) return;
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setResumeOpenBlocked(ms = 90000) {
|
|
106
|
+
const until = Date.now() + ms;
|
|
107
|
+
this.resumeOpenBlockedUntil = Math.max(this.resumeOpenBlockedUntil, until);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
emitProgress(summary, meta = {}) {
|
|
111
|
+
if (!this.onProgress) return;
|
|
112
|
+
try {
|
|
113
|
+
this.onProgress(
|
|
114
|
+
{
|
|
115
|
+
inspected: Number(summary?.inspected || 0),
|
|
116
|
+
passed: Number(summary?.passed || 0),
|
|
117
|
+
requested: Number(summary?.requested || 0),
|
|
118
|
+
skipped: Number(summary?.skipped || 0),
|
|
119
|
+
errors: Number(summary?.errors || 0),
|
|
120
|
+
exhausted: Boolean(summary?.exhausted),
|
|
121
|
+
stopped: Boolean(summary?.stopped),
|
|
122
|
+
stopReason: String(summary?.stopReason || ''),
|
|
123
|
+
reportPath: String(summary?.reportPath || ''),
|
|
124
|
+
},
|
|
125
|
+
meta,
|
|
126
|
+
);
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
buildCardProfile(customer = {}) {
|
|
131
|
+
return {
|
|
132
|
+
name: normalizeText(customer.name || ''),
|
|
133
|
+
school: normalizeText(customer.school || ''),
|
|
134
|
+
major: normalizeText(customer.major || ''),
|
|
135
|
+
company: normalizeText(customer.company || customer.lastCompany || customer.last_company || ''),
|
|
136
|
+
position: normalizeText(customer.position || customer.lastPosition || customer.last_position || ''),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
buildResumeCandidateContext(customer = {}, candidateInfo = null) {
|
|
141
|
+
const info = candidateInfo && typeof candidateInfo === 'object' ? candidateInfo : {};
|
|
142
|
+
const schools = Array.isArray(info.schools) ? info.schools.map((item) => normalizeText(item)).filter(Boolean) : [];
|
|
143
|
+
const majors = Array.isArray(info.majors) ? info.majors.map((item) => normalizeText(item)).filter(Boolean) : [];
|
|
144
|
+
const primarySchool = normalizeText(info.school || info.primarySchool || schools[0] || '');
|
|
145
|
+
const primaryMajor = normalizeText(info.major || majors[0] || '');
|
|
146
|
+
const company = normalizeText(info.company || '');
|
|
147
|
+
const position = normalizeText(info.position || '');
|
|
148
|
+
const hasProfileContext = Boolean(primarySchool || primaryMajor || company || position || schools.length || majors.length);
|
|
149
|
+
return {
|
|
150
|
+
name: customer.name || info.name || '',
|
|
151
|
+
sourceJob: customer.sourceJob || '',
|
|
152
|
+
resumeProfile: hasProfileContext
|
|
153
|
+
? {
|
|
154
|
+
primarySchool,
|
|
155
|
+
schools: schools.length > 0 ? schools : primarySchool ? [primarySchool] : [],
|
|
156
|
+
major: primaryMajor,
|
|
157
|
+
majors: majors.length > 0 ? majors : primaryMajor ? [primaryMajor] : [],
|
|
158
|
+
company,
|
|
159
|
+
position,
|
|
160
|
+
}
|
|
161
|
+
: null,
|
|
162
|
+
resumeText: String(info.resumeText || ''),
|
|
163
|
+
evidenceCorpus: String(info.evidenceCorpus || info.resumeText || ''),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async extractDomResumeCandidateInfo(customer = {}) {
|
|
168
|
+
if (typeof this.page.getResumeProfileFromDom !== 'function') {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const result = await this.page.getResumeProfileFromDom();
|
|
172
|
+
if (!result?.ok) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const resumeText = normalizeText(result.resumeText || '');
|
|
176
|
+
if (!resumeText) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
name: normalizeText(result.name || customer.name || ''),
|
|
181
|
+
school: normalizeText(result.primarySchool || ''),
|
|
182
|
+
schools: Array.isArray(result.schools) ? result.schools.map((item) => normalizeText(item)).filter(Boolean) : [],
|
|
183
|
+
major: normalizeText(result.major || ''),
|
|
184
|
+
majors: Array.isArray(result.majors) ? result.majors.map((item) => normalizeText(item)).filter(Boolean) : [],
|
|
185
|
+
company: normalizeText(result.company || ''),
|
|
186
|
+
position: normalizeText(result.position || ''),
|
|
187
|
+
resumeText: String(result.resumeText || ''),
|
|
188
|
+
evidenceCorpus: String(result.evidenceCorpus || result.resumeText || ''),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async retryCandidateResumeContext(customer = {}) {
|
|
193
|
+
if (typeof this.page.closeResumeModalDomOnce === 'function') {
|
|
194
|
+
try {
|
|
195
|
+
await this.page.closeResumeModalDomOnce();
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
await this.checkpoint();
|
|
199
|
+
if (typeof this.page.activateCandidate === 'function') {
|
|
200
|
+
await this.page.activateCandidate(customer, 0);
|
|
201
|
+
} else {
|
|
202
|
+
const rect = await this.page.centerCustomerCard(customer.domIndex, 0);
|
|
203
|
+
await this.interaction.clickRect(rect);
|
|
204
|
+
}
|
|
205
|
+
await this.interaction.sleepRange(520, 140);
|
|
206
|
+
await this.page.waitForCandidateActivated(customer, {
|
|
207
|
+
maxAttempts: 8,
|
|
208
|
+
delayMs: 180,
|
|
209
|
+
});
|
|
210
|
+
await this.page.waitForConversationReady({
|
|
211
|
+
maxAttempts: 8,
|
|
212
|
+
delayMs: 220,
|
|
213
|
+
});
|
|
214
|
+
const retryStartedAt = Date.now();
|
|
215
|
+
const openResult = await this.page.openOnlineResume();
|
|
216
|
+
await this.interaction.sleepRange(520, 140);
|
|
217
|
+
return {
|
|
218
|
+
retryStartedAt,
|
|
219
|
+
openResult,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async resolveDomResumeFallback(customer = {}, cardProfile = null) {
|
|
224
|
+
let domCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
|
|
225
|
+
let networkCandidateInfo = null;
|
|
226
|
+
let acquisitionReason = domCandidateInfo?.resumeText ? 'dom_initial_hit' : '';
|
|
227
|
+
|
|
228
|
+
if (domCandidateInfo && !isDomProfileConsistentWithCard(cardProfile, domCandidateInfo)) {
|
|
229
|
+
this.logger.log(
|
|
230
|
+
`DOM简历疑似错位:expected=${cardProfile?.name || 'unknown'} | actual=${domCandidateInfo?.name || 'unknown'},尝试重试点击并短暂回查 network。`,
|
|
231
|
+
);
|
|
232
|
+
acquisitionReason = 'dom_profile_mismatch_retry';
|
|
233
|
+
try {
|
|
234
|
+
const retryContext = await this.retryCandidateResumeContext(customer);
|
|
235
|
+
if (this.resumeNetworkTracker) {
|
|
236
|
+
const retryNetwork = await this.resumeNetworkTracker.waitForNetworkResumeCandidateInfo(
|
|
237
|
+
customer,
|
|
238
|
+
NETWORK_RESUME_RETRY_WAIT_MS,
|
|
239
|
+
{ minTs: retryContext.retryStartedAt },
|
|
240
|
+
);
|
|
241
|
+
if (retryNetwork?.candidateInfo?.resumeText) {
|
|
242
|
+
networkCandidateInfo = retryNetwork.candidateInfo;
|
|
243
|
+
acquisitionReason = 'dom_retry_network_recheck_hit';
|
|
244
|
+
domCandidateInfo = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!networkCandidateInfo) {
|
|
248
|
+
const retryDomCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
|
|
249
|
+
if (retryDomCandidateInfo && isDomProfileConsistentWithCard(cardProfile, retryDomCandidateInfo)) {
|
|
250
|
+
domCandidateInfo = retryDomCandidateInfo;
|
|
251
|
+
acquisitionReason = 'dom_retry_hit';
|
|
252
|
+
} else {
|
|
253
|
+
domCandidateInfo = null;
|
|
254
|
+
acquisitionReason = 'dom_profile_mismatch_unresolved';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
domCandidateInfo = null;
|
|
259
|
+
acquisitionReason = `dom_profile_retry_failed:${normalizeText(error?.message || error)}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
domCandidateInfo,
|
|
265
|
+
networkCandidateInfo,
|
|
266
|
+
acquisitionReason,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async acquireResumeAndEvaluate(customer, profile, artifactDir, baseResult) {
|
|
271
|
+
let modalOpened = false;
|
|
272
|
+
let capture = null;
|
|
273
|
+
let lastResumeError = null;
|
|
274
|
+
const timings = {
|
|
275
|
+
initialNetworkWaitMs: 0,
|
|
276
|
+
networkRetryMs: 0,
|
|
277
|
+
imageCaptureMs: 0,
|
|
278
|
+
imageModelMs: 0,
|
|
279
|
+
lateNetworkRetryMs: 0,
|
|
280
|
+
domFallbackMs: 0,
|
|
281
|
+
textModelMs: 0,
|
|
282
|
+
};
|
|
283
|
+
const cardProfile = this.buildCardProfile(customer);
|
|
284
|
+
|
|
285
|
+
await this.waitResumeOpenCooldown(this.resumeOpenCooldownMs + Math.floor(Math.random() * 200));
|
|
286
|
+
await this.checkpoint();
|
|
287
|
+
const acquisitionStartedAt = Date.now();
|
|
288
|
+
const openResult = await this.page.openOnlineResume();
|
|
289
|
+
let openDetected = openResult ? Boolean(openResult?.detectedOpen) : true;
|
|
290
|
+
this.lastResumeOpenAt = Date.now();
|
|
291
|
+
modalOpened = openDetected;
|
|
292
|
+
await this.interaction.sleepRange(600, 220);
|
|
293
|
+
|
|
294
|
+
const rateLimit =
|
|
295
|
+
typeof this.page.getResumeRateLimitWarning === 'function'
|
|
296
|
+
? await this.page.getResumeRateLimitWarning()
|
|
297
|
+
: { hit: false, text: '' };
|
|
298
|
+
if (rateLimit?.hit) {
|
|
299
|
+
const backoffMs = 90000 + Math.floor(Math.random() * 30000);
|
|
300
|
+
this.setResumeOpenBlocked(backoffMs);
|
|
301
|
+
throw new Error(`RESUME_RATE_LIMIT_WARNING:${rateLimit.text}`);
|
|
302
|
+
}
|
|
303
|
+
if (openResult && !openDetected) {
|
|
304
|
+
let delayedDetected = false;
|
|
305
|
+
if (typeof this.page.getResumeModalState === 'function') {
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
307
|
+
const delayedState = await this.page.getResumeModalState();
|
|
308
|
+
delayedDetected =
|
|
309
|
+
Boolean(delayedState?.open) ||
|
|
310
|
+
Number(delayedState?.iframeCount || 0) > 0 ||
|
|
311
|
+
(Number(delayedState?.scopeCount || 0) > 0 && Number(delayedState?.closeCount || 0) > 0);
|
|
312
|
+
}
|
|
313
|
+
if (delayedDetected) {
|
|
314
|
+
openDetected = true;
|
|
315
|
+
modalOpened = true;
|
|
316
|
+
this.logger.log('在线简历首次检测未命中,1秒后复检已打开,继续处理。');
|
|
317
|
+
} else {
|
|
318
|
+
throw new Error('RESUME_MODAL_NOT_DETECTED_AFTER_SINGLE_DOM_CLICK');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!openDetected) {
|
|
322
|
+
throw new Error('RESUME_MODAL_NOT_DETECTED');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let networkResult = null;
|
|
326
|
+
if (this.resumeNetworkTracker) {
|
|
327
|
+
networkResult = await this.resumeNetworkTracker.waitForResumeNetworkByMode(customer, {
|
|
328
|
+
minTs: acquisitionStartedAt,
|
|
329
|
+
});
|
|
330
|
+
timings.initialNetworkWaitMs = Number(networkResult?.initialWaitMs || 0);
|
|
331
|
+
timings.networkRetryMs = Number(networkResult?.retryWaitMs || 0);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (networkResult?.candidateInfo?.resumeText) {
|
|
335
|
+
await this.checkpoint();
|
|
336
|
+
const evaluationStartedAt = Date.now();
|
|
337
|
+
const evaluation = await this.llmClient.evaluateResume({
|
|
338
|
+
screeningCriteria: profile.screeningCriteria,
|
|
339
|
+
candidate: this.buildResumeCandidateContext(customer, networkResult.candidateInfo),
|
|
340
|
+
});
|
|
341
|
+
timings.textModelMs = Date.now() - evaluationStartedAt;
|
|
342
|
+
return {
|
|
343
|
+
modalOpened,
|
|
344
|
+
capture,
|
|
345
|
+
evaluation,
|
|
346
|
+
timings,
|
|
347
|
+
acquisitionMode: 'network',
|
|
348
|
+
acquisitionReason: networkResult.acquisitionReason || 'initial_network_hit',
|
|
349
|
+
sourceCandidateInfo: networkResult.candidateInfo,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await this.checkpoint();
|
|
355
|
+
const captureStartedAt = Date.now();
|
|
356
|
+
capture = await this.resumeCaptureService.captureResume({
|
|
357
|
+
artifactDir,
|
|
358
|
+
waitResumeMs: 30000,
|
|
359
|
+
scrollSettleMs: 500,
|
|
360
|
+
});
|
|
361
|
+
timings.imageCaptureMs = Date.now() - captureStartedAt;
|
|
362
|
+
if (capture?.quality?.likelyBlank) {
|
|
363
|
+
const blankBackoffMs = 45000 + Math.floor(Math.random() * 20000);
|
|
364
|
+
this.setResumeOpenBlocked(blankBackoffMs);
|
|
365
|
+
throw new Error('RESUME_CAPTURE_LIKELY_BLANK');
|
|
366
|
+
}
|
|
367
|
+
const modelImagePaths = Array.isArray(capture.modelImagePaths)
|
|
368
|
+
? capture.modelImagePaths.map((item) => String(item || '').trim()).filter(Boolean)
|
|
369
|
+
: [];
|
|
370
|
+
this.logger.log(`截图完成:chunks=${capture.chunkCount} | modelImages=${modelImagePaths.length}`);
|
|
371
|
+
baseResult.artifacts.chunkDir = capture.chunkDir;
|
|
372
|
+
baseResult.artifacts.metadataFile = capture.metadataFile;
|
|
373
|
+
baseResult.artifacts.stitchedImage = capture.stitchedImage;
|
|
374
|
+
baseResult.artifacts.chunkCount = capture.chunkCount;
|
|
375
|
+
baseResult.artifacts.modelImagePaths = modelImagePaths;
|
|
376
|
+
|
|
377
|
+
if (this.resumeNetworkTracker) {
|
|
378
|
+
this.resumeNetworkTracker.setResumeAcquisitionMode('image', 'image_capture_success');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await this.checkpoint();
|
|
382
|
+
const imageEvalStartedAt = Date.now();
|
|
383
|
+
const evaluation = await this.llmClient.evaluateResume({
|
|
384
|
+
screeningCriteria: profile.screeningCriteria,
|
|
385
|
+
candidate: this.buildResumeCandidateContext(customer, null),
|
|
386
|
+
imagePaths: modelImagePaths,
|
|
387
|
+
});
|
|
388
|
+
timings.imageModelMs = Date.now() - imageEvalStartedAt;
|
|
389
|
+
return {
|
|
390
|
+
modalOpened,
|
|
391
|
+
capture,
|
|
392
|
+
evaluation,
|
|
393
|
+
timings,
|
|
394
|
+
acquisitionMode: 'image_fallback',
|
|
395
|
+
acquisitionReason: 'image_capture_success',
|
|
396
|
+
sourceCandidateInfo: null,
|
|
397
|
+
};
|
|
398
|
+
} catch (imageError) {
|
|
399
|
+
lastResumeError = imageError;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let lateNetworkResult = null;
|
|
403
|
+
if (this.resumeNetworkTracker) {
|
|
404
|
+
lateNetworkResult = await this.resumeNetworkTracker.waitForLateNetworkResumeCandidateInfo(customer, {
|
|
405
|
+
minTs: acquisitionStartedAt,
|
|
406
|
+
});
|
|
407
|
+
timings.lateNetworkRetryMs = Number(lateNetworkResult?.lateRetryMs || 0);
|
|
408
|
+
}
|
|
409
|
+
if (lateNetworkResult?.candidateInfo?.resumeText) {
|
|
410
|
+
await this.checkpoint();
|
|
411
|
+
const evaluationStartedAt = Date.now();
|
|
412
|
+
const evaluation = await this.llmClient.evaluateResume({
|
|
413
|
+
screeningCriteria: profile.screeningCriteria,
|
|
414
|
+
candidate: this.buildResumeCandidateContext(customer, lateNetworkResult.candidateInfo),
|
|
415
|
+
});
|
|
416
|
+
timings.textModelMs = Date.now() - evaluationStartedAt;
|
|
417
|
+
return {
|
|
418
|
+
modalOpened,
|
|
419
|
+
capture,
|
|
420
|
+
evaluation,
|
|
421
|
+
timings,
|
|
422
|
+
acquisitionMode: 'network',
|
|
423
|
+
acquisitionReason: lateNetworkResult.acquisitionReason || 'late_network_hit',
|
|
424
|
+
sourceCandidateInfo: lateNetworkResult.candidateInfo,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const domStartedAt = Date.now();
|
|
429
|
+
const domFallback = await this.resolveDomResumeFallback(customer, cardProfile);
|
|
430
|
+
timings.domFallbackMs = Date.now() - domStartedAt;
|
|
431
|
+
if (domFallback?.networkCandidateInfo?.resumeText) {
|
|
432
|
+
await this.checkpoint();
|
|
433
|
+
const evaluationStartedAt = Date.now();
|
|
434
|
+
const evaluation = await this.llmClient.evaluateResume({
|
|
435
|
+
screeningCriteria: profile.screeningCriteria,
|
|
436
|
+
candidate: this.buildResumeCandidateContext(customer, domFallback.networkCandidateInfo),
|
|
437
|
+
});
|
|
438
|
+
timings.textModelMs = Date.now() - evaluationStartedAt;
|
|
439
|
+
return {
|
|
440
|
+
modalOpened,
|
|
441
|
+
capture,
|
|
442
|
+
evaluation,
|
|
443
|
+
timings,
|
|
444
|
+
acquisitionMode: 'network',
|
|
445
|
+
acquisitionReason: domFallback.acquisitionReason || 'dom_retry_network_recheck_hit',
|
|
446
|
+
sourceCandidateInfo: domFallback.networkCandidateInfo,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (domFallback?.domCandidateInfo?.resumeText) {
|
|
450
|
+
await this.checkpoint();
|
|
451
|
+
const evaluationStartedAt = Date.now();
|
|
452
|
+
const evaluation = await this.llmClient.evaluateResume({
|
|
453
|
+
screeningCriteria: profile.screeningCriteria,
|
|
454
|
+
candidate: this.buildResumeCandidateContext(customer, domFallback.domCandidateInfo),
|
|
455
|
+
});
|
|
456
|
+
timings.textModelMs = Date.now() - evaluationStartedAt;
|
|
457
|
+
return {
|
|
458
|
+
modalOpened,
|
|
459
|
+
capture,
|
|
460
|
+
evaluation,
|
|
461
|
+
timings,
|
|
462
|
+
acquisitionMode: 'dom_fallback',
|
|
463
|
+
acquisitionReason: domFallback.acquisitionReason || 'dom_initial_hit',
|
|
464
|
+
sourceCandidateInfo: domFallback.domCandidateInfo,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
throw lastResumeError || new Error('DOM_RESUME_FALLBACK_FAILED');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async restoreListContext(profile) {
|
|
472
|
+
if (typeof this.page.activatePrimaryChatLabel === 'function') {
|
|
473
|
+
await this.page.activatePrimaryChatLabel('全部');
|
|
474
|
+
}
|
|
475
|
+
await this.page.selectJob(profile.jobSelection);
|
|
476
|
+
return profile.startFrom === 'all'
|
|
477
|
+
? this.page.activateAllFilter()
|
|
478
|
+
: this.page.activateUnreadFilter();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async waitForCandidateList({
|
|
482
|
+
reason = 'unknown',
|
|
483
|
+
maxWaitMs = CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS,
|
|
484
|
+
pollMs = CANDIDATE_LIST_WAIT_POLL_MS,
|
|
485
|
+
} = {}) {
|
|
486
|
+
const startedAt = Date.now();
|
|
487
|
+
let attempts = 0;
|
|
488
|
+
let lastState = null;
|
|
489
|
+
let lastError = '';
|
|
490
|
+
|
|
491
|
+
while (Date.now() - startedAt <= maxWaitMs) {
|
|
492
|
+
attempts += 1;
|
|
493
|
+
try {
|
|
494
|
+
if (typeof this.page.getPageState === 'function') {
|
|
495
|
+
lastState = await this.page.getPageState();
|
|
496
|
+
if (Number(lastState?.listItemCount || 0) > 0) {
|
|
497
|
+
return {
|
|
498
|
+
ready: true,
|
|
499
|
+
waitedMs: Date.now() - startedAt,
|
|
500
|
+
attempts,
|
|
501
|
+
listItemCount: Number(lastState?.listItemCount || 0),
|
|
502
|
+
lastState,
|
|
503
|
+
lastError,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
} else if (typeof this.page.getLoadedCustomers === 'function') {
|
|
507
|
+
const customers = await this.page.getLoadedCustomers();
|
|
508
|
+
if (Array.isArray(customers) && customers.length > 0) {
|
|
509
|
+
return {
|
|
510
|
+
ready: true,
|
|
511
|
+
waitedMs: Date.now() - startedAt,
|
|
512
|
+
attempts,
|
|
513
|
+
listItemCount: customers.length,
|
|
514
|
+
lastState,
|
|
515
|
+
lastError,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} catch (error) {
|
|
520
|
+
lastError = String(error?.message || error || '');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (Date.now() - startedAt >= maxWaitMs) {
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
ready: false,
|
|
531
|
+
waitedMs: Date.now() - startedAt,
|
|
532
|
+
attempts,
|
|
533
|
+
listItemCount: Number(lastState?.listItemCount || 0),
|
|
534
|
+
lastState,
|
|
535
|
+
lastError,
|
|
536
|
+
reason,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async cleanupPanels({
|
|
541
|
+
resumeMaxAttempts = 6,
|
|
542
|
+
detailMaxAttempts = 4,
|
|
543
|
+
ensureDismiss = true,
|
|
544
|
+
} = {}) {
|
|
545
|
+
const resume =
|
|
546
|
+
typeof this.page.closeResumeModalDomOnce === 'function'
|
|
547
|
+
? await this.page.closeResumeModalDomOnce()
|
|
548
|
+
: await this.page.closeResumeModal({
|
|
549
|
+
maxAttempts: resumeMaxAttempts,
|
|
550
|
+
ensureDismiss,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
let detail = {
|
|
554
|
+
closed: true,
|
|
555
|
+
method: 'unsupported',
|
|
556
|
+
finalState: {
|
|
557
|
+
panelCount: 0,
|
|
558
|
+
closeCount: 0,
|
|
559
|
+
topPanelClass: '',
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
if (typeof this.page.closeCandidateDetailDomOnce === 'function') {
|
|
563
|
+
detail = await this.page.closeCandidateDetailDomOnce();
|
|
564
|
+
if (!detail.closed && typeof this.page.closeCandidateDetail === 'function') {
|
|
565
|
+
detail = await this.page.closeCandidateDetail({
|
|
566
|
+
maxAttempts: detailMaxAttempts,
|
|
567
|
+
ensureDismiss,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
} else if (typeof this.page.closeCandidateDetail === 'function') {
|
|
571
|
+
detail = await this.page.closeCandidateDetail({
|
|
572
|
+
maxAttempts: detailMaxAttempts,
|
|
573
|
+
ensureDismiss,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
139
576
|
|
|
140
|
-
|
|
141
|
-
const info = candidateInfo && typeof candidateInfo === 'object' ? candidateInfo : {};
|
|
142
|
-
const schools = Array.isArray(info.schools) ? info.schools.map((item) => normalizeText(item)).filter(Boolean) : [];
|
|
143
|
-
const majors = Array.isArray(info.majors) ? info.majors.map((item) => normalizeText(item)).filter(Boolean) : [];
|
|
144
|
-
const primarySchool = normalizeText(info.school || info.primarySchool || schools[0] || '');
|
|
145
|
-
const primaryMajor = normalizeText(info.major || majors[0] || '');
|
|
146
|
-
const company = normalizeText(info.company || '');
|
|
147
|
-
const position = normalizeText(info.position || '');
|
|
148
|
-
const hasProfileContext = Boolean(primarySchool || primaryMajor || company || position || schools.length || majors.length);
|
|
149
|
-
return {
|
|
150
|
-
name: customer.name || info.name || '',
|
|
151
|
-
sourceJob: customer.sourceJob || '',
|
|
152
|
-
resumeProfile: hasProfileContext
|
|
153
|
-
? {
|
|
154
|
-
primarySchool,
|
|
155
|
-
schools: schools.length > 0 ? schools : primarySchool ? [primarySchool] : [],
|
|
156
|
-
major: primaryMajor,
|
|
157
|
-
majors: majors.length > 0 ? majors : primaryMajor ? [primaryMajor] : [],
|
|
158
|
-
company,
|
|
159
|
-
position,
|
|
160
|
-
}
|
|
161
|
-
: null,
|
|
162
|
-
resumeText: String(info.resumeText || ''),
|
|
163
|
-
evidenceCorpus: String(info.evidenceCorpus || info.resumeText || ''),
|
|
164
|
-
};
|
|
577
|
+
return { resume, detail };
|
|
165
578
|
}
|
|
166
579
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
const resumeText = normalizeText(result.resumeText || '');
|
|
176
|
-
if (!resumeText) {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
return {
|
|
180
|
-
name: normalizeText(result.name || customer.name || ''),
|
|
181
|
-
school: normalizeText(result.primarySchool || ''),
|
|
182
|
-
schools: Array.isArray(result.schools) ? result.schools.map((item) => normalizeText(item)).filter(Boolean) : [],
|
|
183
|
-
major: normalizeText(result.major || ''),
|
|
184
|
-
majors: Array.isArray(result.majors) ? result.majors.map((item) => normalizeText(item)).filter(Boolean) : [],
|
|
185
|
-
company: normalizeText(result.company || ''),
|
|
186
|
-
position: normalizeText(result.position || ''),
|
|
187
|
-
resumeText: String(result.resumeText || ''),
|
|
188
|
-
evidenceCorpus: String(result.evidenceCorpus || result.resumeText || ''),
|
|
189
|
-
};
|
|
580
|
+
isResumeModalVisible(state = {}) {
|
|
581
|
+
return (
|
|
582
|
+
Boolean(state?.open) ||
|
|
583
|
+
Number(state?.iframeCount || 0) > 0 ||
|
|
584
|
+
Number(state?.scopeCount || 0) > 0
|
|
585
|
+
);
|
|
190
586
|
}
|
|
191
587
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
await this.checkpoint();
|
|
199
|
-
if (typeof this.page.activateCandidate === 'function') {
|
|
200
|
-
await this.page.activateCandidate(customer, 0);
|
|
201
|
-
} else {
|
|
202
|
-
const rect = await this.page.centerCustomerCard(customer.domIndex, 0);
|
|
203
|
-
await this.interaction.clickRect(rect);
|
|
204
|
-
}
|
|
205
|
-
await this.interaction.sleepRange(520, 140);
|
|
206
|
-
await this.page.waitForCandidateActivated(customer, {
|
|
207
|
-
maxAttempts: 8,
|
|
208
|
-
delayMs: 180,
|
|
209
|
-
});
|
|
210
|
-
await this.page.waitForConversationReady({
|
|
211
|
-
maxAttempts: 8,
|
|
212
|
-
delayMs: 220,
|
|
213
|
-
});
|
|
214
|
-
const retryStartedAt = Date.now();
|
|
215
|
-
const openResult = await this.page.openOnlineResume();
|
|
216
|
-
await this.interaction.sleepRange(520, 140);
|
|
217
|
-
return {
|
|
218
|
-
retryStartedAt,
|
|
219
|
-
openResult,
|
|
220
|
-
};
|
|
588
|
+
isCandidateDetailVisible(state = {}) {
|
|
589
|
+
return (
|
|
590
|
+
Boolean(state?.open) ||
|
|
591
|
+
Number(state?.panelCount || 0) > 0 ||
|
|
592
|
+
Number(state?.closeCount || 0) > 0
|
|
593
|
+
);
|
|
221
594
|
}
|
|
222
595
|
|
|
223
|
-
async
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (domCandidateInfo && !isDomProfileConsistentWithCard(cardProfile, domCandidateInfo)) {
|
|
229
|
-
this.logger.log(
|
|
230
|
-
`DOM简历疑似错位:expected=${cardProfile?.name || 'unknown'} | actual=${domCandidateInfo?.name || 'unknown'},尝试重试点击并短暂回查 network。`,
|
|
231
|
-
);
|
|
232
|
-
acquisitionReason = 'dom_profile_mismatch_retry';
|
|
233
|
-
try {
|
|
234
|
-
const retryContext = await this.retryCandidateResumeContext(customer);
|
|
235
|
-
if (this.resumeNetworkTracker) {
|
|
236
|
-
const retryNetwork = await this.resumeNetworkTracker.waitForNetworkResumeCandidateInfo(
|
|
237
|
-
customer,
|
|
238
|
-
NETWORK_RESUME_RETRY_WAIT_MS,
|
|
239
|
-
{ minTs: retryContext.retryStartedAt },
|
|
240
|
-
);
|
|
241
|
-
if (retryNetwork?.candidateInfo?.resumeText) {
|
|
242
|
-
networkCandidateInfo = retryNetwork.candidateInfo;
|
|
243
|
-
acquisitionReason = 'dom_retry_network_recheck_hit';
|
|
244
|
-
domCandidateInfo = null;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (!networkCandidateInfo) {
|
|
248
|
-
const retryDomCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
|
|
249
|
-
if (retryDomCandidateInfo && isDomProfileConsistentWithCard(cardProfile, retryDomCandidateInfo)) {
|
|
250
|
-
domCandidateInfo = retryDomCandidateInfo;
|
|
251
|
-
acquisitionReason = 'dom_retry_hit';
|
|
252
|
-
} else {
|
|
253
|
-
domCandidateInfo = null;
|
|
254
|
-
acquisitionReason = 'dom_profile_mismatch_unresolved';
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
domCandidateInfo = null;
|
|
259
|
-
acquisitionReason = `dom_profile_retry_failed:${normalizeText(error?.message || error)}`;
|
|
596
|
+
async ensurePanelsClosedBeforeOutreach({ initialResumeCloseResult = null } = {}) {
|
|
597
|
+
const runResumeLightClose = async () => {
|
|
598
|
+
if (initialResumeCloseResult) {
|
|
599
|
+
return initialResumeCloseResult;
|
|
260
600
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
domCandidateInfo,
|
|
265
|
-
networkCandidateInfo,
|
|
266
|
-
acquisitionReason,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async acquireResumeAndEvaluate(customer, profile, artifactDir, baseResult) {
|
|
271
|
-
let modalOpened = false;
|
|
272
|
-
let capture = null;
|
|
273
|
-
let lastResumeError = null;
|
|
274
|
-
const timings = {
|
|
275
|
-
initialNetworkWaitMs: 0,
|
|
276
|
-
networkRetryMs: 0,
|
|
277
|
-
imageCaptureMs: 0,
|
|
278
|
-
imageModelMs: 0,
|
|
279
|
-
lateNetworkRetryMs: 0,
|
|
280
|
-
domFallbackMs: 0,
|
|
281
|
-
textModelMs: 0,
|
|
282
|
-
};
|
|
283
|
-
const cardProfile = this.buildCardProfile(customer);
|
|
284
|
-
|
|
285
|
-
await this.waitResumeOpenCooldown(this.resumeOpenCooldownMs + Math.floor(Math.random() * 200));
|
|
286
|
-
await this.checkpoint();
|
|
287
|
-
const acquisitionStartedAt = Date.now();
|
|
288
|
-
const openResult = await this.page.openOnlineResume();
|
|
289
|
-
let openDetected = openResult ? Boolean(openResult?.detectedOpen) : true;
|
|
290
|
-
this.lastResumeOpenAt = Date.now();
|
|
291
|
-
modalOpened = openDetected;
|
|
292
|
-
await this.interaction.sleepRange(600, 220);
|
|
293
|
-
|
|
294
|
-
const rateLimit =
|
|
295
|
-
typeof this.page.getResumeRateLimitWarning === 'function'
|
|
296
|
-
? await this.page.getResumeRateLimitWarning()
|
|
297
|
-
: { hit: false, text: '' };
|
|
298
|
-
if (rateLimit?.hit) {
|
|
299
|
-
const backoffMs = 90000 + Math.floor(Math.random() * 30000);
|
|
300
|
-
this.setResumeOpenBlocked(backoffMs);
|
|
301
|
-
throw new Error(`RESUME_RATE_LIMIT_WARNING:${rateLimit.text}`);
|
|
302
|
-
}
|
|
303
|
-
if (openResult && !openDetected) {
|
|
304
|
-
let delayedDetected = false;
|
|
305
|
-
if (typeof this.page.getResumeModalState === 'function') {
|
|
306
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
307
|
-
const delayedState = await this.page.getResumeModalState();
|
|
308
|
-
delayedDetected =
|
|
309
|
-
Boolean(delayedState?.open) ||
|
|
310
|
-
Number(delayedState?.iframeCount || 0) > 0 ||
|
|
311
|
-
(Number(delayedState?.scopeCount || 0) > 0 && Number(delayedState?.closeCount || 0) > 0);
|
|
601
|
+
if (typeof this.page.closeResumeModalDomOnce === 'function') {
|
|
602
|
+
return this.page.closeResumeModalDomOnce();
|
|
312
603
|
}
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
modalOpened = true;
|
|
316
|
-
this.logger.log('在线简历首次检测未命中,1秒后复检已打开,继续处理。');
|
|
317
|
-
} else {
|
|
318
|
-
throw new Error('RESUME_MODAL_NOT_DETECTED_AFTER_SINGLE_DOM_CLICK');
|
|
604
|
+
if (typeof this.page.closeResumeModal === 'function') {
|
|
605
|
+
return this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
|
|
319
606
|
}
|
|
320
|
-
}
|
|
321
|
-
if (!openDetected) {
|
|
322
|
-
throw new Error('RESUME_MODAL_NOT_DETECTED');
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
let networkResult = null;
|
|
326
|
-
if (this.resumeNetworkTracker) {
|
|
327
|
-
networkResult = await this.resumeNetworkTracker.waitForResumeNetworkByMode(customer, {
|
|
328
|
-
minTs: acquisitionStartedAt,
|
|
329
|
-
});
|
|
330
|
-
timings.initialNetworkWaitMs = Number(networkResult?.initialWaitMs || 0);
|
|
331
|
-
timings.networkRetryMs = Number(networkResult?.retryWaitMs || 0);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (networkResult?.candidateInfo?.resumeText) {
|
|
335
|
-
await this.checkpoint();
|
|
336
|
-
const evaluationStartedAt = Date.now();
|
|
337
|
-
const evaluation = await this.llmClient.evaluateResume({
|
|
338
|
-
screeningCriteria: profile.screeningCriteria,
|
|
339
|
-
candidate: this.buildResumeCandidateContext(customer, networkResult.candidateInfo),
|
|
340
|
-
});
|
|
341
|
-
timings.textModelMs = Date.now() - evaluationStartedAt;
|
|
342
607
|
return {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
timings,
|
|
347
|
-
acquisitionMode: 'network',
|
|
348
|
-
acquisitionReason: networkResult.acquisitionReason || 'initial_network_hit',
|
|
349
|
-
sourceCandidateInfo: networkResult.candidateInfo,
|
|
608
|
+
closed: true,
|
|
609
|
+
method: 'unsupported',
|
|
610
|
+
finalState: { open: false, scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: '' },
|
|
350
611
|
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const captureStartedAt = Date.now();
|
|
356
|
-
capture = await this.resumeCaptureService.captureResume({
|
|
357
|
-
artifactDir,
|
|
358
|
-
waitResumeMs: 30000,
|
|
359
|
-
scrollSettleMs: 500,
|
|
360
|
-
});
|
|
361
|
-
timings.imageCaptureMs = Date.now() - captureStartedAt;
|
|
362
|
-
if (capture?.quality?.likelyBlank) {
|
|
363
|
-
const blankBackoffMs = 45000 + Math.floor(Math.random() * 20000);
|
|
364
|
-
this.setResumeOpenBlocked(blankBackoffMs);
|
|
365
|
-
throw new Error('RESUME_CAPTURE_LIKELY_BLANK');
|
|
612
|
+
};
|
|
613
|
+
const runDetailLightClose = async () => {
|
|
614
|
+
if (typeof this.page.closeCandidateDetailDomOnce === 'function') {
|
|
615
|
+
return this.page.closeCandidateDetailDomOnce();
|
|
366
616
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
: [];
|
|
370
|
-
this.logger.log(`截图完成:chunks=${capture.chunkCount} | modelImages=${modelImagePaths.length}`);
|
|
371
|
-
baseResult.artifacts.chunkDir = capture.chunkDir;
|
|
372
|
-
baseResult.artifacts.metadataFile = capture.metadataFile;
|
|
373
|
-
baseResult.artifacts.stitchedImage = capture.stitchedImage;
|
|
374
|
-
baseResult.artifacts.chunkCount = capture.chunkCount;
|
|
375
|
-
baseResult.artifacts.modelImagePaths = modelImagePaths;
|
|
376
|
-
|
|
377
|
-
if (this.resumeNetworkTracker) {
|
|
378
|
-
this.resumeNetworkTracker.setResumeAcquisitionMode('image', 'image_capture_success');
|
|
617
|
+
if (typeof this.page.closeCandidateDetail === 'function') {
|
|
618
|
+
return this.page.closeCandidateDetail({ maxAttempts: 1, ensureDismiss: false });
|
|
379
619
|
}
|
|
380
|
-
|
|
381
|
-
await this.checkpoint();
|
|
382
|
-
const imageEvalStartedAt = Date.now();
|
|
383
|
-
const evaluation = await this.llmClient.evaluateResume({
|
|
384
|
-
screeningCriteria: profile.screeningCriteria,
|
|
385
|
-
candidate: this.buildResumeCandidateContext(customer, null),
|
|
386
|
-
imagePaths: modelImagePaths,
|
|
387
|
-
});
|
|
388
|
-
timings.imageModelMs = Date.now() - imageEvalStartedAt;
|
|
389
620
|
return {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
621
|
+
closed: true,
|
|
622
|
+
method: 'unsupported',
|
|
623
|
+
finalState: {
|
|
624
|
+
open: false,
|
|
625
|
+
panelCount: 0,
|
|
626
|
+
closeCount: 0,
|
|
627
|
+
topPanelClass: '',
|
|
628
|
+
overlayClass: '',
|
|
629
|
+
contentClass: '',
|
|
630
|
+
},
|
|
397
631
|
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
632
|
+
};
|
|
633
|
+
const methodParts = [];
|
|
634
|
+
let retried = false;
|
|
635
|
+
let readyState = null;
|
|
636
|
+
|
|
637
|
+
let resumeResult = await runResumeLightClose();
|
|
638
|
+
let detailResult = await runDetailLightClose();
|
|
639
|
+
methodParts.push(`resume:${resumeResult?.method || 'unknown'}`);
|
|
640
|
+
methodParts.push(`detail:${detailResult?.method || 'unknown'}`);
|
|
641
|
+
this.logger.log(
|
|
642
|
+
`发送前首次关闭结果:resumeClosed=${Boolean(resumeResult?.closed)} | resumeMethod=${resumeResult?.method || 'unknown'} | detailClosed=${Boolean(detailResult?.closed)} | detailMethod=${detailResult?.method || 'unknown'}`,
|
|
643
|
+
);
|
|
401
644
|
|
|
402
|
-
let
|
|
403
|
-
|
|
404
|
-
lateNetworkResult = await this.resumeNetworkTracker.waitForLateNetworkResumeCandidateInfo(customer, {
|
|
405
|
-
minTs: acquisitionStartedAt,
|
|
406
|
-
});
|
|
407
|
-
timings.lateNetworkRetryMs = Number(lateNetworkResult?.lateRetryMs || 0);
|
|
408
|
-
}
|
|
409
|
-
if (lateNetworkResult?.candidateInfo?.resumeText) {
|
|
410
|
-
await this.checkpoint();
|
|
411
|
-
const evaluationStartedAt = Date.now();
|
|
412
|
-
const evaluation = await this.llmClient.evaluateResume({
|
|
413
|
-
screeningCriteria: profile.screeningCriteria,
|
|
414
|
-
candidate: this.buildResumeCandidateContext(customer, lateNetworkResult.candidateInfo),
|
|
415
|
-
});
|
|
416
|
-
timings.textModelMs = Date.now() - evaluationStartedAt;
|
|
417
|
-
return {
|
|
418
|
-
modalOpened,
|
|
419
|
-
capture,
|
|
420
|
-
evaluation,
|
|
421
|
-
timings,
|
|
422
|
-
acquisitionMode: 'network',
|
|
423
|
-
acquisitionReason: lateNetworkResult.acquisitionReason || 'late_network_hit',
|
|
424
|
-
sourceCandidateInfo: lateNetworkResult.candidateInfo,
|
|
425
|
-
};
|
|
426
|
-
}
|
|
645
|
+
let resumeClosed = Boolean(resumeResult?.closed);
|
|
646
|
+
let detailClosed = Boolean(detailResult?.closed);
|
|
427
647
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const evaluationStartedAt = Date.now();
|
|
434
|
-
const evaluation = await this.llmClient.evaluateResume({
|
|
435
|
-
screeningCriteria: profile.screeningCriteria,
|
|
436
|
-
candidate: this.buildResumeCandidateContext(customer, domFallback.networkCandidateInfo),
|
|
437
|
-
});
|
|
438
|
-
timings.textModelMs = Date.now() - evaluationStartedAt;
|
|
439
|
-
return {
|
|
440
|
-
modalOpened,
|
|
441
|
-
capture,
|
|
442
|
-
evaluation,
|
|
443
|
-
timings,
|
|
444
|
-
acquisitionMode: 'network',
|
|
445
|
-
acquisitionReason: domFallback.acquisitionReason || 'dom_retry_network_recheck_hit',
|
|
446
|
-
sourceCandidateInfo: domFallback.networkCandidateInfo,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
if (domFallback?.domCandidateInfo?.resumeText) {
|
|
450
|
-
await this.checkpoint();
|
|
451
|
-
const evaluationStartedAt = Date.now();
|
|
452
|
-
const evaluation = await this.llmClient.evaluateResume({
|
|
453
|
-
screeningCriteria: profile.screeningCriteria,
|
|
454
|
-
candidate: this.buildResumeCandidateContext(customer, domFallback.domCandidateInfo),
|
|
455
|
-
});
|
|
456
|
-
timings.textModelMs = Date.now() - evaluationStartedAt;
|
|
457
|
-
return {
|
|
458
|
-
modalOpened,
|
|
459
|
-
capture,
|
|
460
|
-
evaluation,
|
|
461
|
-
timings,
|
|
462
|
-
acquisitionMode: 'dom_fallback',
|
|
463
|
-
acquisitionReason: domFallback.acquisitionReason || 'dom_initial_hit',
|
|
464
|
-
sourceCandidateInfo: domFallback.domCandidateInfo,
|
|
465
|
-
};
|
|
648
|
+
if (!resumeClosed && typeof this.page.closeResumeModal === 'function') {
|
|
649
|
+
retried = true;
|
|
650
|
+
resumeResult = await this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
|
|
651
|
+
methodParts.push(`resume-retry:${resumeResult?.method || 'unknown'}`);
|
|
652
|
+
resumeClosed = Boolean(resumeResult?.closed);
|
|
466
653
|
}
|
|
467
654
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
await this.page.activatePrimaryChatLabel('全部');
|
|
655
|
+
if (!detailClosed && typeof this.page.closeCandidateDetail === 'function') {
|
|
656
|
+
retried = true;
|
|
657
|
+
detailResult = await this.page.closeCandidateDetail({ maxAttempts: 4, ensureDismiss: true });
|
|
658
|
+
methodParts.push(`detail-retry:${detailResult?.method || 'unknown'}`);
|
|
659
|
+
detailClosed = Boolean(detailResult?.closed);
|
|
474
660
|
}
|
|
475
|
-
await this.page.selectJob(profile.jobSelection);
|
|
476
|
-
return profile.startFrom === 'all'
|
|
477
|
-
? this.page.activateAllFilter()
|
|
478
|
-
: this.page.activateUnreadFilter();
|
|
479
|
-
}
|
|
480
661
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
maxWaitMs = CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS,
|
|
484
|
-
pollMs = CANDIDATE_LIST_WAIT_POLL_MS,
|
|
485
|
-
} = {}) {
|
|
486
|
-
const startedAt = Date.now();
|
|
487
|
-
let attempts = 0;
|
|
488
|
-
let lastState = null;
|
|
489
|
-
let lastError = '';
|
|
662
|
+
let resumeState = resumeResult?.finalState || null;
|
|
663
|
+
let detailState = detailResult?.finalState || null;
|
|
490
664
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (typeof this.page.getPageState === 'function') {
|
|
495
|
-
lastState = await this.page.getPageState();
|
|
496
|
-
if (Number(lastState?.listItemCount || 0) > 0) {
|
|
497
|
-
return {
|
|
498
|
-
ready: true,
|
|
499
|
-
waitedMs: Date.now() - startedAt,
|
|
500
|
-
attempts,
|
|
501
|
-
listItemCount: Number(lastState?.listItemCount || 0),
|
|
502
|
-
lastState,
|
|
503
|
-
lastError,
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
} else if (typeof this.page.getLoadedCustomers === 'function') {
|
|
507
|
-
const customers = await this.page.getLoadedCustomers();
|
|
508
|
-
if (Array.isArray(customers) && customers.length > 0) {
|
|
509
|
-
return {
|
|
510
|
-
ready: true,
|
|
511
|
-
waitedMs: Date.now() - startedAt,
|
|
512
|
-
attempts,
|
|
513
|
-
listItemCount: customers.length,
|
|
514
|
-
lastState,
|
|
515
|
-
lastError,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
} catch (error) {
|
|
520
|
-
lastError = String(error?.message || error || '');
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (Date.now() - startedAt >= maxWaitMs) {
|
|
524
|
-
break;
|
|
525
|
-
}
|
|
526
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
665
|
+
if (typeof this.page.getResumeModalState === 'function') {
|
|
666
|
+
resumeState = await this.page.getResumeModalState();
|
|
667
|
+
resumeClosed = !this.isResumeModalVisible(resumeState);
|
|
527
668
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
waitedMs: Date.now() - startedAt,
|
|
532
|
-
attempts,
|
|
533
|
-
listItemCount: Number(lastState?.listItemCount || 0),
|
|
534
|
-
lastState,
|
|
535
|
-
lastError,
|
|
536
|
-
reason,
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
async cleanupPanels({
|
|
541
|
-
resumeMaxAttempts = 6,
|
|
542
|
-
detailMaxAttempts = 4,
|
|
543
|
-
ensureDismiss = true,
|
|
544
|
-
} = {}) {
|
|
545
|
-
const resume =
|
|
546
|
-
typeof this.page.closeResumeModalDomOnce === 'function'
|
|
547
|
-
? await this.page.closeResumeModalDomOnce()
|
|
548
|
-
: await this.page.closeResumeModal({
|
|
549
|
-
maxAttempts: resumeMaxAttempts,
|
|
550
|
-
ensureDismiss,
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
let detail = {
|
|
554
|
-
closed: true,
|
|
555
|
-
method: 'unsupported',
|
|
556
|
-
finalState: {
|
|
557
|
-
panelCount: 0,
|
|
558
|
-
closeCount: 0,
|
|
559
|
-
topPanelClass: '',
|
|
560
|
-
},
|
|
561
|
-
};
|
|
562
|
-
if (typeof this.page.closeCandidateDetailDomOnce === 'function') {
|
|
563
|
-
detail = await this.page.closeCandidateDetailDomOnce();
|
|
564
|
-
if (!detail.closed && typeof this.page.closeCandidateDetail === 'function') {
|
|
565
|
-
detail = await this.page.closeCandidateDetail({
|
|
566
|
-
maxAttempts: detailMaxAttempts,
|
|
567
|
-
ensureDismiss,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
} else if (typeof this.page.closeCandidateDetail === 'function') {
|
|
571
|
-
detail = await this.page.closeCandidateDetail({
|
|
572
|
-
maxAttempts: detailMaxAttempts,
|
|
573
|
-
ensureDismiss,
|
|
574
|
-
});
|
|
669
|
+
if (typeof this.page.getCandidateDetailState === 'function') {
|
|
670
|
+
detailState = await this.page.getCandidateDetailState();
|
|
671
|
+
detailClosed = !this.isCandidateDetailVisible(detailState);
|
|
575
672
|
}
|
|
576
673
|
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
async run(profile) {
|
|
581
|
-
const startedAt = new Date().toISOString();
|
|
582
|
-
const runId = runToken(new Date());
|
|
583
|
-
const startFrom = profile.startFrom === 'all' ? 'all' : 'unread';
|
|
584
|
-
const filterLabel = startFrom === 'all' ? '全部' : '未读';
|
|
585
|
-
const targetCount =
|
|
586
|
-
Number.isFinite(Number(profile.targetCount)) && Number(profile.targetCount) > 0
|
|
587
|
-
? Number(profile.targetCount)
|
|
588
|
-
: null;
|
|
589
|
-
|
|
590
|
-
await this.stateStore.load();
|
|
591
|
-
try {
|
|
592
|
-
await this.page.ensureReady();
|
|
593
|
-
} catch (error) {
|
|
594
|
-
this.logger.log(`页面就绪检查告警:${error?.message || error},将继续执行预热恢复流程。`);
|
|
595
|
-
}
|
|
596
|
-
let filterResult = await this.restoreListContext(profile);
|
|
597
|
-
await this.interaction.sleepRange(420, 160);
|
|
598
|
-
let initialListWait = await this.waitForCandidateList({
|
|
599
|
-
reason: 'initial-context-restore',
|
|
600
|
-
});
|
|
601
|
-
if (initialListWait.ready) {
|
|
602
|
-
this.logger.log(
|
|
603
|
-
`候选人列表已就绪:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
|
|
604
|
-
);
|
|
605
|
-
} else {
|
|
674
|
+
if (retried) {
|
|
606
675
|
this.logger.log(
|
|
607
|
-
|
|
676
|
+
`发送前重试关闭结果:resumeClosed=${resumeClosed} | resumeMethod=${resumeResult?.method || 'unknown'} | detailClosed=${detailClosed} | detailMethod=${detailResult?.method || 'unknown'}`,
|
|
608
677
|
);
|
|
609
|
-
filterResult = await this.restoreListContext(profile);
|
|
610
|
-
await this.interaction.sleepRange(420, 160);
|
|
611
|
-
initialListWait = await this.waitForCandidateList({
|
|
612
|
-
reason: 'initial-context-restore-reapply',
|
|
613
|
-
});
|
|
614
|
-
if (initialListWait.ready) {
|
|
615
|
-
this.logger.log(
|
|
616
|
-
`候选人列表二次恢复成功:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
|
|
617
|
-
);
|
|
618
|
-
} else {
|
|
619
|
-
this.logger.log(
|
|
620
|
-
`候选人列表二次等待仍超时:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
678
|
}
|
|
624
|
-
this.logger.log('预热步骤:准备点击首位人选初始化聊天容器...');
|
|
625
|
-
let primedCustomer = null;
|
|
626
679
|
|
|
627
|
-
|
|
680
|
+
let failureReason = '';
|
|
681
|
+
if (resumeClosed && detailClosed) {
|
|
628
682
|
try {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
domIndex: Number.isFinite(candidate.domIndex) ? candidate.domIndex : 0,
|
|
636
|
-
textSnippet: '',
|
|
637
|
-
};
|
|
638
|
-
primedCustomer = {
|
|
639
|
-
...candidateBase,
|
|
640
|
-
customerKey: createCustomerKey(candidateBase),
|
|
641
|
-
};
|
|
642
|
-
this.logger.log(
|
|
643
|
-
`预热完成:name=${prime?.candidate?.name || '未知'} | job=${prime?.candidate?.sourceJob || '未知'} | id=${prime?.candidate?.customerId || '无'} | domIndex=${prime?.candidate?.domIndex ?? -1} | 候选人数=${prime?.totalVisibleCandidates ?? '未知'} | ready=${prime?.readyState?.hasOnlineResume ? 'online_resume' : prime?.readyState?.hasAskResume ? 'ask_resume' : 'unknown'}`,
|
|
644
|
-
);
|
|
683
|
+
readyState = await this.page.waitForConversationReady({
|
|
684
|
+
maxAttempts: 12,
|
|
685
|
+
delayMs: 220,
|
|
686
|
+
requirePanelsClosed: true,
|
|
687
|
+
});
|
|
688
|
+
methodParts.push('ready:strict');
|
|
645
689
|
} catch (error) {
|
|
646
|
-
|
|
690
|
+
failureReason = `strict-ready-check-failed:${error?.message || error}`;
|
|
691
|
+
methodParts.push(failureReason);
|
|
647
692
|
}
|
|
693
|
+
} else {
|
|
694
|
+
failureReason = [
|
|
695
|
+
!resumeClosed ? 'resume-modal-still-open' : '',
|
|
696
|
+
!detailClosed ? 'candidate-detail-still-open' : '',
|
|
697
|
+
].filter(Boolean).join('+');
|
|
648
698
|
}
|
|
649
699
|
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
screeningCriteria: profile.screeningCriteria,
|
|
657
|
-
targetCount,
|
|
658
|
-
chromePort: profile.chrome.port,
|
|
659
|
-
model: profile.llm.model,
|
|
660
|
-
startFrom,
|
|
661
|
-
jobSelection: profile.jobSelection,
|
|
662
|
-
},
|
|
663
|
-
inspected: 0,
|
|
664
|
-
passed: 0,
|
|
665
|
-
requested: 0,
|
|
666
|
-
skipped: 0,
|
|
667
|
-
errors: 0,
|
|
668
|
-
exhausted: false,
|
|
669
|
-
stopped: false,
|
|
670
|
-
stopReason: '',
|
|
671
|
-
results,
|
|
672
|
-
reportPath: null,
|
|
700
|
+
const diagnostics = {
|
|
701
|
+
preActionResumeClosed: resumeClosed,
|
|
702
|
+
preActionDetailClosed: detailClosed,
|
|
703
|
+
preActionCleanupMethod: methodParts.join('|'),
|
|
704
|
+
preActionCleanupRetried: retried,
|
|
705
|
+
preActionCleanupFailureReason: failureReason,
|
|
673
706
|
};
|
|
674
707
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
);
|
|
684
|
-
this.logger.log(this.formatProgress(summary));
|
|
685
|
-
this.emitProgress(summary, {
|
|
686
|
-
stage: 'running',
|
|
687
|
-
message: '任务已启动。',
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
let consecutiveErrors = 0;
|
|
691
|
-
let exhaustedScrolls = 0;
|
|
692
|
-
let noMoreMarkerHits = 0;
|
|
693
|
-
let fallbackBottomHits = 0;
|
|
694
|
-
const noMoreMarkerConfirmations = 2;
|
|
695
|
-
const exhaustedScrollLimit = targetCount ? 10 : 60;
|
|
696
|
-
const fallbackBottomLimit = targetCount ? 4 : 12;
|
|
697
|
-
|
|
698
|
-
try {
|
|
699
|
-
while (shouldContinue(summary, targetCount)) {
|
|
700
|
-
await this.checkpoint();
|
|
701
|
-
if (this.resumeOpenBlockedUntil > Date.now()) {
|
|
702
|
-
const remainMs = this.resumeOpenBlockedUntil - Date.now();
|
|
703
|
-
this.logger.log(
|
|
704
|
-
`简历查看冷却中:remaining=${Math.ceil(remainMs / 1000)}s,暂停打开新简历以避免频控。`,
|
|
705
|
-
);
|
|
706
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(remainMs, 30000)));
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
await this.interaction.maybeRest(summary.inspected, this.logger);
|
|
710
|
-
await this.checkpoint();
|
|
711
|
-
|
|
712
|
-
let loadedCustomers = [];
|
|
713
|
-
try {
|
|
714
|
-
loadedCustomers = await this.page.getLoadedCustomers();
|
|
715
|
-
} catch (error) {
|
|
716
|
-
const message = String(error?.message || error || '');
|
|
717
|
-
this.logger.log(`候选人扫描异常:${message}`);
|
|
718
|
-
if (
|
|
719
|
-
/CHAT_CARD_LIST_NOT_FOUND|CHAT_LIST_CONTAINER_NOT_FOUND|ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE/.test(
|
|
720
|
-
message,
|
|
721
|
-
)
|
|
722
|
-
) {
|
|
723
|
-
const delayedListWait = await this.waitForCandidateList({
|
|
724
|
-
reason: `main-loop:${message}`,
|
|
725
|
-
});
|
|
726
|
-
if (delayedListWait.ready) {
|
|
727
|
-
this.logger.log(
|
|
728
|
-
`候选人列表延迟恢复成功:reason=main-loop:${message} | waited=${delayedListWait.waitedMs}ms | attempts=${delayedListWait.attempts} | count=${delayedListWait.listItemCount},继续重试扫描。`,
|
|
729
|
-
);
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
try {
|
|
733
|
-
const recover = await this.page.recoverToChatIndex();
|
|
734
|
-
this.logger.log(
|
|
735
|
-
`页面恢复:changed=${recover.changed} | href=${recover.href || 'unknown'},准备重新预热并继续。`,
|
|
736
|
-
);
|
|
737
|
-
await this.interaction.sleepRange(900, 220);
|
|
738
|
-
let recoveredFilterResult = await this.restoreListContext(profile);
|
|
739
|
-
this.logger.log(
|
|
740
|
-
`恢复后列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
741
|
-
recoveredFilterResult.changed
|
|
742
|
-
? recoveredFilterResult.verified === false
|
|
743
|
-
? '(已尝试切换,未验证 active)'
|
|
744
|
-
: '(已切换)'
|
|
745
|
-
: '(已在目标筛选)'
|
|
746
|
-
}${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
|
|
747
|
-
);
|
|
748
|
-
let recoveredListWait = await this.waitForCandidateList({
|
|
749
|
-
reason: 'post-recovery-context-restore',
|
|
750
|
-
});
|
|
751
|
-
if (recoveredListWait.ready) {
|
|
752
|
-
this.logger.log(
|
|
753
|
-
`恢复后候选人列表已就绪:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
|
|
754
|
-
);
|
|
755
|
-
} else {
|
|
756
|
-
this.logger.log(
|
|
757
|
-
`恢复后候选人列表等待超时:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
758
|
-
);
|
|
759
|
-
recoveredFilterResult = await this.restoreListContext(profile);
|
|
760
|
-
this.logger.log(
|
|
761
|
-
`恢复后二次应用列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
762
|
-
recoveredFilterResult.changed
|
|
763
|
-
? recoveredFilterResult.verified === false
|
|
764
|
-
? '(已尝试切换,未验证 active)'
|
|
765
|
-
: '(已切换)'
|
|
766
|
-
: '(已在目标筛选)'
|
|
767
|
-
}${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
|
|
768
|
-
);
|
|
769
|
-
await this.interaction.sleepRange(420, 160);
|
|
770
|
-
recoveredListWait = await this.waitForCandidateList({
|
|
771
|
-
reason: 'post-recovery-context-restore-reapply',
|
|
772
|
-
});
|
|
773
|
-
if (recoveredListWait.ready) {
|
|
774
|
-
this.logger.log(
|
|
775
|
-
`恢复后二次候选人列表恢复成功:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
|
|
776
|
-
);
|
|
777
|
-
} else {
|
|
778
|
-
this.logger.log(
|
|
779
|
-
`恢复后二次候选人列表等待仍超时:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
const prime = await this.page.primeConversationByFirstCandidate();
|
|
784
|
-
const candidate = prime?.candidate || {};
|
|
785
|
-
const candidateBase = {
|
|
786
|
-
customerId: candidate.customerId || '',
|
|
787
|
-
name: candidate.name || '',
|
|
788
|
-
sourceJob: candidate.sourceJob || '',
|
|
789
|
-
domIndex: Number.isFinite(candidate.domIndex) ? candidate.domIndex : 0,
|
|
790
|
-
textSnippet: '',
|
|
791
|
-
};
|
|
792
|
-
primedCustomer = {
|
|
793
|
-
...candidateBase,
|
|
794
|
-
customerKey: createCustomerKey(candidateBase),
|
|
795
|
-
};
|
|
796
|
-
this.logger.log(
|
|
797
|
-
`恢复后预热完成:name=${prime?.candidate?.name || '未知'} | id=${prime?.candidate?.customerId || '无'}`,
|
|
798
|
-
);
|
|
799
|
-
continue;
|
|
800
|
-
} catch (recoverError) {
|
|
801
|
-
throw new Error(
|
|
802
|
-
`CHAT_LIST_RECOVERY_FAILED: ${recoverError?.message || recoverError}`,
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
throw error;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const shouldUsePrimedFirst =
|
|
810
|
-
Boolean(primedCustomer) &&
|
|
811
|
-
(startFrom === 'unread' || !this.stateStore.hasAny(createCustomerAliases(primedCustomer)));
|
|
812
|
-
if (shouldUsePrimedFirst && primedCustomer) {
|
|
813
|
-
this.logger.log(
|
|
814
|
-
`优先处理预热候选人:name=${primedCustomer.name || '未知'} | key=${primedCustomer.customerKey}`,
|
|
815
|
-
);
|
|
816
|
-
const result = await this.processCustomer(primedCustomer, profile, runId, {
|
|
817
|
-
skipCardClick: true,
|
|
818
|
-
});
|
|
819
|
-
primedCustomer = null;
|
|
820
|
-
results.push(result);
|
|
821
|
-
summary.inspected += 1;
|
|
822
|
-
|
|
823
|
-
if (result.error) {
|
|
824
|
-
summary.errors += 1;
|
|
825
|
-
consecutiveErrors += 1;
|
|
826
|
-
} else {
|
|
827
|
-
consecutiveErrors = 0;
|
|
828
|
-
}
|
|
829
|
-
if (result.passed) summary.passed += 1;
|
|
830
|
-
if (result.requested) summary.requested += 1;
|
|
831
|
-
if (!result.passed && !result.error) summary.skipped += 1;
|
|
832
|
-
|
|
833
|
-
this.logger.log(
|
|
834
|
-
`候选人结果: ${result.name || '未知'} | ${result.passed ? 'passed' : result.error ? 'error' : 'skipped'}${result.reason ? ` | ${result.reason}` : ''}${result.error ? ` | ${result.error}` : ''}`,
|
|
835
|
-
);
|
|
836
|
-
this.logger.log(this.formatProgress(summary));
|
|
837
|
-
this.emitProgress(summary, {
|
|
838
|
-
stage: 'running',
|
|
839
|
-
message: `已处理候选人:${result.name || '未知'}`,
|
|
840
|
-
});
|
|
841
|
-
exhaustedScrolls = 0;
|
|
842
|
-
noMoreMarkerHits = 0;
|
|
843
|
-
fallbackBottomHits = 0;
|
|
844
|
-
if (consecutiveErrors >= 3) {
|
|
845
|
-
this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
continue;
|
|
849
|
-
}
|
|
850
|
-
primedCustomer = null;
|
|
851
|
-
|
|
852
|
-
this.logger.log(`候选人扫描:当前可见 ${loadedCustomers.length} 位`);
|
|
853
|
-
const nextCustomer = loadedCustomers.find(
|
|
854
|
-
(customer) => !this.stateStore.hasAny(createCustomerAliases(customer)),
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
if (!nextCustomer) {
|
|
858
|
-
const ratio = 0.52 + Math.random() * 0.34;
|
|
859
|
-
const scrollResult = await this.page.scrollCustomerList(ratio);
|
|
860
|
-
const noMoreDetected =
|
|
861
|
-
Boolean(scrollResult.noMoreDetectedAfter) || Boolean(scrollResult.noMoreDetectedBefore);
|
|
862
|
-
this.logger.log(
|
|
863
|
-
`列表滚动:ratio=${ratio.toFixed(2)} | didScroll=${Boolean(scrollResult.didScroll)} | top=${scrollResult.after?.top ?? 'n/a'} | atBottom=${Boolean(scrollResult.atBottom)} | noMore=${noMoreDetected}${scrollResult.noMoreTextAfter ? `(${scrollResult.noMoreTextAfter})` : ''} | scrollRetry=${exhaustedScrolls + 1}`,
|
|
864
|
-
);
|
|
865
|
-
if (noMoreDetected) {
|
|
866
|
-
noMoreMarkerHits += 1;
|
|
867
|
-
if (noMoreMarkerHits >= noMoreMarkerConfirmations) {
|
|
868
|
-
summary.exhausted = true;
|
|
869
|
-
this.logger.log('列表滚动终止:检测到“没有更多了”标识,判定为 exhausted。');
|
|
870
|
-
break;
|
|
871
|
-
}
|
|
872
|
-
await this.interaction.sleepRange(920, 260);
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
noMoreMarkerHits = 0;
|
|
877
|
-
exhaustedScrolls = scrollResult.didScroll ? exhaustedScrolls + 1 : exhaustedScrolls + 2;
|
|
878
|
-
fallbackBottomHits = scrollResult.atBottom ? fallbackBottomHits + 1 : 0;
|
|
879
|
-
if (fallbackBottomHits >= fallbackBottomLimit && exhaustedScrolls >= Math.ceil(exhaustedScrollLimit / 2)) {
|
|
880
|
-
summary.exhausted = true;
|
|
881
|
-
this.logger.log('列表滚动终止:未发现“没有更多了”标识,但已多次触底且无可处理候选人,判定为 exhausted。');
|
|
882
|
-
break;
|
|
883
|
-
}
|
|
884
|
-
if (exhaustedScrolls >= exhaustedScrollLimit) {
|
|
885
|
-
summary.exhausted = true;
|
|
886
|
-
this.logger.log('列表滚动终止:连续无可处理候选人达到保护上限,判定为 exhausted。');
|
|
887
|
-
break;
|
|
888
|
-
}
|
|
889
|
-
await this.interaction.sleepRange(920, 260);
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
exhaustedScrolls = 0;
|
|
894
|
-
noMoreMarkerHits = 0;
|
|
895
|
-
fallbackBottomHits = 0;
|
|
896
|
-
this.logger.log(
|
|
897
|
-
`准备处理候选人:name=${nextCustomer.name || '未知'} | key=${nextCustomer.customerKey} | job=${nextCustomer.sourceJob || '未知'} | domIndex=${nextCustomer.domIndex}`,
|
|
898
|
-
);
|
|
899
|
-
const result = await this.processCustomer(nextCustomer, profile, runId, {
|
|
900
|
-
skipCardClick: false,
|
|
901
|
-
});
|
|
902
|
-
results.push(result);
|
|
903
|
-
summary.inspected += 1;
|
|
904
|
-
|
|
905
|
-
if (result.error) {
|
|
906
|
-
summary.errors += 1;
|
|
907
|
-
consecutiveErrors += 1;
|
|
908
|
-
} else {
|
|
909
|
-
consecutiveErrors = 0;
|
|
910
|
-
}
|
|
911
|
-
if (result.passed) summary.passed += 1;
|
|
912
|
-
if (result.requested) summary.requested += 1;
|
|
913
|
-
if (!result.passed && !result.error) summary.skipped += 1;
|
|
914
|
-
|
|
915
|
-
this.logger.log(
|
|
916
|
-
`候选人结果: ${result.name || '未知'} | ${result.passed ? 'passed' : result.error ? 'error' : 'skipped'}${result.reason ? ` | ${result.reason}` : ''}${result.error ? ` | ${result.error}` : ''}`,
|
|
917
|
-
);
|
|
918
|
-
this.logger.log(this.formatProgress(summary));
|
|
919
|
-
this.emitProgress(summary, {
|
|
920
|
-
stage: 'running',
|
|
921
|
-
message: `已处理候选人:${result.name || '未知'}`,
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
if (consecutiveErrors >= 3) {
|
|
925
|
-
this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
|
|
926
|
-
break;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
} catch (error) {
|
|
930
|
-
if (error?.name !== 'StopRequestedError') {
|
|
931
|
-
throw error;
|
|
932
|
-
}
|
|
933
|
-
summary.stopped = true;
|
|
934
|
-
summary.stopReason = error.message;
|
|
935
|
-
this.emitProgress(summary, {
|
|
936
|
-
stage: 'running',
|
|
937
|
-
message: `运行停止:${summary.stopReason}`,
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
try {
|
|
942
|
-
const finalClose = await this.cleanupPanels({
|
|
943
|
-
resumeMaxAttempts: 6,
|
|
944
|
-
detailMaxAttempts: 4,
|
|
945
|
-
ensureDismiss: true,
|
|
946
|
-
});
|
|
947
|
-
this.logger.log(
|
|
948
|
-
`运行收尾关闭弹层:resumeClosed=${finalClose.resume.closed} | resumeMethod=${finalClose.resume.method} | detailClosed=${finalClose.detail.closed} | detailMethod=${finalClose.detail.method}`,
|
|
949
|
-
);
|
|
950
|
-
} catch (cleanupError) {
|
|
951
|
-
this.logger.log(`运行收尾清理告警:${cleanupError?.message || cleanupError}`);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
summary.finishedAt = new Date().toISOString();
|
|
955
|
-
summary.reportPath = await this.reportStore.write(summary);
|
|
956
|
-
this.emitProgress(summary, {
|
|
957
|
-
stage: 'finalize',
|
|
958
|
-
message: summary.stopped ? '任务已停止并完成收尾。' : '任务执行完成。',
|
|
959
|
-
});
|
|
960
|
-
return summary;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
async processCustomer(customer, profile, runId, options = {}) {
|
|
964
|
-
const skipCardClick = Boolean(options?.skipCardClick);
|
|
965
|
-
const baseAliases = createCustomerAliases(customer);
|
|
966
|
-
const baseResult = {
|
|
967
|
-
customerKey: customer.customerKey,
|
|
968
|
-
name: customer.name || '',
|
|
969
|
-
sourceJob: customer.sourceJob || '',
|
|
970
|
-
decision: 'skipped',
|
|
971
|
-
passed: false,
|
|
972
|
-
requested: false,
|
|
973
|
-
reason: '',
|
|
974
|
-
error: '',
|
|
975
|
-
artifacts: {},
|
|
708
|
+
return {
|
|
709
|
+
ok: resumeClosed && detailClosed && Boolean(readyState?.panelsClosed),
|
|
710
|
+
readyState,
|
|
711
|
+
diagnostics,
|
|
712
|
+
resumeResult,
|
|
713
|
+
detailResult,
|
|
714
|
+
resumeState,
|
|
715
|
+
detailState,
|
|
976
716
|
};
|
|
717
|
+
}
|
|
977
718
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
719
|
+
async run(profile) {
|
|
720
|
+
const startedAt = new Date().toISOString();
|
|
721
|
+
const runId = runToken(new Date());
|
|
722
|
+
const startFrom = profile.startFrom === 'all' ? 'all' : 'unread';
|
|
723
|
+
const filterLabel = startFrom === 'all' ? '全部' : '未读';
|
|
724
|
+
const targetCount =
|
|
725
|
+
Number.isFinite(Number(profile.targetCount)) && Number(profile.targetCount) > 0
|
|
726
|
+
? Number(profile.targetCount)
|
|
727
|
+
: null;
|
|
728
|
+
|
|
729
|
+
await this.stateStore.load();
|
|
730
|
+
try {
|
|
731
|
+
await this.page.ensureReady();
|
|
732
|
+
} catch (error) {
|
|
733
|
+
this.logger.log(`页面就绪检查告警:${error?.message || error},将继续执行预热恢复流程。`);
|
|
734
|
+
}
|
|
735
|
+
let filterResult = await this.restoreListContext(profile);
|
|
736
|
+
await this.interaction.sleepRange(420, 160);
|
|
737
|
+
let initialListWait = await this.waitForCandidateList({
|
|
738
|
+
reason: 'initial-context-restore',
|
|
739
|
+
});
|
|
740
|
+
if (initialListWait.ready) {
|
|
741
|
+
this.logger.log(
|
|
742
|
+
`候选人列表已就绪:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
|
|
743
|
+
);
|
|
744
|
+
} else {
|
|
745
|
+
this.logger.log(
|
|
746
|
+
`候选人列表等待超时:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
747
|
+
);
|
|
748
|
+
filterResult = await this.restoreListContext(profile);
|
|
749
|
+
await this.interaction.sleepRange(420, 160);
|
|
750
|
+
initialListWait = await this.waitForCandidateList({
|
|
751
|
+
reason: 'initial-context-restore-reapply',
|
|
752
|
+
});
|
|
753
|
+
if (initialListWait.ready) {
|
|
754
|
+
this.logger.log(
|
|
755
|
+
`候选人列表二次恢复成功:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
|
|
756
|
+
);
|
|
757
|
+
} else {
|
|
758
|
+
this.logger.log(
|
|
759
|
+
`候选人列表二次等待仍超时:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
this.logger.log('预热步骤:准备点击首位人选初始化聊天容器...');
|
|
764
|
+
let primedCustomer = null;
|
|
765
|
+
|
|
766
|
+
if (typeof this.page.primeConversationByFirstCandidate === 'function') {
|
|
767
|
+
try {
|
|
768
|
+
const prime = await this.page.primeConversationByFirstCandidate();
|
|
769
|
+
const candidate = prime?.candidate || {};
|
|
770
|
+
const candidateBase = {
|
|
771
|
+
customerId: candidate.customerId || '',
|
|
772
|
+
name: candidate.name || '',
|
|
773
|
+
sourceJob: candidate.sourceJob || '',
|
|
774
|
+
domIndex: Number.isFinite(candidate.domIndex) ? candidate.domIndex : 0,
|
|
775
|
+
textSnippet: '',
|
|
776
|
+
};
|
|
777
|
+
primedCustomer = {
|
|
778
|
+
...candidateBase,
|
|
779
|
+
customerKey: createCustomerKey(candidateBase),
|
|
780
|
+
};
|
|
781
|
+
this.logger.log(
|
|
782
|
+
`预热完成:name=${prime?.candidate?.name || '未知'} | job=${prime?.candidate?.sourceJob || '未知'} | id=${prime?.candidate?.customerId || '无'} | domIndex=${prime?.candidate?.domIndex ?? -1} | 候选人数=${prime?.totalVisibleCandidates ?? '未知'} | ready=${prime?.readyState?.hasOnlineResume ? 'online_resume' : prime?.readyState?.hasAskResume ? 'ask_resume' : 'unknown'}`,
|
|
783
|
+
);
|
|
784
|
+
} catch (error) {
|
|
785
|
+
this.logger.log(`预热失败:${error?.message || error}(将继续尝试主循环)`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const results = [];
|
|
790
|
+
const summary = {
|
|
791
|
+
startedAt,
|
|
792
|
+
finishedAt: null,
|
|
793
|
+
dryRun: this.dryRun,
|
|
794
|
+
profile: {
|
|
795
|
+
screeningCriteria: profile.screeningCriteria,
|
|
796
|
+
targetCount,
|
|
797
|
+
chromePort: profile.chrome.port,
|
|
798
|
+
model: profile.llm.model,
|
|
799
|
+
startFrom,
|
|
800
|
+
jobSelection: profile.jobSelection,
|
|
801
|
+
},
|
|
802
|
+
inspected: 0,
|
|
803
|
+
passed: 0,
|
|
804
|
+
requested: 0,
|
|
805
|
+
skipped: 0,
|
|
806
|
+
errors: 0,
|
|
807
|
+
exhausted: false,
|
|
808
|
+
stopped: false,
|
|
809
|
+
stopReason: '',
|
|
810
|
+
results,
|
|
811
|
+
reportPath: null,
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
this.logger.log(
|
|
815
|
+
`岗位: ${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
816
|
+
filterResult.changed
|
|
817
|
+
? filterResult.verified === false
|
|
818
|
+
? '(已尝试切换,未验证 active)'
|
|
819
|
+
: '(已切换)'
|
|
820
|
+
: '(已在目标筛选)'
|
|
821
|
+
}${filterResult?.activeLabel ? ` | active=${filterResult.activeLabel}` : ''}`,
|
|
822
|
+
);
|
|
823
|
+
this.logger.log(this.formatProgress(summary));
|
|
824
|
+
this.emitProgress(summary, {
|
|
825
|
+
stage: 'running',
|
|
826
|
+
message: '任务已启动。',
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
let consecutiveErrors = 0;
|
|
830
|
+
let exhaustedScrolls = 0;
|
|
831
|
+
let noMoreMarkerHits = 0;
|
|
832
|
+
let fallbackBottomHits = 0;
|
|
833
|
+
const noMoreMarkerConfirmations = 2;
|
|
834
|
+
const exhaustedScrollLimit = targetCount ? 10 : 60;
|
|
835
|
+
const fallbackBottomLimit = targetCount ? 4 : 12;
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
while (shouldContinue(summary, targetCount)) {
|
|
839
|
+
await this.checkpoint();
|
|
840
|
+
if (this.resumeOpenBlockedUntil > Date.now()) {
|
|
841
|
+
const remainMs = this.resumeOpenBlockedUntil - Date.now();
|
|
842
|
+
this.logger.log(
|
|
843
|
+
`简历查看冷却中:remaining=${Math.ceil(remainMs / 1000)}s,暂停打开新简历以避免频控。`,
|
|
844
|
+
);
|
|
845
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(remainMs, 30000)));
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
await this.interaction.maybeRest(summary.inspected, this.logger);
|
|
849
|
+
await this.checkpoint();
|
|
850
|
+
|
|
851
|
+
let loadedCustomers = [];
|
|
852
|
+
try {
|
|
853
|
+
loadedCustomers = await this.page.getLoadedCustomers();
|
|
854
|
+
} catch (error) {
|
|
855
|
+
const message = String(error?.message || error || '');
|
|
856
|
+
this.logger.log(`候选人扫描异常:${message}`);
|
|
857
|
+
if (
|
|
858
|
+
/CHAT_CARD_LIST_NOT_FOUND|CHAT_LIST_CONTAINER_NOT_FOUND|ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE/.test(
|
|
859
|
+
message,
|
|
860
|
+
)
|
|
861
|
+
) {
|
|
862
|
+
const delayedListWait = await this.waitForCandidateList({
|
|
863
|
+
reason: `main-loop:${message}`,
|
|
864
|
+
});
|
|
865
|
+
if (delayedListWait.ready) {
|
|
866
|
+
this.logger.log(
|
|
867
|
+
`候选人列表延迟恢复成功:reason=main-loop:${message} | waited=${delayedListWait.waitedMs}ms | attempts=${delayedListWait.attempts} | count=${delayedListWait.listItemCount},继续重试扫描。`,
|
|
868
|
+
);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
try {
|
|
872
|
+
const recover = await this.page.recoverToChatIndex();
|
|
873
|
+
this.logger.log(
|
|
874
|
+
`页面恢复:changed=${recover.changed} | href=${recover.href || 'unknown'},准备重新预热并继续。`,
|
|
875
|
+
);
|
|
876
|
+
await this.interaction.sleepRange(900, 220);
|
|
877
|
+
let recoveredFilterResult = await this.restoreListContext(profile);
|
|
878
|
+
this.logger.log(
|
|
879
|
+
`恢复后列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
880
|
+
recoveredFilterResult.changed
|
|
881
|
+
? recoveredFilterResult.verified === false
|
|
882
|
+
? '(已尝试切换,未验证 active)'
|
|
883
|
+
: '(已切换)'
|
|
884
|
+
: '(已在目标筛选)'
|
|
885
|
+
}${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
|
|
886
|
+
);
|
|
887
|
+
let recoveredListWait = await this.waitForCandidateList({
|
|
888
|
+
reason: 'post-recovery-context-restore',
|
|
889
|
+
});
|
|
890
|
+
if (recoveredListWait.ready) {
|
|
891
|
+
this.logger.log(
|
|
892
|
+
`恢复后候选人列表已就绪:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
|
|
893
|
+
);
|
|
894
|
+
} else {
|
|
895
|
+
this.logger.log(
|
|
896
|
+
`恢复后候选人列表等待超时:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
897
|
+
);
|
|
898
|
+
recoveredFilterResult = await this.restoreListContext(profile);
|
|
899
|
+
this.logger.log(
|
|
900
|
+
`恢复后二次应用列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
901
|
+
recoveredFilterResult.changed
|
|
902
|
+
? recoveredFilterResult.verified === false
|
|
903
|
+
? '(已尝试切换,未验证 active)'
|
|
904
|
+
: '(已切换)'
|
|
905
|
+
: '(已在目标筛选)'
|
|
906
|
+
}${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
|
|
907
|
+
);
|
|
908
|
+
await this.interaction.sleepRange(420, 160);
|
|
909
|
+
recoveredListWait = await this.waitForCandidateList({
|
|
910
|
+
reason: 'post-recovery-context-restore-reapply',
|
|
911
|
+
});
|
|
912
|
+
if (recoveredListWait.ready) {
|
|
913
|
+
this.logger.log(
|
|
914
|
+
`恢复后二次候选人列表恢复成功:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
|
|
915
|
+
);
|
|
916
|
+
} else {
|
|
917
|
+
this.logger.log(
|
|
918
|
+
`恢复后二次候选人列表等待仍超时:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const prime = await this.page.primeConversationByFirstCandidate();
|
|
923
|
+
const candidate = prime?.candidate || {};
|
|
924
|
+
const candidateBase = {
|
|
925
|
+
customerId: candidate.customerId || '',
|
|
926
|
+
name: candidate.name || '',
|
|
927
|
+
sourceJob: candidate.sourceJob || '',
|
|
928
|
+
domIndex: Number.isFinite(candidate.domIndex) ? candidate.domIndex : 0,
|
|
929
|
+
textSnippet: '',
|
|
930
|
+
};
|
|
931
|
+
primedCustomer = {
|
|
932
|
+
...candidateBase,
|
|
933
|
+
customerKey: createCustomerKey(candidateBase),
|
|
934
|
+
};
|
|
935
|
+
this.logger.log(
|
|
936
|
+
`恢复后预热完成:name=${prime?.candidate?.name || '未知'} | id=${prime?.candidate?.customerId || '无'}`,
|
|
937
|
+
);
|
|
938
|
+
continue;
|
|
939
|
+
} catch (recoverError) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`CHAT_LIST_RECOVERY_FAILED: ${recoverError?.message || recoverError}`,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const shouldUsePrimedFirst =
|
|
949
|
+
Boolean(primedCustomer) &&
|
|
950
|
+
(startFrom === 'unread' || !this.stateStore.hasAny(createCustomerAliases(primedCustomer)));
|
|
951
|
+
if (shouldUsePrimedFirst && primedCustomer) {
|
|
952
|
+
this.logger.log(
|
|
953
|
+
`优先处理预热候选人:name=${primedCustomer.name || '未知'} | key=${primedCustomer.customerKey}`,
|
|
954
|
+
);
|
|
955
|
+
const result = await this.processCustomer(primedCustomer, profile, runId, {
|
|
956
|
+
skipCardClick: true,
|
|
957
|
+
});
|
|
958
|
+
primedCustomer = null;
|
|
959
|
+
results.push(result);
|
|
960
|
+
summary.inspected += 1;
|
|
961
|
+
|
|
962
|
+
if (result.error) {
|
|
963
|
+
summary.errors += 1;
|
|
964
|
+
consecutiveErrors += 1;
|
|
965
|
+
} else {
|
|
966
|
+
consecutiveErrors = 0;
|
|
967
|
+
}
|
|
968
|
+
if (result.passed) summary.passed += 1;
|
|
969
|
+
if (result.requested) summary.requested += 1;
|
|
970
|
+
if (!result.passed && !result.error) summary.skipped += 1;
|
|
971
|
+
|
|
972
|
+
this.logger.log(
|
|
973
|
+
`候选人结果: ${result.name || '未知'} | ${result.passed ? 'passed' : result.error ? 'error' : 'skipped'}${result.reason ? ` | ${result.reason}` : ''}${result.error ? ` | ${result.error}` : ''}`,
|
|
974
|
+
);
|
|
975
|
+
this.logger.log(this.formatProgress(summary));
|
|
976
|
+
this.emitProgress(summary, {
|
|
977
|
+
stage: 'running',
|
|
978
|
+
message: `已处理候选人:${result.name || '未知'}`,
|
|
979
|
+
});
|
|
980
|
+
exhaustedScrolls = 0;
|
|
981
|
+
noMoreMarkerHits = 0;
|
|
982
|
+
fallbackBottomHits = 0;
|
|
983
|
+
if (consecutiveErrors >= 3) {
|
|
984
|
+
this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
primedCustomer = null;
|
|
990
|
+
|
|
991
|
+
this.logger.log(`候选人扫描:当前可见 ${loadedCustomers.length} 位`);
|
|
992
|
+
const nextCustomer = loadedCustomers.find(
|
|
993
|
+
(customer) => !this.stateStore.hasAny(createCustomerAliases(customer)),
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
if (!nextCustomer) {
|
|
997
|
+
const ratio = 0.52 + Math.random() * 0.34;
|
|
998
|
+
const scrollResult = await this.page.scrollCustomerList(ratio);
|
|
999
|
+
const noMoreDetected =
|
|
1000
|
+
Boolean(scrollResult.noMoreDetectedAfter) || Boolean(scrollResult.noMoreDetectedBefore);
|
|
1001
|
+
this.logger.log(
|
|
1002
|
+
`列表滚动:ratio=${ratio.toFixed(2)} | didScroll=${Boolean(scrollResult.didScroll)} | top=${scrollResult.after?.top ?? 'n/a'} | atBottom=${Boolean(scrollResult.atBottom)} | noMore=${noMoreDetected}${scrollResult.noMoreTextAfter ? `(${scrollResult.noMoreTextAfter})` : ''} | scrollRetry=${exhaustedScrolls + 1}`,
|
|
1003
|
+
);
|
|
1004
|
+
if (noMoreDetected) {
|
|
1005
|
+
noMoreMarkerHits += 1;
|
|
1006
|
+
if (noMoreMarkerHits >= noMoreMarkerConfirmations) {
|
|
1007
|
+
summary.exhausted = true;
|
|
1008
|
+
this.logger.log('列表滚动终止:检测到“没有更多了”标识,判定为 exhausted。');
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
await this.interaction.sleepRange(920, 260);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
noMoreMarkerHits = 0;
|
|
1016
|
+
exhaustedScrolls = scrollResult.didScroll ? exhaustedScrolls + 1 : exhaustedScrolls + 2;
|
|
1017
|
+
fallbackBottomHits = scrollResult.atBottom ? fallbackBottomHits + 1 : 0;
|
|
1018
|
+
if (fallbackBottomHits >= fallbackBottomLimit && exhaustedScrolls >= Math.ceil(exhaustedScrollLimit / 2)) {
|
|
1019
|
+
summary.exhausted = true;
|
|
1020
|
+
this.logger.log('列表滚动终止:未发现“没有更多了”标识,但已多次触底且无可处理候选人,判定为 exhausted。');
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
if (exhaustedScrolls >= exhaustedScrollLimit) {
|
|
1024
|
+
summary.exhausted = true;
|
|
1025
|
+
this.logger.log('列表滚动终止:连续无可处理候选人达到保护上限,判定为 exhausted。');
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
await this.interaction.sleepRange(920, 260);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
exhaustedScrolls = 0;
|
|
1033
|
+
noMoreMarkerHits = 0;
|
|
1034
|
+
fallbackBottomHits = 0;
|
|
1035
|
+
this.logger.log(
|
|
1036
|
+
`准备处理候选人:name=${nextCustomer.name || '未知'} | key=${nextCustomer.customerKey} | job=${nextCustomer.sourceJob || '未知'} | domIndex=${nextCustomer.domIndex}`,
|
|
1037
|
+
);
|
|
1038
|
+
const result = await this.processCustomer(nextCustomer, profile, runId, {
|
|
1039
|
+
skipCardClick: false,
|
|
1040
|
+
});
|
|
1041
|
+
results.push(result);
|
|
1042
|
+
summary.inspected += 1;
|
|
1043
|
+
|
|
1044
|
+
if (result.error) {
|
|
1045
|
+
summary.errors += 1;
|
|
1046
|
+
consecutiveErrors += 1;
|
|
1047
|
+
} else {
|
|
1048
|
+
consecutiveErrors = 0;
|
|
1049
|
+
}
|
|
1050
|
+
if (result.passed) summary.passed += 1;
|
|
1051
|
+
if (result.requested) summary.requested += 1;
|
|
1052
|
+
if (!result.passed && !result.error) summary.skipped += 1;
|
|
1053
|
+
|
|
1054
|
+
this.logger.log(
|
|
1055
|
+
`候选人结果: ${result.name || '未知'} | ${result.passed ? 'passed' : result.error ? 'error' : 'skipped'}${result.reason ? ` | ${result.reason}` : ''}${result.error ? ` | ${result.error}` : ''}`,
|
|
1056
|
+
);
|
|
1057
|
+
this.logger.log(this.formatProgress(summary));
|
|
1058
|
+
this.emitProgress(summary, {
|
|
1059
|
+
stage: 'running',
|
|
1060
|
+
message: `已处理候选人:${result.name || '未知'}`,
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
if (consecutiveErrors >= 3) {
|
|
1064
|
+
this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
if (error?.name !== 'StopRequestedError') {
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
summary.stopped = true;
|
|
1073
|
+
summary.stopReason = error.message;
|
|
1074
|
+
this.emitProgress(summary, {
|
|
1075
|
+
stage: 'running',
|
|
1076
|
+
message: `运行停止:${summary.stopReason}`,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
try {
|
|
1081
|
+
const finalClose = await this.cleanupPanels({
|
|
1082
|
+
resumeMaxAttempts: 6,
|
|
1083
|
+
detailMaxAttempts: 4,
|
|
1084
|
+
ensureDismiss: true,
|
|
1085
|
+
});
|
|
1086
|
+
this.logger.log(
|
|
1087
|
+
`运行收尾关闭弹层:resumeClosed=${finalClose.resume.closed} | resumeMethod=${finalClose.resume.method} | detailClosed=${finalClose.detail.closed} | detailMethod=${finalClose.detail.method}`,
|
|
1088
|
+
);
|
|
1089
|
+
} catch (cleanupError) {
|
|
1090
|
+
this.logger.log(`运行收尾清理告警:${cleanupError?.message || cleanupError}`);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
summary.finishedAt = new Date().toISOString();
|
|
1094
|
+
summary.reportPath = await this.reportStore.write(summary);
|
|
1095
|
+
this.emitProgress(summary, {
|
|
1096
|
+
stage: 'finalize',
|
|
1097
|
+
message: summary.stopped ? '任务已停止并完成收尾。' : '任务执行完成。',
|
|
1098
|
+
});
|
|
1099
|
+
return summary;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async processCustomer(customer, profile, runId, options = {}) {
|
|
1103
|
+
const skipCardClick = Boolean(options?.skipCardClick);
|
|
1104
|
+
const baseAliases = createCustomerAliases(customer);
|
|
1105
|
+
const baseResult = {
|
|
1106
|
+
customerKey: customer.customerKey,
|
|
1107
|
+
name: customer.name || '',
|
|
1108
|
+
sourceJob: customer.sourceJob || '',
|
|
1109
|
+
decision: 'skipped',
|
|
1110
|
+
passed: false,
|
|
1111
|
+
requested: false,
|
|
1112
|
+
reason: '',
|
|
1113
|
+
error: '',
|
|
1114
|
+
artifacts: {},
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
let modalOpened = false;
|
|
1118
|
+
try {
|
|
1119
|
+
this.logger.log(`候选人开始:${customer.name || '未知'} (${customer.customerKey})`);
|
|
1120
|
+
const preClose = await this.cleanupPanels({
|
|
1121
|
+
resumeMaxAttempts: 4,
|
|
1122
|
+
detailMaxAttempts: 3,
|
|
1123
|
+
ensureDismiss: true,
|
|
1124
|
+
});
|
|
1125
|
+
if (
|
|
1126
|
+
preClose.resume.method !== 'already-closed' ||
|
|
1127
|
+
preClose.detail.method !== 'already-closed'
|
|
1128
|
+
) {
|
|
1129
|
+
this.logger.log(
|
|
1130
|
+
`候选人开始前清理残留面板:resumeClosed=${preClose.resume.closed} | resumeMethod=${preClose.resume.method} | detailClosed=${preClose.detail.closed} | detailMethod=${preClose.detail.method}`,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
if (!skipCardClick) {
|
|
1134
|
+
await this.checkpoint();
|
|
1135
|
+
const drift = Math.round((Math.random() - 0.5) * 46);
|
|
1136
|
+
this.logger.log(`卡片定位:domIndex=${customer.domIndex} | drift=${drift}`);
|
|
1137
|
+
if (typeof this.page.activateCandidate === 'function') {
|
|
1138
|
+
await this.page.activateCandidate(customer, drift);
|
|
1139
|
+
} else {
|
|
1140
|
+
const rect = await this.page.centerCustomerCard(customer.domIndex, drift);
|
|
1141
|
+
await this.interaction.sleepRange(320, 120);
|
|
1142
|
+
await this.checkpoint();
|
|
1143
|
+
await this.interaction.clickRect(rect);
|
|
1144
|
+
}
|
|
1145
|
+
await this.interaction.sleepRange(860, 280);
|
|
1146
|
+
let activated = await this.page.waitForCandidateActivated(customer, {
|
|
1147
|
+
maxAttempts: 12,
|
|
1148
|
+
delayMs: 220,
|
|
1149
|
+
});
|
|
1150
|
+
if (!activated?.matched) {
|
|
1151
|
+
this.logger.log(
|
|
1152
|
+
`候选人激活首次校验未命中,开始重试:expectedId=${customer.customerId || 'n/a'} | expectedName=${customer.name || 'n/a'} | activeId=${activated?.customerId || 'n/a'} | activeName=${activated?.name || 'n/a'}`,
|
|
1153
|
+
);
|
|
1154
|
+
for (let retry = 0; retry < 2; retry += 1) {
|
|
1155
|
+
const retryDrift = Math.round((Math.random() - 0.5) * 36);
|
|
1156
|
+
if (typeof this.page.activateCandidate === 'function') {
|
|
1157
|
+
await this.page.activateCandidate(customer, retryDrift);
|
|
1158
|
+
} else {
|
|
1159
|
+
const retryRect = await this.page.centerCustomerCard(customer.domIndex, retryDrift);
|
|
1160
|
+
await this.interaction.clickRect(retryRect);
|
|
1161
|
+
}
|
|
1162
|
+
await this.interaction.sleepRange(700, 200);
|
|
1163
|
+
activated = await this.page.waitForCandidateActivated(customer, {
|
|
1164
|
+
maxAttempts: 8,
|
|
1165
|
+
delayMs: 180,
|
|
1166
|
+
});
|
|
1167
|
+
if (activated?.matched) break;
|
|
1168
|
+
}
|
|
1169
|
+
if (!activated?.matched) {
|
|
1170
|
+
baseResult.decision = 'skipped';
|
|
1171
|
+
baseResult.reason = `候选人上下文切换失败,已跳过避免误判(expected=${customer.name || customer.customerId || 'unknown'}, active=${activated?.name || activated?.customerId || 'unknown'})`;
|
|
1172
|
+
this.logger.log(
|
|
1173
|
+
`候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
|
|
1174
|
+
);
|
|
1175
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
1176
|
+
return baseResult;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
} else {
|
|
1180
|
+
this.logger.log('复用预热候选人上下文,跳过再次点击卡片。');
|
|
1181
|
+
}
|
|
1182
|
+
await this.checkpoint();
|
|
1183
|
+
const readyState = await this.page.waitForConversationReady();
|
|
1184
|
+
this.logger.log(
|
|
1185
|
+
`会话面板就绪。onlineResume=${Boolean(readyState?.hasOnlineResume)} | askResume=${Boolean(readyState?.hasAskResume)} | attachmentResume=${Boolean(readyState?.hasAttachmentResume)} | attachmentResumeEnabled=${Boolean(readyState?.attachmentResumeEnabled)}`,
|
|
1186
|
+
);
|
|
1187
|
+
if (readyState?.attachmentResumeEnabled) {
|
|
1188
|
+
baseResult.decision = 'skipped';
|
|
1189
|
+
baseResult.reason = '检测到附件简历按钮可用,按策略跳过,不进入在线简历截图与LLM评估。';
|
|
1190
|
+
baseResult.artifacts.attachmentResume = {
|
|
1191
|
+
present: Boolean(readyState?.hasAttachmentResume),
|
|
1192
|
+
enabled: Boolean(readyState?.attachmentResumeEnabled),
|
|
1193
|
+
className: String(readyState?.attachmentResumeClass || ''),
|
|
1194
|
+
};
|
|
1195
|
+
this.logger.log(
|
|
1196
|
+
`候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
|
|
1197
|
+
);
|
|
1198
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
1199
|
+
return baseResult;
|
|
1200
|
+
}
|
|
1201
|
+
if (!readyState?.hasOnlineResume) {
|
|
1202
|
+
throw new Error('ONLINE_RESUME_UNAVAILABLE');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const candidateToken = safePathToken(customer.customerKey || customer.name || 'candidate');
|
|
1206
|
+
const artifactDir = path.join(this.artifactRootDir, runId, candidateToken);
|
|
1207
|
+
await mkdir(artifactDir, { recursive: true });
|
|
1208
|
+
|
|
1209
|
+
const acquisition = await this.acquireResumeAndEvaluate(
|
|
1210
|
+
customer,
|
|
1211
|
+
profile,
|
|
1212
|
+
artifactDir,
|
|
1213
|
+
baseResult,
|
|
1214
|
+
);
|
|
1215
|
+
const evaluation = acquisition.evaluation;
|
|
1216
|
+
const capture = acquisition.capture;
|
|
1217
|
+
modalOpened = Boolean(acquisition.modalOpened);
|
|
1218
|
+
const finalReason =
|
|
1219
|
+
normalizeText(evaluation.reason || evaluation.summary || evaluation.cot) ||
|
|
1220
|
+
(evaluation.passed ? 'LLM判定通过' : 'LLM判定不通过');
|
|
1221
|
+
this.logger.log(
|
|
1222
|
+
`LLM评估完成:passed=${evaluation.passed} | source=${acquisition.acquisitionMode} | reason=${acquisition.acquisitionReason || 'n/a'} | mode=${evaluation.evaluationMode || 'unknown'} | imageCount=${Number(evaluation.imageCount || baseResult.artifacts.modelImagePaths?.length || 0)} | result=${normalizeText(evaluation.rawOutputText || '') || 'n/a'}`,
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
baseResult.reason = finalReason;
|
|
1226
|
+
baseResult.passed = evaluation.passed;
|
|
1227
|
+
baseResult.decision = evaluation.passed ? 'passed' : 'skipped';
|
|
1228
|
+
baseResult.artifacts.finalPassed = Boolean(evaluation.passed);
|
|
1229
|
+
baseResult.artifacts.evaluationMode = String(evaluation.evaluationMode || '');
|
|
1230
|
+
baseResult.artifacts.evaluationImageCount = Number.isFinite(Number(evaluation.imageCount))
|
|
1231
|
+
? Number(evaluation.imageCount)
|
|
1232
|
+
: Array.isArray(baseResult.artifacts.modelImagePaths)
|
|
1233
|
+
? baseResult.artifacts.modelImagePaths.length
|
|
1234
|
+
: 0;
|
|
1096
1235
|
baseResult.artifacts.evaluationChunkIndex = Number.isFinite(Number(evaluation.chunkIndex))
|
|
1097
1236
|
? Number(evaluation.chunkIndex)
|
|
1098
1237
|
: null;
|
|
1099
1238
|
baseResult.artifacts.evaluationChunkTotal = Number.isFinite(Number(evaluation.chunkTotal))
|
|
1100
1239
|
? Number(evaluation.chunkTotal)
|
|
1101
1240
|
: null;
|
|
1241
|
+
baseResult.artifacts.evaluationAggregateRetryUsed = evaluation.aggregateRetryUsed === true;
|
|
1102
1242
|
baseResult.artifacts.llmReason = normalizeText(evaluation.reason || '');
|
|
1103
|
-
baseResult.artifacts.llmSummary = normalizeText(evaluation.summary || '');
|
|
1104
|
-
baseResult.artifacts.llmCot = normalizeText(evaluation.cot || '');
|
|
1105
|
-
baseResult.artifacts.llmEvidence = toStringArray(evaluation.evidence);
|
|
1106
|
-
baseResult.artifacts.llmRawReasoning = String(evaluation.rawReasoningText || '');
|
|
1107
|
-
baseResult.artifacts.llmRawOutput = String(evaluation.rawOutputText || '');
|
|
1108
|
-
baseResult.artifacts.resumeAcquisitionMode = String(acquisition.acquisitionMode || '');
|
|
1109
|
-
baseResult.artifacts.resumeAcquisitionReason = String(acquisition.acquisitionReason || '');
|
|
1110
|
-
baseResult.artifacts.initialNetworkWaitMs = Number(acquisition.timings?.initialNetworkWaitMs || 0);
|
|
1111
|
-
baseResult.artifacts.networkRetryMs = Number(acquisition.timings?.networkRetryMs || 0);
|
|
1112
|
-
baseResult.artifacts.imageCaptureMs = Number(acquisition.timings?.imageCaptureMs || 0);
|
|
1113
|
-
baseResult.artifacts.imageModelMs = Number(acquisition.timings?.imageModelMs || 0);
|
|
1114
|
-
baseResult.artifacts.lateNetworkRetryMs = Number(acquisition.timings?.lateNetworkRetryMs || 0);
|
|
1115
|
-
baseResult.artifacts.domFallbackMs = Number(acquisition.timings?.domFallbackMs || 0);
|
|
1116
|
-
baseResult.artifacts.textModelMs = Number(acquisition.timings?.textModelMs || 0);
|
|
1117
|
-
if (acquisition.sourceCandidateInfo) {
|
|
1118
|
-
baseResult.artifacts.resumeProfile = {
|
|
1119
|
-
primarySchool: normalizeText(acquisition.sourceCandidateInfo.school || ''),
|
|
1120
|
-
schools: Array.isArray(acquisition.sourceCandidateInfo.schools)
|
|
1121
|
-
? acquisition.sourceCandidateInfo.schools
|
|
1122
|
-
: [],
|
|
1123
|
-
major: normalizeText(acquisition.sourceCandidateInfo.major || ''),
|
|
1124
|
-
majors: Array.isArray(acquisition.sourceCandidateInfo.majors)
|
|
1125
|
-
? acquisition.sourceCandidateInfo.majors
|
|
1126
|
-
: [],
|
|
1127
|
-
company: normalizeText(acquisition.sourceCandidateInfo.company || ''),
|
|
1128
|
-
position: normalizeText(acquisition.sourceCandidateInfo.position || ''),
|
|
1129
|
-
resumeTextLength: String(acquisition.sourceCandidateInfo.resumeText || '').length,
|
|
1130
|
-
evidenceCorpusLength: String(
|
|
1131
|
-
acquisition.sourceCandidateInfo.evidenceCorpus || acquisition.sourceCandidateInfo.resumeText || '',
|
|
1132
|
-
).length,
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
if (this.resumeNetworkTracker) {
|
|
1136
|
-
baseResult.artifacts.resumeNetworkMode = this.resumeNetworkTracker.getResumeAcquisitionState().mode;
|
|
1137
|
-
baseResult.artifacts.resumeNetworkModeReason =
|
|
1138
|
-
this.resumeNetworkTracker.getResumeAcquisitionState().reason;
|
|
1139
|
-
baseResult.artifacts.resumeNetworkDiagnostics =
|
|
1140
|
-
this.resumeNetworkTracker.resumeNetworkDiagnostics.slice(-12);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1243
|
+
baseResult.artifacts.llmSummary = normalizeText(evaluation.summary || '');
|
|
1244
|
+
baseResult.artifacts.llmCot = normalizeText(evaluation.cot || '');
|
|
1245
|
+
baseResult.artifacts.llmEvidence = toStringArray(evaluation.evidence);
|
|
1246
|
+
baseResult.artifacts.llmRawReasoning = String(evaluation.rawReasoningText || '');
|
|
1247
|
+
baseResult.artifacts.llmRawOutput = String(evaluation.rawOutputText || '');
|
|
1248
|
+
baseResult.artifacts.resumeAcquisitionMode = String(acquisition.acquisitionMode || '');
|
|
1249
|
+
baseResult.artifacts.resumeAcquisitionReason = String(acquisition.acquisitionReason || '');
|
|
1250
|
+
baseResult.artifacts.initialNetworkWaitMs = Number(acquisition.timings?.initialNetworkWaitMs || 0);
|
|
1251
|
+
baseResult.artifacts.networkRetryMs = Number(acquisition.timings?.networkRetryMs || 0);
|
|
1252
|
+
baseResult.artifacts.imageCaptureMs = Number(acquisition.timings?.imageCaptureMs || 0);
|
|
1253
|
+
baseResult.artifacts.imageModelMs = Number(acquisition.timings?.imageModelMs || 0);
|
|
1254
|
+
baseResult.artifacts.lateNetworkRetryMs = Number(acquisition.timings?.lateNetworkRetryMs || 0);
|
|
1255
|
+
baseResult.artifacts.domFallbackMs = Number(acquisition.timings?.domFallbackMs || 0);
|
|
1256
|
+
baseResult.artifacts.textModelMs = Number(acquisition.timings?.textModelMs || 0);
|
|
1257
|
+
if (acquisition.sourceCandidateInfo) {
|
|
1258
|
+
baseResult.artifacts.resumeProfile = {
|
|
1259
|
+
primarySchool: normalizeText(acquisition.sourceCandidateInfo.school || ''),
|
|
1260
|
+
schools: Array.isArray(acquisition.sourceCandidateInfo.schools)
|
|
1261
|
+
? acquisition.sourceCandidateInfo.schools
|
|
1262
|
+
: [],
|
|
1263
|
+
major: normalizeText(acquisition.sourceCandidateInfo.major || ''),
|
|
1264
|
+
majors: Array.isArray(acquisition.sourceCandidateInfo.majors)
|
|
1265
|
+
? acquisition.sourceCandidateInfo.majors
|
|
1266
|
+
: [],
|
|
1267
|
+
company: normalizeText(acquisition.sourceCandidateInfo.company || ''),
|
|
1268
|
+
position: normalizeText(acquisition.sourceCandidateInfo.position || ''),
|
|
1269
|
+
resumeTextLength: String(acquisition.sourceCandidateInfo.resumeText || '').length,
|
|
1270
|
+
evidenceCorpusLength: String(
|
|
1271
|
+
acquisition.sourceCandidateInfo.evidenceCorpus || acquisition.sourceCandidateInfo.resumeText || '',
|
|
1272
|
+
).length,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
if (this.resumeNetworkTracker) {
|
|
1276
|
+
baseResult.artifacts.resumeNetworkMode = this.resumeNetworkTracker.getResumeAcquisitionState().mode;
|
|
1277
|
+
baseResult.artifacts.resumeNetworkModeReason =
|
|
1278
|
+
this.resumeNetworkTracker.getResumeAcquisitionState().reason;
|
|
1279
|
+
baseResult.artifacts.resumeNetworkDiagnostics =
|
|
1280
|
+
this.resumeNetworkTracker.resumeNetworkDiagnostics.slice(-12);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1143
1283
|
await this.checkpoint();
|
|
1144
1284
|
const closeResult =
|
|
1145
1285
|
typeof this.page.closeResumeModalDomOnce === 'function'
|
|
1146
|
-
? await this.page.closeResumeModalDomOnce()
|
|
1147
|
-
: await this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
|
|
1148
|
-
modalOpened = false;
|
|
1149
|
-
baseResult.artifacts.resumeCloseMethod = closeResult.method;
|
|
1150
|
-
baseResult.artifacts.resumeClosed = closeResult.closed;
|
|
1151
|
-
this.logger.log(
|
|
1152
|
-
`简历关闭结果:closed=${closeResult.closed} | method=${closeResult.method} | scope=${closeResult?.finalState?.scopeCount ?? 'n/a'} | iframe=${closeResult?.finalState?.iframeCount ?? 'n/a'} | close=${closeResult?.finalState?.closeCount ?? 'n/a'} | class=${closeResult?.finalState?.topScopeClass || 'n/a'}`,
|
|
1153
|
-
);
|
|
1286
|
+
? await this.page.closeResumeModalDomOnce()
|
|
1287
|
+
: await this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
|
|
1288
|
+
modalOpened = false;
|
|
1289
|
+
baseResult.artifacts.resumeCloseMethod = closeResult.method;
|
|
1290
|
+
baseResult.artifacts.resumeClosed = closeResult.closed;
|
|
1291
|
+
this.logger.log(
|
|
1292
|
+
`简历关闭结果:closed=${closeResult.closed} | method=${closeResult.method} | scope=${closeResult?.finalState?.scopeCount ?? 'n/a'} | iframe=${closeResult?.finalState?.iframeCount ?? 'n/a'} | close=${closeResult?.finalState?.closeCount ?? 'n/a'} | class=${closeResult?.finalState?.topScopeClass || 'n/a'}`,
|
|
1293
|
+
);
|
|
1154
1294
|
if (!closeResult.closed) {
|
|
1155
1295
|
baseResult.artifacts.resumeCloseWarning = 'resume modal not fully closed by single DOM close';
|
|
1156
1296
|
}
|
|
1157
1297
|
|
|
1158
1298
|
if (evaluation.passed && !this.dryRun) {
|
|
1159
|
-
const greetingText = 'Hi同学,能麻烦发下简历吗?';
|
|
1160
|
-
this.logger.log(`候选人通过,先发送消息:${greetingText}`);
|
|
1161
1299
|
await this.checkpoint();
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (!sendResult?.sent) {
|
|
1173
|
-
throw new Error(
|
|
1174
|
-
`CHAT_GREETING_SEND_FAILED(method=${sendResult?.method || 'unknown'},editorAfter=${sendResult?.editorAfter || ''})`,
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
baseResult.artifacts.greetingMessage = greetingText;
|
|
1178
|
-
baseResult.artifacts.greetingSent = Boolean(sendResult?.sent);
|
|
1179
|
-
baseResult.artifacts.greetingSendMethod = sendResult?.method || 'unknown';
|
|
1180
|
-
this.logger.log(
|
|
1181
|
-
`招呼语发送结果:sent=${Boolean(sendResult?.sent)} | method=${sendResult?.method || 'unknown'} | cleared=${Boolean(sendResult?.cleared)} | editorAfter=${sendResult?.editorAfter || ''}`,
|
|
1182
|
-
);
|
|
1183
|
-
await this.interaction.sleepRange(360, 120);
|
|
1184
|
-
|
|
1185
|
-
this.logger.log('候选人通过,执行求简历动作。');
|
|
1186
|
-
const maxRequestAttempts = 3;
|
|
1187
|
-
let requestSucceeded = false;
|
|
1188
|
-
let lastAttempt = null;
|
|
1189
|
-
|
|
1190
|
-
for (let requestAttempt = 0; requestAttempt < maxRequestAttempts; requestAttempt += 1) {
|
|
1191
|
-
await this.checkpoint();
|
|
1192
|
-
const messageBefore =
|
|
1193
|
-
typeof this.page.getResumeRequestMessageState === 'function'
|
|
1194
|
-
? await this.page.getResumeRequestMessageState()
|
|
1195
|
-
: { ok: false, count: 0, lastText: '', recent: [] };
|
|
1196
|
-
const askResult = await this.page.clickAskResume();
|
|
1197
|
-
await this.interaction.sleepRange(460, 150);
|
|
1198
|
-
|
|
1199
|
-
let confirmResult = {
|
|
1200
|
-
confirmed: false,
|
|
1201
|
-
requestedVerified: false,
|
|
1202
|
-
assumedRequested: false,
|
|
1203
|
-
uiState: null,
|
|
1204
|
-
};
|
|
1205
|
-
if (!askResult?.alreadyRequested) {
|
|
1206
|
-
await this.checkpoint();
|
|
1207
|
-
confirmResult = await this.page.clickConfirmRequestResume();
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
let messageObserved = false;
|
|
1211
|
-
let messageAfter = null;
|
|
1212
|
-
if (typeof this.page.waitForResumeRequestMessage === 'function') {
|
|
1213
|
-
const messageCheck = await this.page.waitForResumeRequestMessage({
|
|
1214
|
-
baselineCount: Number(messageBefore?.count || 0),
|
|
1215
|
-
timeoutMs: 7000,
|
|
1216
|
-
pollMs: 260,
|
|
1217
|
-
});
|
|
1218
|
-
messageAfter = messageCheck?.state || null;
|
|
1219
|
-
messageObserved = Boolean(messageCheck?.observed) || hasResumeRequestSentMessage(messageAfter || {});
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
const requestedVerified = Boolean(messageObserved);
|
|
1223
|
-
lastAttempt = {
|
|
1224
|
-
attempt: requestAttempt + 1,
|
|
1225
|
-
askResult,
|
|
1226
|
-
confirmResult,
|
|
1227
|
-
messageBefore,
|
|
1228
|
-
messageAfter,
|
|
1229
|
-
messageObserved,
|
|
1230
|
-
requestedVerified,
|
|
1231
|
-
};
|
|
1232
|
-
|
|
1233
|
-
if (messageAfter) {
|
|
1234
|
-
baseResult.artifacts.resumeRequestMessageBefore = Number(messageBefore?.count || 0);
|
|
1235
|
-
baseResult.artifacts.resumeRequestMessageAfter = Number(messageAfter?.count || 0);
|
|
1236
|
-
baseResult.artifacts.resumeRequestMessageObserved = messageObserved;
|
|
1237
|
-
baseResult.artifacts.resumeRequestMessageLastText = String(messageAfter?.lastText || '');
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1300
|
+
const preAction = await this.ensurePanelsClosedBeforeOutreach({
|
|
1301
|
+
initialResumeCloseResult: closeResult,
|
|
1302
|
+
});
|
|
1303
|
+
Object.assign(baseResult.artifacts, preAction.diagnostics);
|
|
1304
|
+
if (!preAction.ok) {
|
|
1305
|
+
baseResult.decision = 'skipped';
|
|
1306
|
+
baseResult.passed = false;
|
|
1307
|
+
baseResult.requested = false;
|
|
1308
|
+
baseResult.reason =
|
|
1309
|
+
'发送前未能安全关闭简历/详情面板,已跳过避免触发风控';
|
|
1240
1310
|
this.logger.log(
|
|
1241
|
-
|
|
1311
|
+
`候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason} | cleanupFailure=${preAction.diagnostics.preActionCleanupFailureReason || 'unknown'}`,
|
|
1242
1312
|
);
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
requestSucceeded = true;
|
|
1246
|
-
break;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
if (requestAttempt < maxRequestAttempts - 1) {
|
|
1250
|
-
this.logger.log('未检测到“简历请求已发送”提示,重新发起求简历。');
|
|
1251
|
-
await this.interaction.sleepRange(640, 180);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
baseResult.requested = requestSucceeded;
|
|
1256
|
-
if (!requestSucceeded) {
|
|
1257
|
-
const confirmStateText = JSON.stringify(lastAttempt?.confirmResult?.uiState || {});
|
|
1258
|
-
throw new Error(
|
|
1259
|
-
`REQUEST_RESUME_MESSAGE_NOT_OBSERVED(state=${confirmStateText},messageBefore=${Number(lastAttempt?.messageBefore?.count || 0)},messageAfter=${Number(lastAttempt?.messageAfter?.count || 0)},attempts=${maxRequestAttempts})`,
|
|
1260
|
-
);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
const finalPanels = await this.cleanupPanels({
|
|
1265
|
-
resumeMaxAttempts: 4,
|
|
1266
|
-
detailMaxAttempts: 4,
|
|
1267
|
-
ensureDismiss: true,
|
|
1268
|
-
});
|
|
1269
|
-
baseResult.artifacts.finalResumeCloseMethod = finalPanels.resume.method;
|
|
1270
|
-
baseResult.artifacts.finalResumeClosed = finalPanels.resume.closed;
|
|
1271
|
-
baseResult.artifacts.finalDetailCloseMethod = finalPanels.detail.method;
|
|
1272
|
-
baseResult.artifacts.finalDetailClosed = finalPanels.detail.closed;
|
|
1273
|
-
if (
|
|
1274
|
-
finalPanels.resume.method !== 'already-closed' ||
|
|
1275
|
-
finalPanels.detail.method !== 'already-closed'
|
|
1276
|
-
) {
|
|
1277
|
-
this.logger.log(
|
|
1278
|
-
`候选人收尾清理:resumeClosed=${finalPanels.resume.closed} | resumeMethod=${finalPanels.resume.method} | detailClosed=${finalPanels.detail.closed} | detailMethod=${finalPanels.detail.method}`,
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
1283
|
-
return baseResult;
|
|
1284
|
-
} catch (error) {
|
|
1285
|
-
if (error?.name === 'StopRequestedError') {
|
|
1286
|
-
throw error;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
if (modalOpened || typeof this.page.closeCandidateDetailDomOnce === 'function' || typeof this.page.closeCandidateDetail === 'function') {
|
|
1290
|
-
try {
|
|
1291
|
-
const closeResult = await this.cleanupPanels({
|
|
1292
|
-
resumeMaxAttempts: 6,
|
|
1313
|
+
const finalPanels = await this.cleanupPanels({
|
|
1314
|
+
resumeMaxAttempts: 4,
|
|
1293
1315
|
detailMaxAttempts: 4,
|
|
1294
1316
|
ensureDismiss: true,
|
|
1295
1317
|
});
|
|
1296
|
-
baseResult.artifacts.
|
|
1297
|
-
baseResult.artifacts.
|
|
1298
|
-
baseResult.artifacts.finalDetailCloseMethod =
|
|
1299
|
-
baseResult.artifacts.finalDetailClosed =
|
|
1300
|
-
this.
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
} catch {}
|
|
1304
|
-
}
|
|
1318
|
+
baseResult.artifacts.finalResumeCloseMethod = finalPanels.resume.method;
|
|
1319
|
+
baseResult.artifacts.finalResumeClosed = finalPanels.resume.closed;
|
|
1320
|
+
baseResult.artifacts.finalDetailCloseMethod = finalPanels.detail.method;
|
|
1321
|
+
baseResult.artifacts.finalDetailClosed = finalPanels.detail.closed;
|
|
1322
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
1323
|
+
return baseResult;
|
|
1324
|
+
}
|
|
1305
1325
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
)
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
);
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1326
|
+
const greetingText = 'Hi同学,能麻烦发下简历吗?';
|
|
1327
|
+
this.logger.log(`候选人通过,先发送消息:${greetingText}`);
|
|
1328
|
+
await this.checkpoint();
|
|
1329
|
+
const editorState = await this.page.setEditorMessage(greetingText);
|
|
1330
|
+
if (!String(editorState?.value || '').includes('Hi同学')) {
|
|
1331
|
+
throw new Error('CHAT_EDITOR_MESSAGE_MISMATCH');
|
|
1332
|
+
}
|
|
1333
|
+
this.logger.log(
|
|
1334
|
+
`招呼语写入输入框:activeSubmit=${Boolean(editorState?.activeSubmit)} | valueLen=${String(editorState?.value || '').length}`,
|
|
1335
|
+
);
|
|
1336
|
+
await this.interaction.sleepRange(320, 120);
|
|
1337
|
+
await this.checkpoint();
|
|
1338
|
+
const sendResult = await this.page.sendMessage(greetingText);
|
|
1339
|
+
if (!sendResult?.sent) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`CHAT_GREETING_SEND_FAILED(method=${sendResult?.method || 'unknown'},editorAfter=${sendResult?.editorAfter || ''})`,
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
baseResult.artifacts.greetingMessage = greetingText;
|
|
1345
|
+
baseResult.artifacts.greetingSent = Boolean(sendResult?.sent);
|
|
1346
|
+
baseResult.artifacts.greetingSendMethod = sendResult?.method || 'unknown';
|
|
1347
|
+
this.logger.log(
|
|
1348
|
+
`招呼语发送结果:sent=${Boolean(sendResult?.sent)} | method=${sendResult?.method || 'unknown'} | cleared=${Boolean(sendResult?.cleared)} | editorAfter=${sendResult?.editorAfter || ''}`,
|
|
1349
|
+
);
|
|
1350
|
+
await this.interaction.sleepRange(360, 120);
|
|
1351
|
+
|
|
1352
|
+
this.logger.log('候选人通过,执行求简历动作。');
|
|
1353
|
+
const maxRequestAttempts = 3;
|
|
1354
|
+
let requestSucceeded = false;
|
|
1355
|
+
let lastAttempt = null;
|
|
1356
|
+
|
|
1357
|
+
for (let requestAttempt = 0; requestAttempt < maxRequestAttempts; requestAttempt += 1) {
|
|
1358
|
+
await this.checkpoint();
|
|
1359
|
+
const messageBefore =
|
|
1360
|
+
typeof this.page.getResumeRequestMessageState === 'function'
|
|
1361
|
+
? await this.page.getResumeRequestMessageState()
|
|
1362
|
+
: { ok: false, count: 0, lastText: '', recent: [] };
|
|
1363
|
+
const askResult = await this.page.clickAskResume();
|
|
1364
|
+
await this.interaction.sleepRange(460, 150);
|
|
1365
|
+
|
|
1366
|
+
let confirmResult = {
|
|
1367
|
+
confirmed: false,
|
|
1368
|
+
requestedVerified: false,
|
|
1369
|
+
assumedRequested: false,
|
|
1370
|
+
uiState: null,
|
|
1371
|
+
};
|
|
1372
|
+
if (!askResult?.alreadyRequested) {
|
|
1373
|
+
await this.checkpoint();
|
|
1374
|
+
confirmResult = await this.page.clickConfirmRequestResume();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
let messageObserved = false;
|
|
1378
|
+
let messageAfter = null;
|
|
1379
|
+
if (typeof this.page.waitForResumeRequestMessage === 'function') {
|
|
1380
|
+
const messageCheck = await this.page.waitForResumeRequestMessage({
|
|
1381
|
+
baselineCount: Number(messageBefore?.count || 0),
|
|
1382
|
+
timeoutMs: 7000,
|
|
1383
|
+
pollMs: 260,
|
|
1384
|
+
});
|
|
1385
|
+
messageAfter = messageCheck?.state || null;
|
|
1386
|
+
messageObserved = Boolean(messageCheck?.observed) || hasResumeRequestSentMessage(messageAfter || {});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const requestedVerified = Boolean(messageObserved);
|
|
1390
|
+
lastAttempt = {
|
|
1391
|
+
attempt: requestAttempt + 1,
|
|
1392
|
+
askResult,
|
|
1393
|
+
confirmResult,
|
|
1394
|
+
messageBefore,
|
|
1395
|
+
messageAfter,
|
|
1396
|
+
messageObserved,
|
|
1397
|
+
requestedVerified,
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
if (messageAfter) {
|
|
1401
|
+
baseResult.artifacts.resumeRequestMessageBefore = Number(messageBefore?.count || 0);
|
|
1402
|
+
baseResult.artifacts.resumeRequestMessageAfter = Number(messageAfter?.count || 0);
|
|
1403
|
+
baseResult.artifacts.resumeRequestMessageObserved = messageObserved;
|
|
1404
|
+
baseResult.artifacts.resumeRequestMessageLastText = String(messageAfter?.lastText || '');
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
this.logger.log(
|
|
1408
|
+
`求简历动作检查:attempt=${requestAttempt + 1}/${maxRequestAttempts} | alreadyRequested=${Boolean(askResult?.alreadyRequested)} | confirmed=${Boolean(confirmResult?.confirmed)} | disabledOperateAsk=${Boolean(confirmResult?.uiState?.hasDisabledOperateAsk)} | messageObserved=${messageObserved} | verified=${requestedVerified} | assumed=${Boolean(confirmResult?.assumedRequested)}`,
|
|
1409
|
+
);
|
|
1410
|
+
|
|
1411
|
+
if (requestedVerified) {
|
|
1412
|
+
requestSucceeded = true;
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (requestAttempt < maxRequestAttempts - 1) {
|
|
1417
|
+
this.logger.log('未检测到“简历请求已发送”提示,重新发起求简历。');
|
|
1418
|
+
await this.interaction.sleepRange(640, 180);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
baseResult.requested = requestSucceeded;
|
|
1423
|
+
if (!requestSucceeded) {
|
|
1424
|
+
const confirmStateText = JSON.stringify(lastAttempt?.confirmResult?.uiState || {});
|
|
1425
|
+
throw new Error(
|
|
1426
|
+
`REQUEST_RESUME_MESSAGE_NOT_OBSERVED(state=${confirmStateText},messageBefore=${Number(lastAttempt?.messageBefore?.count || 0)},messageAfter=${Number(lastAttempt?.messageAfter?.count || 0)},attempts=${maxRequestAttempts})`,
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const finalPanels = await this.cleanupPanels({
|
|
1432
|
+
resumeMaxAttempts: 4,
|
|
1433
|
+
detailMaxAttempts: 4,
|
|
1434
|
+
ensureDismiss: true,
|
|
1435
|
+
});
|
|
1436
|
+
baseResult.artifacts.finalResumeCloseMethod = finalPanels.resume.method;
|
|
1437
|
+
baseResult.artifacts.finalResumeClosed = finalPanels.resume.closed;
|
|
1438
|
+
baseResult.artifacts.finalDetailCloseMethod = finalPanels.detail.method;
|
|
1439
|
+
baseResult.artifacts.finalDetailClosed = finalPanels.detail.closed;
|
|
1440
|
+
if (
|
|
1441
|
+
finalPanels.resume.method !== 'already-closed' ||
|
|
1442
|
+
finalPanels.detail.method !== 'already-closed'
|
|
1443
|
+
) {
|
|
1444
|
+
this.logger.log(
|
|
1445
|
+
`候选人收尾清理:resumeClosed=${finalPanels.resume.closed} | resumeMethod=${finalPanels.resume.method} | detailClosed=${finalPanels.detail.closed} | detailMethod=${finalPanels.detail.method}`,
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
1450
|
+
return baseResult;
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (error?.name === 'StopRequestedError') {
|
|
1453
|
+
throw error;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (modalOpened || typeof this.page.closeCandidateDetailDomOnce === 'function' || typeof this.page.closeCandidateDetail === 'function') {
|
|
1457
|
+
try {
|
|
1458
|
+
const closeResult = await this.cleanupPanels({
|
|
1459
|
+
resumeMaxAttempts: 6,
|
|
1460
|
+
detailMaxAttempts: 4,
|
|
1461
|
+
ensureDismiss: true,
|
|
1462
|
+
});
|
|
1463
|
+
baseResult.artifacts.resumeCloseMethod = closeResult.resume.method;
|
|
1464
|
+
baseResult.artifacts.resumeClosed = closeResult.resume.closed;
|
|
1465
|
+
baseResult.artifacts.finalDetailCloseMethod = closeResult.detail.method;
|
|
1466
|
+
baseResult.artifacts.finalDetailClosed = closeResult.detail.closed;
|
|
1467
|
+
this.logger.log(
|
|
1468
|
+
`异常后关闭面板结果:resumeClosed=${closeResult.resume.closed} | resumeMethod=${closeResult.resume.method} | resumeScope=${closeResult?.resume?.finalState?.scopeCount ?? 'n/a'} | resumeIframe=${closeResult?.resume?.finalState?.iframeCount ?? 'n/a'} | resumeClose=${closeResult?.resume?.finalState?.closeCount ?? 'n/a'} | resumeClass=${closeResult?.resume?.finalState?.topScopeClass || 'n/a'} | detailClosed=${closeResult.detail.closed} | detailMethod=${closeResult.detail.method} | detailPanels=${closeResult?.detail?.finalState?.panelCount ?? 'n/a'} | detailClose=${closeResult?.detail?.finalState?.closeCount ?? 'n/a'} | detailClass=${closeResult?.detail?.finalState?.topPanelClass || 'n/a'}`,
|
|
1469
|
+
);
|
|
1470
|
+
} catch {}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const message = error.message || String(error);
|
|
1474
|
+
if (
|
|
1475
|
+
/ONLINE_RESUME_UNAVAILABLE|ONLINE_RESUME_BUTTON_NOT_FOUND|OPEN_ONLINE_RESUME_FAILED|NO_RESUME_IFRAME|NO_SCROLL_CONTAINER|RESUME_MODAL_OPEN_TIMEOUT|Resume context probe timeout: reason=NO_RESUME_IFRAME|RESUME_RATE_LIMIT_WARNING|RESUME_CAPTURE_LIKELY_BLANK|DOM_RESUME_FALLBACK_FAILED|RESUME_MODAL_NOT_DETECTED/i.test(
|
|
1476
|
+
message,
|
|
1477
|
+
)
|
|
1478
|
+
) {
|
|
1479
|
+
baseResult.decision = 'skipped';
|
|
1480
|
+
baseResult.reason = `在线简历不可用或未加载,已跳过该候选人(${message})`;
|
|
1481
|
+
baseResult.artifacts.resumeUnavailable = true;
|
|
1482
|
+
this.logger.log(
|
|
1483
|
+
`候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
|
|
1484
|
+
);
|
|
1485
|
+
} else {
|
|
1486
|
+
baseResult.error = message;
|
|
1487
|
+
baseResult.decision = 'error';
|
|
1488
|
+
this.logger.log(
|
|
1489
|
+
`候选人处理异常:name=${customer.name || '未知'} | key=${customer.customerKey} | error=${baseResult.error}`,
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
1493
|
+
return baseResult;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|