@reconcrap/boss-recommend-mcp 2.1.13 → 2.1.15

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.
@@ -39,7 +39,9 @@ import { createViewportRunGuard } from "../../core/self-heal/index.js";
39
39
  import {
40
40
  callScreeningLlm,
41
41
  compactScreeningLlmResult,
42
+ createFatalLlmRunError,
42
43
  createFailedLlmScreeningResult,
44
+ isFatalLlmProviderError,
43
45
  llmResultToScreening,
44
46
  screenCandidate
45
47
  } from "../../core/screening/index.js";
@@ -51,6 +53,10 @@ import {
51
53
  openRecruitCardDetail,
52
54
  waitForRecruitDetailNetworkEvents
53
55
  } from "./detail.js";
56
+ import {
57
+ clickRecruitActionControl,
58
+ waitForRecruitDetailActionControls
59
+ } from "./actions.js";
54
60
  import {
55
61
  readRecruitCardCandidate,
56
62
  waitForRecruitCardNodeIds
@@ -68,6 +74,7 @@ import {
68
74
  RECRUIT_CARD_SELECTOR,
69
75
  RECRUIT_LIST_CONTAINER_SELECTORS
70
76
  } from "./constants.js";
77
+ import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
71
78
 
72
79
  function compactScreening(screening) {
73
80
  return {
@@ -118,6 +125,164 @@ function createMissingLlmConfigResult() {
118
125
  return createFailedLlmScreeningResult(new Error("LLM screening config is required for production search runs"));
119
126
  }
120
127
 
128
+ function normalizeRecruitPostAction(value) {
129
+ const normalized = String(value || "").trim().toLowerCase();
130
+ if (["", "none", "skip", "no", "不执行", "无", "什么也不做"].includes(normalized)) return "none";
131
+ if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
132
+ return "none";
133
+ }
134
+
135
+ function resolveRecruitPostAction({
136
+ postAction = "none",
137
+ greetCount = 0,
138
+ maxGreetCount = null
139
+ } = {}) {
140
+ const requested = normalizeRecruitPostAction(postAction);
141
+ const currentGreetCount = Number.isInteger(greetCount) && greetCount >= 0 ? greetCount : 0;
142
+ const limit = Number.isInteger(maxGreetCount) && maxGreetCount > 0 ? maxGreetCount : null;
143
+ if (requested === "greet" && limit !== null && currentGreetCount >= limit) {
144
+ return {
145
+ requested,
146
+ effective: "none",
147
+ reason: "greet_limit_reached",
148
+ greet_count: currentGreetCount,
149
+ max_greet_count: limit
150
+ };
151
+ }
152
+ return {
153
+ requested,
154
+ effective: requested,
155
+ reason: "requested_action",
156
+ greet_count: currentGreetCount,
157
+ max_greet_count: limit
158
+ };
159
+ }
160
+
161
+ function compactActionDiscovery(discovery) {
162
+ if (!discovery) return null;
163
+ return {
164
+ ok: Boolean(discovery.ok),
165
+ elapsed_ms: discovery.elapsed_ms,
166
+ summary: discovery.summary || null
167
+ };
168
+ }
169
+
170
+ async function runRecruitPostAction({
171
+ client,
172
+ rootNodeIds = [],
173
+ screening,
174
+ actionDiscovery,
175
+ postAction = "none",
176
+ greetCount = 0,
177
+ maxGreetCount = null,
178
+ executePostAction = true,
179
+ afterClickDelayMs = 900
180
+ } = {}) {
181
+ const plan = resolveRecruitPostAction({
182
+ postAction,
183
+ greetCount,
184
+ maxGreetCount
185
+ });
186
+ const result = {
187
+ requested: postAction,
188
+ execute_post_action: Boolean(executePostAction),
189
+ plan,
190
+ eligible: Boolean(screening?.passed),
191
+ action_attempted: false,
192
+ action_clicked: false,
193
+ counted_as_greet: false,
194
+ reason: ""
195
+ };
196
+
197
+ if (!screening?.passed) {
198
+ result.reason = "screening_not_passed";
199
+ return result;
200
+ }
201
+ if (plan.effective === "none") {
202
+ result.reason = plan.reason === "greet_limit_reached" ? "greet_limit_reached" : "post_action_none";
203
+ return result;
204
+ }
205
+
206
+ const summary = actionDiscovery?.summary || {};
207
+ const control = summary.greet?.control || summary.greet;
208
+ if (!control?.found && !control?.node_id) {
209
+ result.reason = `${plan.effective}_control_not_found`;
210
+ return result;
211
+ }
212
+ result.control = control;
213
+
214
+ if (plan.effective === "greet" && control.continue_chat) {
215
+ result.reason = "already_connected_continue_chat";
216
+ result.already_connected = true;
217
+ return result;
218
+ }
219
+ if (plan.effective === "greet" && control.greet_quota?.exhausted) {
220
+ result.reason = "greet_credits_exhausted";
221
+ result.out_of_greet_credits = true;
222
+ result.stop_run = true;
223
+ return result;
224
+ }
225
+ if (plan.effective === "greet" && control.available === false) {
226
+ result.reason = "greet_control_not_available";
227
+ return result;
228
+ }
229
+ if (control.disabled) {
230
+ result.reason = `${plan.effective}_control_disabled`;
231
+ return result;
232
+ }
233
+ if (!executePostAction) {
234
+ result.reason = "dry_run_post_action";
235
+ result.would_click = true;
236
+ return result;
237
+ }
238
+
239
+ result.action_attempted = true;
240
+ result.control_before = control;
241
+ let clickResult;
242
+ try {
243
+ clickResult = await clickRecruitActionControl(client, {
244
+ ...control,
245
+ kind: plan.effective
246
+ });
247
+ } catch (error) {
248
+ if (error?.code === GREET_CREDITS_EXHAUSTED_CODE) {
249
+ result.reason = "greet_credits_exhausted";
250
+ result.out_of_greet_credits = true;
251
+ result.stop_run = true;
252
+ result.greet_quota = error.greet_quota || control.greet_quota || null;
253
+ return result;
254
+ }
255
+ throw error;
256
+ }
257
+ result.click_result = clickResult;
258
+ result.action_clicked = true;
259
+ result.counted_as_greet = plan.effective === "greet";
260
+ result.reason = "clicked";
261
+ if (afterClickDelayMs > 0) await sleep(afterClickDelayMs);
262
+ try {
263
+ const afterDiscovery = await waitForRecruitDetailActionControls(client, {
264
+ rootNodeIds,
265
+ timeoutMs: 2500,
266
+ intervalMs: 300,
267
+ requireAny: false
268
+ });
269
+ const afterControl = afterDiscovery?.summary?.greet?.control || afterDiscovery?.summary?.greet || null;
270
+ result.action_discovery_after = compactActionDiscovery(afterDiscovery);
271
+ result.control_after = afterControl;
272
+ if (plan.effective === "greet") {
273
+ result.verified_after_click = Boolean(
274
+ afterControl?.continue_chat
275
+ || String(afterControl?.label || "").includes("继续沟通")
276
+ );
277
+ }
278
+ } catch (error) {
279
+ result.verify_error = {
280
+ message: error?.message || String(error)
281
+ };
282
+ }
283
+ return result;
284
+ }
285
+
121
286
  function normalizeSearchParams(searchParams = {}) {
122
287
  return normalizeRecruitSearchParams(searchParams);
123
288
  }
@@ -326,13 +491,17 @@ function createRecoverableDetailFailureScreening(candidate, error) {
326
491
  };
327
492
  }
328
493
 
329
- export function countRecruitResultStatuses(results = []) {
494
+ export function countRecruitResultStatuses(results = [], {
495
+ greetCount = 0
496
+ } = {}) {
330
497
  return {
331
498
  processed: results.length,
332
499
  screened: results.length,
333
500
  detail_opened: results.filter((item) => item.detail).length,
334
501
  passed: results.filter((item) => item.screening?.passed).length,
335
502
  llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
503
+ greet_count: greetCount,
504
+ post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
336
505
  image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
337
506
  detail_open_failed: results.filter((item) => (
338
507
  item.error?.code === "DETAIL_STALE_NODE"
@@ -378,7 +547,13 @@ export async function runRecruitWorkflow({
378
547
  llmImageDetail = "high",
379
548
  imageOutputDir = "",
380
549
  humanRestEnabled = false,
381
- humanBehavior = null
550
+ humanBehavior = null,
551
+ postAction = "none",
552
+ maxGreetCount = null,
553
+ executePostAction = true,
554
+ actionTimeoutMs = 8000,
555
+ actionIntervalMs = 400,
556
+ actionAfterClickDelayMs = 900
382
557
  } = {}, runControl) {
383
558
  if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
384
559
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
@@ -400,6 +575,8 @@ export async function runRecruitWorkflow({
400
575
  });
401
576
  const normalizedSearchParams = normalizeSearchParams(searchParams);
402
577
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
578
+ const normalizedPostAction = normalizeRecruitPostAction(postAction);
579
+ const postActionEnabled = normalizedPostAction !== "none";
403
580
  const useLlmScreening = normalizedScreeningMode !== "deterministic";
404
581
  const limit = Math.max(1, Number(maxCandidates) || 1);
405
582
  const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
@@ -425,6 +602,7 @@ export async function runRecruitWorkflow({
425
602
  }
426
603
  const results = [];
427
604
  const refreshAttempts = [];
605
+ let greetCount = 0;
428
606
  let refreshRounds = 0;
429
607
  let contextRecoveryAttempts = 0;
430
608
  const candidateRecoveryCounts = new Map();
@@ -468,7 +646,7 @@ export async function runRecruitWorkflow({
468
646
  }
469
647
 
470
648
  function updateRecruitProgress(extra = {}) {
471
- const counts = countRecruitResultStatuses(results);
649
+ const counts = countRecruitResultStatuses(results, { greetCount });
472
650
  const listSnapshot = compactInfiniteListState(listState);
473
651
  const humanRestState = humanRestController.getState();
474
652
  runControl.updateProgress({
@@ -508,7 +686,7 @@ export async function runRecruitWorkflow({
508
686
  key: candidateKey,
509
687
  card_node_id: cardNodeId,
510
688
  detail_step: detailStep || null,
511
- counters: countRecruitResultStatuses(results),
689
+ counters: countRecruitResultStatuses(results, { greetCount }),
512
690
  error: compactError(error, "RECRUIT_IN_PROGRESS_ERROR")
513
691
  },
514
692
  candidate_list: compactInfiniteListState(listState)
@@ -564,7 +742,7 @@ export async function runRecruitWorkflow({
564
742
  reason,
565
743
  trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
566
744
  refresh: compactRefresh,
567
- counters: countRecruitResultStatuses(results)
745
+ counters: countRecruitResultStatuses(results, { greetCount })
568
746
  },
569
747
  candidate_list: compactInfiniteListState(listState)
570
748
  });
@@ -594,7 +772,7 @@ export async function runRecruitWorkflow({
594
772
  metadata: {
595
773
  card_count: cardNodeIds.length,
596
774
  forced_recent_viewed: forceRecentViewed,
597
- counters: countRecruitResultStatuses(results)
775
+ counters: countRecruitResultStatuses(results, { greetCount })
598
776
  }
599
777
  });
600
778
  listEndReason = "";
@@ -780,6 +958,7 @@ export async function runRecruitWorkflow({
780
958
 
781
959
  let screeningCandidate = cardCandidate;
782
960
  let detailResult = null;
961
+ let detailActionRootNodeIds = [];
783
962
  let recoverableDetailError = null;
784
963
  let detailStep = "not_started";
785
964
  if (index < detailCountLimit) {
@@ -813,6 +992,7 @@ export async function runRecruitWorkflow({
813
992
  networkRecorder.clear();
814
993
  await maybeHumanActionCooldown("before_detail_open", timings);
815
994
  const openedDetail = await openRecruitCardDetail(client, cardNodeId);
995
+ detailActionRootNodeIds = (openedDetail.detail_state?.roots || []).map((root) => root.nodeId).filter(Boolean);
816
996
  addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
817
997
  addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
818
998
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
@@ -955,7 +1135,7 @@ export async function runRecruitWorkflow({
955
1135
  capture_target_wait: captureTargetWait
956
1136
  };
957
1137
  screeningCandidate = detailResult.candidate;
958
- if (closeDetail) {
1138
+ if (closeDetail && !postActionEnabled) {
959
1139
  detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
960
1140
  await maybeHumanActionCooldown("after_detail_close", timings);
961
1141
  if (!detailResult.close_result?.closed) {
@@ -1022,6 +1202,12 @@ export async function runRecruitWorkflow({
1022
1202
  imageDetail: llmImageDetail
1023
1203
  }));
1024
1204
  } catch (error) {
1205
+ if (isFatalLlmProviderError(error)) {
1206
+ throw createFatalLlmRunError(error, {
1207
+ domain: "recruit",
1208
+ candidate: screeningCandidate
1209
+ });
1210
+ }
1025
1211
  llmResult = createFailedLlmScreeningResult(error);
1026
1212
  }
1027
1213
  }
@@ -1037,6 +1223,70 @@ export async function runRecruitWorkflow({
1037
1223
  : useLlmScreening
1038
1224
  ? llmResultToScreening(llmResult, screeningCandidate)
1039
1225
  : screenCandidate(screeningCandidate, { criteria });
1226
+ let actionDiscovery = null;
1227
+ let postActionResult = null;
1228
+ let closeFailureError = null;
1229
+ let closeRecoveryFailure = null;
1230
+ if (postActionEnabled && detailResult) {
1231
+ const postActionStarted = Date.now();
1232
+ await runControl.waitIfPaused();
1233
+ runControl.throwIfCanceled();
1234
+ runControl.setPhase("recruit:post-action");
1235
+ await maybeHumanActionCooldown("before_post_action", timings);
1236
+ actionDiscovery = await waitForRecruitDetailActionControls(client, {
1237
+ rootNodeIds: detailActionRootNodeIds,
1238
+ timeoutMs: actionTimeoutMs,
1239
+ intervalMs: actionIntervalMs,
1240
+ requireAny: true
1241
+ });
1242
+ postActionResult = await runRecruitPostAction({
1243
+ client,
1244
+ rootNodeIds: detailActionRootNodeIds,
1245
+ screening,
1246
+ actionDiscovery,
1247
+ postAction: normalizedPostAction,
1248
+ greetCount,
1249
+ maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
1250
+ executePostAction,
1251
+ afterClickDelayMs: actionAfterClickDelayMs
1252
+ });
1253
+ if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
1254
+ greetCount += 1;
1255
+ }
1256
+ addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1257
+ }
1258
+ if (postActionEnabled && detailResult && closeDetail) {
1259
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
1260
+ await maybeHumanActionCooldown("after_detail_close", timings);
1261
+ if (!detailResult.close_result?.closed) {
1262
+ closeFailureError = createRecruitCloseFailureError(detailResult.close_result);
1263
+ try {
1264
+ const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeFailureError, {
1265
+ forceRecentViewed: true
1266
+ });
1267
+ detailResult.cv_acquisition = {
1268
+ ...(detailResult.cv_acquisition || {}),
1269
+ close_recovery: {
1270
+ ok: Boolean(recovery.ok),
1271
+ method: recovery.method || "",
1272
+ forced_recent_viewed: Boolean(recovery.forced_recent_viewed),
1273
+ card_count: recovery.card_count || 0
1274
+ }
1275
+ };
1276
+ } catch (error) {
1277
+ closeRecoveryFailure = error;
1278
+ detailResult.cv_acquisition = {
1279
+ ...(detailResult.cv_acquisition || {}),
1280
+ close_recovery: {
1281
+ ok: false,
1282
+ reason: "context_recovery_failed",
1283
+ error: error?.message || String(error),
1284
+ forced_recent_viewed: true
1285
+ }
1286
+ };
1287
+ }
1288
+ }
1289
+ }
1040
1290
  timings.total_ms = Date.now() - candidateStarted;
1041
1291
  const compactResult = {
1042
1292
  index,
@@ -1046,8 +1296,12 @@ export async function runRecruitWorkflow({
1046
1296
  detail: compactDetail(detailResult),
1047
1297
  llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
1048
1298
  screening: compactScreening(screening),
1299
+ action_discovery: compactActionDiscovery(actionDiscovery),
1300
+ post_action: postActionResult,
1049
1301
  error: recoverableDetailError
1050
1302
  ? compactRecoverableDetailError(recoverableDetailError)
1303
+ : closeRecoveryFailure
1304
+ ? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
1051
1305
  : detailResult?.image_evidence?.ok === false
1052
1306
  ? compactError({
1053
1307
  code: detailResult.image_evidence.error_code,
@@ -1115,6 +1369,10 @@ export async function runRecruitWorkflow({
1115
1369
  addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1116
1370
  compactResult.timings.total_ms = Date.now() - candidateStarted;
1117
1371
  }
1372
+ if (postActionResult?.stop_run) {
1373
+ listEndReason = postActionResult.reason || "post_action_stop";
1374
+ break;
1375
+ }
1118
1376
  }
1119
1377
 
1120
1378
  runControl.setPhase("recruit:done");
@@ -1135,7 +1393,7 @@ export async function runRecruitWorkflow({
1135
1393
  refresh_rounds: refreshRounds,
1136
1394
  refresh_attempts: refreshAttempts,
1137
1395
  context_recoveries: contextRecoveryAttempts,
1138
- ...countRecruitResultStatuses(results),
1396
+ ...countRecruitResultStatuses(results, { greetCount }),
1139
1397
  results
1140
1398
  };
1141
1399
  }
@@ -1181,11 +1439,18 @@ export function createRecruitRunService({
1181
1439
  imageOutputDir = "",
1182
1440
  humanRestEnabled = false,
1183
1441
  humanBehavior = null,
1442
+ postAction = "none",
1443
+ maxGreetCount = null,
1444
+ executePostAction = true,
1445
+ actionTimeoutMs = 8000,
1446
+ actionIntervalMs = 400,
1447
+ actionAfterClickDelayMs = 900,
1184
1448
  name = "recruit-domain-run"
1185
1449
  } = {}) {
1186
1450
  if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
1187
1451
  const normalizedSearchParams = normalizeSearchParams(searchParams);
1188
1452
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1453
+ const normalizedPostAction = normalizeRecruitPostAction(postAction);
1189
1454
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1190
1455
  const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
1191
1456
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
@@ -1227,7 +1492,13 @@ export function createRecruitRunService({
1227
1492
  human_behavior_profile: effectiveHumanBehavior.profile,
1228
1493
  human_behavior: effectiveHumanBehavior,
1229
1494
  human_rest_level: effectiveHumanBehavior.restLevel,
1230
- human_rest_enabled: effectiveHumanRestEnabled
1495
+ human_rest_enabled: effectiveHumanRestEnabled,
1496
+ post_action: normalizedPostAction,
1497
+ max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
1498
+ execute_post_action: Boolean(executePostAction),
1499
+ action_timeout_ms: actionTimeoutMs,
1500
+ action_interval_ms: actionIntervalMs,
1501
+ action_after_click_delay_ms: actionAfterClickDelayMs
1231
1502
  },
1232
1503
  progress: {
1233
1504
  card_count: 0,
@@ -1247,6 +1518,8 @@ export function createRecruitRunService({
1247
1518
  human_rest_enabled: effectiveHumanRestEnabled,
1248
1519
  human_rest_count: 0,
1249
1520
  human_rest_ms: 0,
1521
+ greet_count: 0,
1522
+ post_action_clicked: 0,
1250
1523
  last_human_event: null
1251
1524
  },
1252
1525
  checkpoint: {},
@@ -1281,7 +1554,13 @@ export function createRecruitRunService({
1281
1554
  llmImageDetail,
1282
1555
  imageOutputDir,
1283
1556
  humanRestEnabled: effectiveHumanRestEnabled,
1284
- humanBehavior: effectiveHumanBehavior
1557
+ humanBehavior: effectiveHumanBehavior,
1558
+ postAction: normalizedPostAction,
1559
+ maxGreetCount,
1560
+ executePostAction,
1561
+ actionTimeoutMs,
1562
+ actionIntervalMs,
1563
+ actionAfterClickDelayMs
1285
1564
  }, runControl)
1286
1565
  });
1287
1566
  }