@reconcrap/boss-recommend-mcp 1.2.10 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -1
- package/package.json +2 -1
- package/skills/boss-chat/README.md +5 -0
- package/skills/boss-chat/SKILL.md +69 -0
- package/skills/boss-recommend-pipeline/SKILL.md +40 -4
- package/src/adapters.js +19 -5
- package/src/boss-chat.js +436 -0
- package/src/cli.js +294 -129
- package/src/index.js +459 -108
- package/src/pipeline.js +605 -8
- package/src/run-state.js +5 -0
- package/src/test-adapters-runtime.js +69 -0
- package/src/test-boss-chat.js +399 -0
- package/src/test-index-async.js +238 -4
- package/src/test-pipeline.js +408 -1
- package/vendor/boss-chat-cli/README.md +134 -0
- package/vendor/boss-chat-cli/package.json +53 -0
- package/vendor/boss-chat-cli/src/app.js +783 -0
- package/vendor/boss-chat-cli/src/browser/chat-page.js +2698 -0
- package/vendor/boss-chat-cli/src/cli.js +1350 -0
- package/vendor/boss-chat-cli/src/mcp/server.js +149 -0
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +193 -0
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +260 -0
- package/vendor/boss-chat-cli/src/runtime/interaction.js +102 -0
- package/vendor/boss-chat-cli/src/runtime/run-control.js +102 -0
- package/vendor/boss-chat-cli/src/services/chrome-client.js +97 -0
- package/vendor/boss-chat-cli/src/services/llm.js +352 -0
- package/vendor/boss-chat-cli/src/services/profile-store.js +157 -0
- package/vendor/boss-chat-cli/src/services/report-store.js +19 -0
- package/vendor/boss-chat-cli/src/services/resume-capture.js +554 -0
- package/vendor/boss-chat-cli/src/services/state-store.js +217 -0
- package/vendor/boss-chat-cli/src/utils/customer-key.js +82 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +902 -56
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +387 -1
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { createCustomerAliases, createCustomerKey } from './utils/customer-key.js';
|
|
5
|
+
|
|
6
|
+
function runToken(date = new Date()) {
|
|
7
|
+
return date.toISOString().replace(/[:.]/g, '-');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function safePathToken(value) {
|
|
11
|
+
return String(value || 'unknown')
|
|
12
|
+
.replace(/[^\w.-]+/g, '_')
|
|
13
|
+
.slice(0, 80);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeText(value) {
|
|
17
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sanitizeReasonWithResumeProfile(reason, resumeProfile) {
|
|
21
|
+
const rawReason = normalizeText(reason);
|
|
22
|
+
if (!rawReason) return rawReason;
|
|
23
|
+
const schools = Array.isArray(resumeProfile?.schools)
|
|
24
|
+
? resumeProfile.schools.map((item) => normalizeText(item)).filter(Boolean)
|
|
25
|
+
: [];
|
|
26
|
+
const primarySchool = normalizeText(resumeProfile?.primarySchool || schools[0] || '');
|
|
27
|
+
const schoolPool = primarySchool ? [primarySchool, ...schools] : schools;
|
|
28
|
+
if (schoolPool.length <= 0) return rawReason;
|
|
29
|
+
|
|
30
|
+
if (schoolPool.some((school) => rawReason.includes(school))) {
|
|
31
|
+
return rawReason;
|
|
32
|
+
}
|
|
33
|
+
if (!/(大学|学院|院校|中科院|学校)/.test(rawReason)) {
|
|
34
|
+
return rawReason;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sentences = rawReason
|
|
38
|
+
.split(/[。;;]+/)
|
|
39
|
+
.map((item) => normalizeText(item))
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
const filtered = sentences.filter((sentence) => {
|
|
42
|
+
if (!/(大学|学院|院校|中科院|学校)/.test(sentence)) return true;
|
|
43
|
+
return schoolPool.some((school) => sentence.includes(school));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const prefix = `教育经历学校以简历主内容为准:${schoolPool[0]}`;
|
|
47
|
+
if (filtered.length <= 0) {
|
|
48
|
+
return `${prefix}。`;
|
|
49
|
+
}
|
|
50
|
+
return `${prefix}。${filtered.join(';')}。`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shouldContinue(summary, targetCount) {
|
|
54
|
+
if (!targetCount || !Number.isFinite(targetCount) || targetCount <= 0) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return summary.inspected < targetCount;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class BossChatApp {
|
|
61
|
+
constructor({
|
|
62
|
+
page,
|
|
63
|
+
llmClient,
|
|
64
|
+
interaction,
|
|
65
|
+
resumeCaptureService,
|
|
66
|
+
stateStore,
|
|
67
|
+
reportStore,
|
|
68
|
+
runControl = null,
|
|
69
|
+
logger = console,
|
|
70
|
+
dryRun = false,
|
|
71
|
+
artifactRootDir = '',
|
|
72
|
+
resumeOpenCooldownMs = 16000,
|
|
73
|
+
onProgress = null,
|
|
74
|
+
}) {
|
|
75
|
+
this.page = page;
|
|
76
|
+
this.llmClient = llmClient;
|
|
77
|
+
this.interaction = interaction;
|
|
78
|
+
this.resumeCaptureService = resumeCaptureService;
|
|
79
|
+
this.stateStore = stateStore;
|
|
80
|
+
this.reportStore = reportStore;
|
|
81
|
+
this.runControl = runControl;
|
|
82
|
+
this.logger = logger;
|
|
83
|
+
this.dryRun = dryRun;
|
|
84
|
+
this.artifactRootDir = artifactRootDir;
|
|
85
|
+
this.lastResumeOpenAt = 0;
|
|
86
|
+
this.resumeOpenBlockedUntil = 0;
|
|
87
|
+
this.resumeOpenCooldownMs = Number.isFinite(Number(resumeOpenCooldownMs))
|
|
88
|
+
? Math.max(0, Number(resumeOpenCooldownMs))
|
|
89
|
+
: 16000;
|
|
90
|
+
this.onProgress = typeof onProgress === 'function' ? onProgress : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
formatProgress(summary) {
|
|
94
|
+
const targetText = summary.profile.targetCount || '∞';
|
|
95
|
+
return `进度: 已处理 ${summary.inspected}/${targetText},通过 ${summary.passed},已求简历 ${summary.requested},跳过 ${summary.skipped},错误 ${summary.errors}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async checkpoint() {
|
|
99
|
+
if (this.runControl) {
|
|
100
|
+
await this.runControl.checkpoint();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async waitResumeOpenCooldown(minGapMs = this.resumeOpenCooldownMs) {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const waitFromLast = Math.max(0, minGapMs - (now - this.lastResumeOpenAt));
|
|
107
|
+
const waitFromBlock = Math.max(0, this.resumeOpenBlockedUntil - now);
|
|
108
|
+
const waitMs = Math.max(waitFromLast, waitFromBlock);
|
|
109
|
+
if (waitMs <= 0) return;
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setResumeOpenBlocked(ms = 90000) {
|
|
114
|
+
const until = Date.now() + ms;
|
|
115
|
+
this.resumeOpenBlockedUntil = Math.max(this.resumeOpenBlockedUntil, until);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
emitProgress(summary, meta = {}) {
|
|
119
|
+
if (!this.onProgress) return;
|
|
120
|
+
try {
|
|
121
|
+
this.onProgress(
|
|
122
|
+
{
|
|
123
|
+
inspected: Number(summary?.inspected || 0),
|
|
124
|
+
passed: Number(summary?.passed || 0),
|
|
125
|
+
requested: Number(summary?.requested || 0),
|
|
126
|
+
skipped: Number(summary?.skipped || 0),
|
|
127
|
+
errors: Number(summary?.errors || 0),
|
|
128
|
+
exhausted: Boolean(summary?.exhausted),
|
|
129
|
+
stopped: Boolean(summary?.stopped),
|
|
130
|
+
stopReason: String(summary?.stopReason || ''),
|
|
131
|
+
reportPath: String(summary?.reportPath || ''),
|
|
132
|
+
},
|
|
133
|
+
meta,
|
|
134
|
+
);
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async run(profile) {
|
|
139
|
+
const startedAt = new Date().toISOString();
|
|
140
|
+
const runId = runToken(new Date());
|
|
141
|
+
const startFrom = profile.startFrom === 'all' ? 'all' : 'unread';
|
|
142
|
+
const filterLabel = startFrom === 'all' ? '全部' : '未读';
|
|
143
|
+
const targetCount =
|
|
144
|
+
Number.isFinite(Number(profile.targetCount)) && Number(profile.targetCount) > 0
|
|
145
|
+
? Number(profile.targetCount)
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
await this.stateStore.load();
|
|
149
|
+
try {
|
|
150
|
+
await this.page.ensureReady();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.logger.log(`页面就绪检查告警:${error?.message || error},将继续执行预热恢复流程。`);
|
|
153
|
+
}
|
|
154
|
+
await this.page.selectJob(profile.jobSelection);
|
|
155
|
+
|
|
156
|
+
const filterResult =
|
|
157
|
+
startFrom === 'all'
|
|
158
|
+
? await this.page.activateAllFilter()
|
|
159
|
+
: await this.page.activateUnreadFilter();
|
|
160
|
+
await this.interaction.sleepRange(420, 160);
|
|
161
|
+
this.logger.log('预热步骤:准备点击首位人选初始化聊天容器...');
|
|
162
|
+
let primedCustomer = null;
|
|
163
|
+
|
|
164
|
+
if (typeof this.page.primeConversationByFirstCandidate === 'function') {
|
|
165
|
+
try {
|
|
166
|
+
const prime = await this.page.primeConversationByFirstCandidate();
|
|
167
|
+
const candidate = prime?.candidate || {};
|
|
168
|
+
const candidateBase = {
|
|
169
|
+
customerId: candidate.customerId || '',
|
|
170
|
+
name: candidate.name || '',
|
|
171
|
+
sourceJob: candidate.sourceJob || '',
|
|
172
|
+
domIndex: Number.isFinite(candidate.domIndex) ? candidate.domIndex : 0,
|
|
173
|
+
textSnippet: '',
|
|
174
|
+
};
|
|
175
|
+
primedCustomer = {
|
|
176
|
+
...candidateBase,
|
|
177
|
+
customerKey: createCustomerKey(candidateBase),
|
|
178
|
+
};
|
|
179
|
+
this.logger.log(
|
|
180
|
+
`预热完成: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'}`,
|
|
181
|
+
);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.logger.log(`预热失败:${error?.message || error}(将继续尝试主循环)`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const results = [];
|
|
188
|
+
const summary = {
|
|
189
|
+
startedAt,
|
|
190
|
+
finishedAt: null,
|
|
191
|
+
dryRun: this.dryRun,
|
|
192
|
+
profile: {
|
|
193
|
+
screeningCriteria: profile.screeningCriteria,
|
|
194
|
+
targetCount,
|
|
195
|
+
chromePort: profile.chrome.port,
|
|
196
|
+
model: profile.llm.model,
|
|
197
|
+
startFrom,
|
|
198
|
+
jobSelection: profile.jobSelection,
|
|
199
|
+
},
|
|
200
|
+
inspected: 0,
|
|
201
|
+
passed: 0,
|
|
202
|
+
requested: 0,
|
|
203
|
+
skipped: 0,
|
|
204
|
+
errors: 0,
|
|
205
|
+
exhausted: false,
|
|
206
|
+
stopped: false,
|
|
207
|
+
stopReason: '',
|
|
208
|
+
results,
|
|
209
|
+
reportPath: null,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
this.logger.log(
|
|
213
|
+
`岗位: ${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
214
|
+
filterResult.changed
|
|
215
|
+
? filterResult.verified === false
|
|
216
|
+
? '(已尝试切换,未验证 active)'
|
|
217
|
+
: '(已切换)'
|
|
218
|
+
: '(已在目标筛选)'
|
|
219
|
+
}${filterResult?.activeLabel ? ` | active=${filterResult.activeLabel}` : ''}`,
|
|
220
|
+
);
|
|
221
|
+
this.logger.log(this.formatProgress(summary));
|
|
222
|
+
this.emitProgress(summary, {
|
|
223
|
+
stage: 'running',
|
|
224
|
+
message: '任务已启动。',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
let consecutiveErrors = 0;
|
|
228
|
+
let exhaustedScrolls = 0;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
while (shouldContinue(summary, targetCount)) {
|
|
232
|
+
await this.checkpoint();
|
|
233
|
+
if (this.resumeOpenBlockedUntil > Date.now()) {
|
|
234
|
+
const remainMs = this.resumeOpenBlockedUntil - Date.now();
|
|
235
|
+
this.logger.log(
|
|
236
|
+
`简历查看冷却中:remaining=${Math.ceil(remainMs / 1000)}s,暂停打开新简历以避免频控。`,
|
|
237
|
+
);
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(remainMs, 30000)));
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
await this.interaction.maybeRest(summary.inspected, this.logger);
|
|
242
|
+
await this.checkpoint();
|
|
243
|
+
|
|
244
|
+
let loadedCustomers = [];
|
|
245
|
+
try {
|
|
246
|
+
loadedCustomers = await this.page.getLoadedCustomers();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
const message = String(error?.message || error || '');
|
|
249
|
+
this.logger.log(`候选人扫描异常:${message}`);
|
|
250
|
+
if (
|
|
251
|
+
/CHAT_CARD_LIST_NOT_FOUND|CHAT_LIST_CONTAINER_NOT_FOUND|ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE/.test(
|
|
252
|
+
message,
|
|
253
|
+
)
|
|
254
|
+
) {
|
|
255
|
+
try {
|
|
256
|
+
const recover = await this.page.recoverToChatIndex();
|
|
257
|
+
this.logger.log(
|
|
258
|
+
`页面恢复:changed=${recover.changed} | href=${recover.href || 'unknown'},准备重新预热并继续。`,
|
|
259
|
+
);
|
|
260
|
+
await this.interaction.sleepRange(900, 220);
|
|
261
|
+
const prime = await this.page.primeConversationByFirstCandidate();
|
|
262
|
+
const candidate = prime?.candidate || {};
|
|
263
|
+
const candidateBase = {
|
|
264
|
+
customerId: candidate.customerId || '',
|
|
265
|
+
name: candidate.name || '',
|
|
266
|
+
sourceJob: candidate.sourceJob || '',
|
|
267
|
+
domIndex: Number.isFinite(candidate.domIndex) ? candidate.domIndex : 0,
|
|
268
|
+
textSnippet: '',
|
|
269
|
+
};
|
|
270
|
+
primedCustomer = {
|
|
271
|
+
...candidateBase,
|
|
272
|
+
customerKey: createCustomerKey(candidateBase),
|
|
273
|
+
};
|
|
274
|
+
this.logger.log(
|
|
275
|
+
`恢复后预热完成:name=${prime?.candidate?.name || '未知'} | id=${prime?.candidate?.customerId || '无'}`,
|
|
276
|
+
);
|
|
277
|
+
continue;
|
|
278
|
+
} catch (recoverError) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`CHAT_LIST_RECOVERY_FAILED: ${recoverError?.message || recoverError}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const shouldUsePrimedFirst =
|
|
288
|
+
Boolean(primedCustomer) &&
|
|
289
|
+
(startFrom === 'unread' || !this.stateStore.hasAny(createCustomerAliases(primedCustomer)));
|
|
290
|
+
if (shouldUsePrimedFirst && primedCustomer) {
|
|
291
|
+
this.logger.log(
|
|
292
|
+
`优先处理预热候选人:name=${primedCustomer.name || '未知'} | key=${primedCustomer.customerKey}`,
|
|
293
|
+
);
|
|
294
|
+
const result = await this.processCustomer(primedCustomer, profile, runId, {
|
|
295
|
+
skipCardClick: true,
|
|
296
|
+
});
|
|
297
|
+
primedCustomer = null;
|
|
298
|
+
results.push(result);
|
|
299
|
+
summary.inspected += 1;
|
|
300
|
+
|
|
301
|
+
if (result.error) {
|
|
302
|
+
summary.errors += 1;
|
|
303
|
+
consecutiveErrors += 1;
|
|
304
|
+
} else {
|
|
305
|
+
consecutiveErrors = 0;
|
|
306
|
+
}
|
|
307
|
+
if (result.passed) summary.passed += 1;
|
|
308
|
+
if (result.requested) summary.requested += 1;
|
|
309
|
+
if (!result.passed && !result.error) summary.skipped += 1;
|
|
310
|
+
|
|
311
|
+
this.logger.log(
|
|
312
|
+
`候选人结果: ${result.name || '未知'} | ${result.passed ? 'passed' : result.error ? 'error' : 'skipped'}${result.reason ? ` | ${result.reason}` : ''}${result.error ? ` | ${result.error}` : ''}`,
|
|
313
|
+
);
|
|
314
|
+
this.logger.log(this.formatProgress(summary));
|
|
315
|
+
this.emitProgress(summary, {
|
|
316
|
+
stage: 'running',
|
|
317
|
+
message: `已处理候选人:${result.name || '未知'}`,
|
|
318
|
+
});
|
|
319
|
+
if (consecutiveErrors >= 3) {
|
|
320
|
+
this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
primedCustomer = null;
|
|
326
|
+
|
|
327
|
+
this.logger.log(`候选人扫描:当前可见 ${loadedCustomers.length} 位`);
|
|
328
|
+
const nextCustomer = loadedCustomers.find(
|
|
329
|
+
(customer) => !this.stateStore.hasAny(createCustomerAliases(customer)),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (!nextCustomer) {
|
|
333
|
+
const ratio = 0.52 + Math.random() * 0.34;
|
|
334
|
+
const scrollResult = await this.page.scrollCustomerList(ratio);
|
|
335
|
+
this.logger.log(
|
|
336
|
+
`列表滚动:ratio=${ratio.toFixed(2)} | didScroll=${Boolean(scrollResult.didScroll)} | top=${scrollResult.after?.top ?? 'n/a'} | scrollRetry=${exhaustedScrolls + 1}`,
|
|
337
|
+
);
|
|
338
|
+
exhaustedScrolls = scrollResult.didScroll ? exhaustedScrolls + 1 : exhaustedScrolls + 2;
|
|
339
|
+
if (exhaustedScrolls >= 3) {
|
|
340
|
+
summary.exhausted = true;
|
|
341
|
+
this.logger.log('列表滚动终止:连续无可处理候选人,判定为 exhausted。');
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
await this.interaction.sleepRange(920, 260);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
exhaustedScrolls = 0;
|
|
349
|
+
this.logger.log(
|
|
350
|
+
`准备处理候选人:name=${nextCustomer.name || '未知'} | key=${nextCustomer.customerKey} | job=${nextCustomer.sourceJob || '未知'} | domIndex=${nextCustomer.domIndex}`,
|
|
351
|
+
);
|
|
352
|
+
const result = await this.processCustomer(nextCustomer, profile, runId, {
|
|
353
|
+
skipCardClick: false,
|
|
354
|
+
});
|
|
355
|
+
results.push(result);
|
|
356
|
+
summary.inspected += 1;
|
|
357
|
+
|
|
358
|
+
if (result.error) {
|
|
359
|
+
summary.errors += 1;
|
|
360
|
+
consecutiveErrors += 1;
|
|
361
|
+
} else {
|
|
362
|
+
consecutiveErrors = 0;
|
|
363
|
+
}
|
|
364
|
+
if (result.passed) summary.passed += 1;
|
|
365
|
+
if (result.requested) summary.requested += 1;
|
|
366
|
+
if (!result.passed && !result.error) summary.skipped += 1;
|
|
367
|
+
|
|
368
|
+
this.logger.log(
|
|
369
|
+
`候选人结果: ${result.name || '未知'} | ${result.passed ? 'passed' : result.error ? 'error' : 'skipped'}${result.reason ? ` | ${result.reason}` : ''}${result.error ? ` | ${result.error}` : ''}`,
|
|
370
|
+
);
|
|
371
|
+
this.logger.log(this.formatProgress(summary));
|
|
372
|
+
this.emitProgress(summary, {
|
|
373
|
+
stage: 'running',
|
|
374
|
+
message: `已处理候选人:${result.name || '未知'}`,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (consecutiveErrors >= 3) {
|
|
378
|
+
this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (error?.name !== 'StopRequestedError') {
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
summary.stopped = true;
|
|
387
|
+
summary.stopReason = error.message;
|
|
388
|
+
this.emitProgress(summary, {
|
|
389
|
+
stage: 'running',
|
|
390
|
+
message: `运行停止:${summary.stopReason}`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const finalClose =
|
|
396
|
+
typeof this.page.closeResumeModalDomOnce === 'function'
|
|
397
|
+
? await this.page.closeResumeModalDomOnce()
|
|
398
|
+
: await this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
|
|
399
|
+
this.logger.log(
|
|
400
|
+
`运行收尾关闭简历弹层:closed=${finalClose.closed} | method=${finalClose.method}`,
|
|
401
|
+
);
|
|
402
|
+
} catch (cleanupError) {
|
|
403
|
+
this.logger.log(`运行收尾清理告警:${cleanupError?.message || cleanupError}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
summary.finishedAt = new Date().toISOString();
|
|
407
|
+
summary.reportPath = await this.reportStore.write(summary);
|
|
408
|
+
this.emitProgress(summary, {
|
|
409
|
+
stage: 'finalize',
|
|
410
|
+
message: summary.stopped ? '任务已停止并完成收尾。' : '任务执行完成。',
|
|
411
|
+
});
|
|
412
|
+
return summary;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async processCustomer(customer, profile, runId, options = {}) {
|
|
416
|
+
const skipCardClick = Boolean(options?.skipCardClick);
|
|
417
|
+
const baseAliases = createCustomerAliases(customer);
|
|
418
|
+
const baseResult = {
|
|
419
|
+
customerKey: customer.customerKey,
|
|
420
|
+
name: customer.name || '',
|
|
421
|
+
sourceJob: customer.sourceJob || '',
|
|
422
|
+
decision: 'skipped',
|
|
423
|
+
passed: false,
|
|
424
|
+
requested: false,
|
|
425
|
+
reason: '',
|
|
426
|
+
error: '',
|
|
427
|
+
artifacts: {},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
let modalOpened = false;
|
|
431
|
+
try {
|
|
432
|
+
this.logger.log(`候选人开始:${customer.name || '未知'} (${customer.customerKey})`);
|
|
433
|
+
const preClose =
|
|
434
|
+
typeof this.page.closeResumeModalDomOnce === 'function'
|
|
435
|
+
? await this.page.closeResumeModalDomOnce()
|
|
436
|
+
: await this.page.closeResumeModal({ maxAttempts: 4, ensureDismiss: true });
|
|
437
|
+
if (preClose.method !== 'already-closed') {
|
|
438
|
+
this.logger.log(
|
|
439
|
+
`候选人开始前清理残留弹层:closed=${preClose.closed} | method=${preClose.method}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (!skipCardClick) {
|
|
443
|
+
await this.checkpoint();
|
|
444
|
+
const drift = Math.round((Math.random() - 0.5) * 46);
|
|
445
|
+
this.logger.log(`卡片定位:domIndex=${customer.domIndex} | drift=${drift}`);
|
|
446
|
+
if (typeof this.page.activateCandidate === 'function') {
|
|
447
|
+
await this.page.activateCandidate(customer, drift);
|
|
448
|
+
} else {
|
|
449
|
+
const rect = await this.page.centerCustomerCard(customer.domIndex, drift);
|
|
450
|
+
await this.interaction.sleepRange(320, 120);
|
|
451
|
+
await this.checkpoint();
|
|
452
|
+
await this.interaction.clickRect(rect);
|
|
453
|
+
}
|
|
454
|
+
await this.interaction.sleepRange(860, 280);
|
|
455
|
+
let activated = await this.page.waitForCandidateActivated(customer, {
|
|
456
|
+
maxAttempts: 12,
|
|
457
|
+
delayMs: 220,
|
|
458
|
+
});
|
|
459
|
+
if (!activated?.matched) {
|
|
460
|
+
this.logger.log(
|
|
461
|
+
`候选人激活首次校验未命中,开始重试:expectedId=${customer.customerId || 'n/a'} | expectedName=${customer.name || 'n/a'} | activeId=${activated?.customerId || 'n/a'} | activeName=${activated?.name || 'n/a'}`,
|
|
462
|
+
);
|
|
463
|
+
for (let retry = 0; retry < 2; retry += 1) {
|
|
464
|
+
const retryDrift = Math.round((Math.random() - 0.5) * 36);
|
|
465
|
+
if (typeof this.page.activateCandidate === 'function') {
|
|
466
|
+
await this.page.activateCandidate(customer, retryDrift);
|
|
467
|
+
} else {
|
|
468
|
+
const retryRect = await this.page.centerCustomerCard(customer.domIndex, retryDrift);
|
|
469
|
+
await this.interaction.clickRect(retryRect);
|
|
470
|
+
}
|
|
471
|
+
await this.interaction.sleepRange(700, 200);
|
|
472
|
+
activated = await this.page.waitForCandidateActivated(customer, {
|
|
473
|
+
maxAttempts: 8,
|
|
474
|
+
delayMs: 180,
|
|
475
|
+
});
|
|
476
|
+
if (activated?.matched) break;
|
|
477
|
+
}
|
|
478
|
+
if (!activated?.matched) {
|
|
479
|
+
baseResult.decision = 'skipped';
|
|
480
|
+
baseResult.reason = `候选人上下文切换失败,已跳过避免误判(expected=${customer.name || customer.customerId || 'unknown'}, active=${activated?.name || activated?.customerId || 'unknown'})`;
|
|
481
|
+
this.logger.log(
|
|
482
|
+
`候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
|
|
483
|
+
);
|
|
484
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
485
|
+
return baseResult;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
this.logger.log('复用预热候选人上下文,跳过再次点击卡片。');
|
|
490
|
+
}
|
|
491
|
+
await this.checkpoint();
|
|
492
|
+
const readyState = await this.page.waitForConversationReady();
|
|
493
|
+
this.logger.log(
|
|
494
|
+
`会话面板就绪。onlineResume=${Boolean(readyState?.hasOnlineResume)} | askResume=${Boolean(readyState?.hasAskResume)} | attachmentResume=${Boolean(readyState?.hasAttachmentResume)} | attachmentResumeEnabled=${Boolean(readyState?.attachmentResumeEnabled)}`,
|
|
495
|
+
);
|
|
496
|
+
if (readyState?.attachmentResumeEnabled) {
|
|
497
|
+
baseResult.decision = 'skipped';
|
|
498
|
+
baseResult.reason = '检测到附件简历按钮可用,按策略跳过,不进入在线简历截图与LLM评估。';
|
|
499
|
+
baseResult.artifacts.attachmentResume = {
|
|
500
|
+
present: Boolean(readyState?.hasAttachmentResume),
|
|
501
|
+
enabled: Boolean(readyState?.attachmentResumeEnabled),
|
|
502
|
+
className: String(readyState?.attachmentResumeClass || ''),
|
|
503
|
+
};
|
|
504
|
+
this.logger.log(
|
|
505
|
+
`候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
|
|
506
|
+
);
|
|
507
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
508
|
+
return baseResult;
|
|
509
|
+
}
|
|
510
|
+
if (!readyState?.hasOnlineResume) {
|
|
511
|
+
throw new Error('ONLINE_RESUME_UNAVAILABLE');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const candidateToken = safePathToken(customer.customerKey || customer.name || 'candidate');
|
|
515
|
+
const artifactDir = path.join(this.artifactRootDir, runId, candidateToken);
|
|
516
|
+
await mkdir(artifactDir, { recursive: true });
|
|
517
|
+
|
|
518
|
+
let capture = null;
|
|
519
|
+
let lastResumeError = null;
|
|
520
|
+
let resumeProfile = null;
|
|
521
|
+
await this.waitResumeOpenCooldown(this.resumeOpenCooldownMs + Math.floor(Math.random() * 1800));
|
|
522
|
+
await this.checkpoint();
|
|
523
|
+
const openResult = await this.page.openOnlineResume();
|
|
524
|
+
let openDetected = openResult ? Boolean(openResult?.detectedOpen) : true;
|
|
525
|
+
this.lastResumeOpenAt = Date.now();
|
|
526
|
+
modalOpened = openDetected;
|
|
527
|
+
await this.interaction.sleepRange(600, 220);
|
|
528
|
+
const rateLimit =
|
|
529
|
+
typeof this.page.getResumeRateLimitWarning === 'function'
|
|
530
|
+
? await this.page.getResumeRateLimitWarning()
|
|
531
|
+
: { hit: false, text: '' };
|
|
532
|
+
if (rateLimit?.hit) {
|
|
533
|
+
const backoffMs = 90000 + Math.floor(Math.random() * 30000);
|
|
534
|
+
this.setResumeOpenBlocked(backoffMs);
|
|
535
|
+
this.logger.log(
|
|
536
|
+
`检测到简历查看频控提示:${rateLimit.text},进入冷却 ${Math.round(backoffMs / 1000)}s,当前候选跳过。`,
|
|
537
|
+
);
|
|
538
|
+
lastResumeError = new Error(`RESUME_RATE_LIMIT_WARNING:${rateLimit.text}`);
|
|
539
|
+
} else if (openResult && !openDetected) {
|
|
540
|
+
let delayedDetected = false;
|
|
541
|
+
if (typeof this.page.getResumeModalState === 'function') {
|
|
542
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
543
|
+
const delayedState = await this.page.getResumeModalState();
|
|
544
|
+
delayedDetected =
|
|
545
|
+
Boolean(delayedState?.open) ||
|
|
546
|
+
Number(delayedState?.iframeCount || 0) > 0 ||
|
|
547
|
+
(Number(delayedState?.scopeCount || 0) > 0 &&
|
|
548
|
+
Number(delayedState?.closeCount || 0) > 0);
|
|
549
|
+
}
|
|
550
|
+
if (delayedDetected) {
|
|
551
|
+
openDetected = true;
|
|
552
|
+
modalOpened = true;
|
|
553
|
+
this.logger.log('在线简历首次检测未命中,1秒后复检已打开,继续处理。');
|
|
554
|
+
} else {
|
|
555
|
+
lastResumeError = new Error('RESUME_MODAL_NOT_DETECTED_AFTER_SINGLE_DOM_CLICK');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!lastResumeError && openDetected) {
|
|
560
|
+
if (typeof this.page.getResumeProfileFromDom === 'function') {
|
|
561
|
+
resumeProfile = await this.page.getResumeProfileFromDom();
|
|
562
|
+
if (resumeProfile?.ok) {
|
|
563
|
+
this.logger.log(
|
|
564
|
+
`简历结构化信息:school=${resumeProfile.primarySchool || 'n/a'} | major=${resumeProfile.major || 'n/a'} | company=${resumeProfile.company || 'n/a'} | position=${resumeProfile.position || 'n/a'}`,
|
|
565
|
+
);
|
|
566
|
+
baseResult.artifacts.resumeProfile = {
|
|
567
|
+
primarySchool: resumeProfile.primarySchool || '',
|
|
568
|
+
schools: Array.isArray(resumeProfile.schools) ? resumeProfile.schools : [],
|
|
569
|
+
major: resumeProfile.major || '',
|
|
570
|
+
majors: Array.isArray(resumeProfile.majors) ? resumeProfile.majors : [],
|
|
571
|
+
company: resumeProfile.company || '',
|
|
572
|
+
position: resumeProfile.position || '',
|
|
573
|
+
};
|
|
574
|
+
} else {
|
|
575
|
+
this.logger.log(`简历结构化提取未命中:${resumeProfile?.error || 'unknown'}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
this.logger.log(
|
|
579
|
+
`在线简历点击完成:clicked=${Boolean(openResult?.clicked)} | detectedOpen=${openDetected} | by=${openResult?.by || 'unknown'},开始截图探测与拼接。`,
|
|
580
|
+
);
|
|
581
|
+
this.logger.log(
|
|
582
|
+
`在线简历截图前状态:modalOpened=${modalOpened} | openDetected=${openDetected}`,
|
|
583
|
+
);
|
|
584
|
+
try {
|
|
585
|
+
await this.checkpoint();
|
|
586
|
+
capture = await this.resumeCaptureService.captureResume({
|
|
587
|
+
artifactDir,
|
|
588
|
+
waitResumeMs: 30000,
|
|
589
|
+
scrollSettleMs: 500,
|
|
590
|
+
});
|
|
591
|
+
if (capture?.quality?.likelyBlank) {
|
|
592
|
+
const blankBackoffMs = 45000 + Math.floor(Math.random() * 20000);
|
|
593
|
+
this.setResumeOpenBlocked(blankBackoffMs);
|
|
594
|
+
this.logger.log(
|
|
595
|
+
`检测到疑似空白简历截图(luma=${capture?.quality?.luma},std=${capture?.quality?.avgStd}),冷却 ${Math.round(blankBackoffMs / 1000)}s,当前候选跳过。`,
|
|
596
|
+
);
|
|
597
|
+
lastResumeError = new Error('RESUME_CAPTURE_LIKELY_BLANK');
|
|
598
|
+
capture = null;
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
lastResumeError = error;
|
|
602
|
+
}
|
|
603
|
+
} else if (!lastResumeError && !openDetected) {
|
|
604
|
+
lastResumeError = new Error('RESUME_MODAL_NOT_DETECTED');
|
|
605
|
+
}
|
|
606
|
+
if (!capture) {
|
|
607
|
+
throw lastResumeError || new Error('RESUME_CAPTURE_FAILED');
|
|
608
|
+
}
|
|
609
|
+
this.logger.log(
|
|
610
|
+
`截图完成:chunks=${capture.chunkCount} | image=${capture.stitchedImage}`,
|
|
611
|
+
);
|
|
612
|
+
baseResult.artifacts = {
|
|
613
|
+
chunkDir: capture.chunkDir,
|
|
614
|
+
metadataFile: capture.metadataFile,
|
|
615
|
+
stitchedImage: capture.stitchedImage,
|
|
616
|
+
chunkCount: capture.chunkCount,
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
await this.checkpoint();
|
|
620
|
+
const evaluation = await this.llmClient.evaluateResume({
|
|
621
|
+
screeningCriteria: profile.screeningCriteria,
|
|
622
|
+
candidate: {
|
|
623
|
+
name: customer.name || '',
|
|
624
|
+
sourceJob: customer.sourceJob || '',
|
|
625
|
+
resumeProfile: resumeProfile?.ok ? {
|
|
626
|
+
primarySchool: resumeProfile.primarySchool || '',
|
|
627
|
+
schools: Array.isArray(resumeProfile.schools) ? resumeProfile.schools : [],
|
|
628
|
+
major: resumeProfile.major || '',
|
|
629
|
+
majors: Array.isArray(resumeProfile.majors) ? resumeProfile.majors : [],
|
|
630
|
+
company: resumeProfile.company || '',
|
|
631
|
+
position: resumeProfile.position || '',
|
|
632
|
+
} : null,
|
|
633
|
+
},
|
|
634
|
+
imagePath: capture.stitchedImage,
|
|
635
|
+
});
|
|
636
|
+
const finalReason = sanitizeReasonWithResumeProfile(evaluation.reason, resumeProfile);
|
|
637
|
+
if (finalReason !== evaluation.reason) {
|
|
638
|
+
this.logger.log(
|
|
639
|
+
`评估理由学校字段已按主简历纠偏:rawReason=${evaluation.reason} | finalReason=${finalReason}`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
this.logger.log(
|
|
643
|
+
`LLM评估完成:passed=${evaluation.passed} | reason=${finalReason}`,
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
baseResult.reason = finalReason;
|
|
647
|
+
baseResult.passed = evaluation.passed;
|
|
648
|
+
baseResult.decision = evaluation.passed ? 'passed' : 'skipped';
|
|
649
|
+
|
|
650
|
+
await this.checkpoint();
|
|
651
|
+
const closeResult =
|
|
652
|
+
typeof this.page.closeResumeModalDomOnce === 'function'
|
|
653
|
+
? await this.page.closeResumeModalDomOnce()
|
|
654
|
+
: await this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
|
|
655
|
+
modalOpened = false;
|
|
656
|
+
baseResult.artifacts.resumeCloseMethod = closeResult.method;
|
|
657
|
+
baseResult.artifacts.resumeClosed = closeResult.closed;
|
|
658
|
+
this.logger.log(
|
|
659
|
+
`简历关闭结果: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'}`,
|
|
660
|
+
);
|
|
661
|
+
if (!closeResult.closed) {
|
|
662
|
+
baseResult.artifacts.resumeCloseWarning = 'resume modal not fully closed by single DOM close';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (evaluation.passed && !this.dryRun) {
|
|
666
|
+
const greetingText = 'Hi同学,能麻烦发下简历吗?';
|
|
667
|
+
this.logger.log(`候选人通过,先发送消息:${greetingText}`);
|
|
668
|
+
await this.checkpoint();
|
|
669
|
+
const editorState = await this.page.setEditorMessage(greetingText);
|
|
670
|
+
if (!String(editorState?.value || '').includes('Hi同学')) {
|
|
671
|
+
throw new Error('CHAT_EDITOR_MESSAGE_MISMATCH');
|
|
672
|
+
}
|
|
673
|
+
this.logger.log(
|
|
674
|
+
`招呼语写入输入框:activeSubmit=${Boolean(editorState?.activeSubmit)} | valueLen=${String(editorState?.value || '').length}`,
|
|
675
|
+
);
|
|
676
|
+
await this.interaction.sleepRange(320, 120);
|
|
677
|
+
await this.checkpoint();
|
|
678
|
+
const sendResult = await this.page.sendMessage(greetingText);
|
|
679
|
+
if (!sendResult?.sent) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`CHAT_GREETING_SEND_FAILED(method=${sendResult?.method || 'unknown'},editorAfter=${sendResult?.editorAfter || ''})`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
baseResult.artifacts.greetingMessage = greetingText;
|
|
685
|
+
baseResult.artifacts.greetingSent = Boolean(sendResult?.sent);
|
|
686
|
+
baseResult.artifacts.greetingSendMethod = sendResult?.method || 'unknown';
|
|
687
|
+
this.logger.log(
|
|
688
|
+
`招呼语发送结果:sent=${Boolean(sendResult?.sent)} | method=${sendResult?.method || 'unknown'} | cleared=${Boolean(sendResult?.cleared)} | editorAfter=${sendResult?.editorAfter || ''}`,
|
|
689
|
+
);
|
|
690
|
+
await this.interaction.sleepRange(360, 120);
|
|
691
|
+
|
|
692
|
+
this.logger.log('候选人通过,执行求简历动作。');
|
|
693
|
+
await this.checkpoint();
|
|
694
|
+
const messageBefore =
|
|
695
|
+
typeof this.page.getResumeRequestMessageState === 'function'
|
|
696
|
+
? await this.page.getResumeRequestMessageState()
|
|
697
|
+
: { ok: false, count: 0, lastText: '', recent: [] };
|
|
698
|
+
const askResult = await this.page.clickAskResume();
|
|
699
|
+
await this.interaction.sleepRange(460, 150);
|
|
700
|
+
if (askResult?.alreadyRequested) {
|
|
701
|
+
baseResult.requested = true;
|
|
702
|
+
this.logger.log('求简历动作完成:alreadyRequested=true');
|
|
703
|
+
} else {
|
|
704
|
+
await this.checkpoint();
|
|
705
|
+
const confirmResult = await this.page.clickConfirmRequestResume();
|
|
706
|
+
let messageObserved = false;
|
|
707
|
+
let messageAfter = null;
|
|
708
|
+
if (typeof this.page.waitForResumeRequestMessage === 'function') {
|
|
709
|
+
const messageCheck = await this.page.waitForResumeRequestMessage({
|
|
710
|
+
baselineCount: Number(messageBefore?.count || 0),
|
|
711
|
+
timeoutMs: 7000,
|
|
712
|
+
pollMs: 260,
|
|
713
|
+
});
|
|
714
|
+
messageObserved = Boolean(messageCheck?.observed);
|
|
715
|
+
messageAfter = messageCheck?.state || null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const requestedVerified =
|
|
719
|
+
Boolean(confirmResult?.requestedVerified) ||
|
|
720
|
+
Boolean(messageObserved);
|
|
721
|
+
baseResult.requested = requestedVerified;
|
|
722
|
+
if (messageAfter) {
|
|
723
|
+
baseResult.artifacts.resumeRequestMessageBefore = Number(messageBefore?.count || 0);
|
|
724
|
+
baseResult.artifacts.resumeRequestMessageAfter = Number(messageAfter?.count || 0);
|
|
725
|
+
baseResult.artifacts.resumeRequestMessageObserved = messageObserved;
|
|
726
|
+
baseResult.artifacts.resumeRequestMessageLastText = String(messageAfter?.lastText || '');
|
|
727
|
+
}
|
|
728
|
+
this.logger.log(
|
|
729
|
+
`求简历动作完成:confirmed=${Boolean(confirmResult?.confirmed)} | disabledOperateAsk=${Boolean(confirmResult?.uiState?.hasDisabledOperateAsk)} | verified=${requestedVerified} | messageObserved=${messageObserved} | assumed=${Boolean(confirmResult?.assumedRequested)}`,
|
|
730
|
+
);
|
|
731
|
+
if (!requestedVerified) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`REQUEST_RESUME_NOT_CONFIRMED(state=${JSON.stringify(confirmResult?.uiState || {})},messageBefore=${Number(messageBefore?.count || 0)},messageAfter=${Number(messageAfter?.count || 0)})`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
740
|
+
return baseResult;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
if (error?.name === 'StopRequestedError') {
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (modalOpened) {
|
|
747
|
+
try {
|
|
748
|
+
const closeResult =
|
|
749
|
+
typeof this.page.closeResumeModalDomOnce === 'function'
|
|
750
|
+
? await this.page.closeResumeModalDomOnce()
|
|
751
|
+
: await this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
|
|
752
|
+
baseResult.artifacts.resumeCloseMethod = closeResult.method;
|
|
753
|
+
baseResult.artifacts.resumeClosed = closeResult.closed;
|
|
754
|
+
this.logger.log(
|
|
755
|
+
`异常后关闭简历结果: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'}`,
|
|
756
|
+
);
|
|
757
|
+
} catch {}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const message = error.message || String(error);
|
|
761
|
+
if (
|
|
762
|
+
/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/i.test(
|
|
763
|
+
message,
|
|
764
|
+
)
|
|
765
|
+
) {
|
|
766
|
+
baseResult.decision = 'skipped';
|
|
767
|
+
baseResult.reason = `在线简历不可用或未加载,已跳过该候选人(${message})`;
|
|
768
|
+
baseResult.artifacts.resumeUnavailable = true;
|
|
769
|
+
this.logger.log(
|
|
770
|
+
`候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
|
|
771
|
+
);
|
|
772
|
+
} else {
|
|
773
|
+
baseResult.error = message;
|
|
774
|
+
baseResult.decision = 'error';
|
|
775
|
+
this.logger.log(
|
|
776
|
+
`候选人处理异常:name=${customer.name || '未知'} | key=${customer.customerKey} | error=${baseResult.error}`,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
780
|
+
return baseResult;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|