@reconcrap/boss-recommend-mcp 2.1.14 → 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.
@@ -53,6 +53,10 @@ import {
53
53
  openRecruitCardDetail,
54
54
  waitForRecruitDetailNetworkEvents
55
55
  } from "./detail.js";
56
+ import {
57
+ clickRecruitActionControl,
58
+ waitForRecruitDetailActionControls
59
+ } from "./actions.js";
56
60
  import {
57
61
  readRecruitCardCandidate,
58
62
  waitForRecruitCardNodeIds
@@ -70,6 +74,7 @@ import {
70
74
  RECRUIT_CARD_SELECTOR,
71
75
  RECRUIT_LIST_CONTAINER_SELECTORS
72
76
  } from "./constants.js";
77
+ import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
73
78
 
74
79
  function compactScreening(screening) {
75
80
  return {
@@ -120,6 +125,164 @@ function createMissingLlmConfigResult() {
120
125
  return createFailedLlmScreeningResult(new Error("LLM screening config is required for production search runs"));
121
126
  }
122
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
+
123
286
  function normalizeSearchParams(searchParams = {}) {
124
287
  return normalizeRecruitSearchParams(searchParams);
125
288
  }
@@ -328,13 +491,17 @@ function createRecoverableDetailFailureScreening(candidate, error) {
328
491
  };
329
492
  }
330
493
 
331
- export function countRecruitResultStatuses(results = []) {
494
+ export function countRecruitResultStatuses(results = [], {
495
+ greetCount = 0
496
+ } = {}) {
332
497
  return {
333
498
  processed: results.length,
334
499
  screened: results.length,
335
500
  detail_opened: results.filter((item) => item.detail).length,
336
501
  passed: results.filter((item) => item.screening?.passed).length,
337
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,
338
505
  image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
339
506
  detail_open_failed: results.filter((item) => (
340
507
  item.error?.code === "DETAIL_STALE_NODE"
@@ -380,7 +547,13 @@ export async function runRecruitWorkflow({
380
547
  llmImageDetail = "high",
381
548
  imageOutputDir = "",
382
549
  humanRestEnabled = false,
383
- humanBehavior = null
550
+ humanBehavior = null,
551
+ postAction = "none",
552
+ maxGreetCount = null,
553
+ executePostAction = true,
554
+ actionTimeoutMs = 8000,
555
+ actionIntervalMs = 400,
556
+ actionAfterClickDelayMs = 900
384
557
  } = {}, runControl) {
385
558
  if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
386
559
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
@@ -402,6 +575,8 @@ export async function runRecruitWorkflow({
402
575
  });
403
576
  const normalizedSearchParams = normalizeSearchParams(searchParams);
404
577
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
578
+ const normalizedPostAction = normalizeRecruitPostAction(postAction);
579
+ const postActionEnabled = normalizedPostAction !== "none";
405
580
  const useLlmScreening = normalizedScreeningMode !== "deterministic";
406
581
  const limit = Math.max(1, Number(maxCandidates) || 1);
407
582
  const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
@@ -427,6 +602,7 @@ export async function runRecruitWorkflow({
427
602
  }
428
603
  const results = [];
429
604
  const refreshAttempts = [];
605
+ let greetCount = 0;
430
606
  let refreshRounds = 0;
431
607
  let contextRecoveryAttempts = 0;
432
608
  const candidateRecoveryCounts = new Map();
@@ -470,7 +646,7 @@ export async function runRecruitWorkflow({
470
646
  }
471
647
 
472
648
  function updateRecruitProgress(extra = {}) {
473
- const counts = countRecruitResultStatuses(results);
649
+ const counts = countRecruitResultStatuses(results, { greetCount });
474
650
  const listSnapshot = compactInfiniteListState(listState);
475
651
  const humanRestState = humanRestController.getState();
476
652
  runControl.updateProgress({
@@ -510,7 +686,7 @@ export async function runRecruitWorkflow({
510
686
  key: candidateKey,
511
687
  card_node_id: cardNodeId,
512
688
  detail_step: detailStep || null,
513
- counters: countRecruitResultStatuses(results),
689
+ counters: countRecruitResultStatuses(results, { greetCount }),
514
690
  error: compactError(error, "RECRUIT_IN_PROGRESS_ERROR")
515
691
  },
516
692
  candidate_list: compactInfiniteListState(listState)
@@ -566,7 +742,7 @@ export async function runRecruitWorkflow({
566
742
  reason,
567
743
  trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
568
744
  refresh: compactRefresh,
569
- counters: countRecruitResultStatuses(results)
745
+ counters: countRecruitResultStatuses(results, { greetCount })
570
746
  },
571
747
  candidate_list: compactInfiniteListState(listState)
572
748
  });
@@ -596,7 +772,7 @@ export async function runRecruitWorkflow({
596
772
  metadata: {
597
773
  card_count: cardNodeIds.length,
598
774
  forced_recent_viewed: forceRecentViewed,
599
- counters: countRecruitResultStatuses(results)
775
+ counters: countRecruitResultStatuses(results, { greetCount })
600
776
  }
601
777
  });
602
778
  listEndReason = "";
@@ -782,6 +958,7 @@ export async function runRecruitWorkflow({
782
958
 
783
959
  let screeningCandidate = cardCandidate;
784
960
  let detailResult = null;
961
+ let detailActionRootNodeIds = [];
785
962
  let recoverableDetailError = null;
786
963
  let detailStep = "not_started";
787
964
  if (index < detailCountLimit) {
@@ -815,6 +992,7 @@ export async function runRecruitWorkflow({
815
992
  networkRecorder.clear();
816
993
  await maybeHumanActionCooldown("before_detail_open", timings);
817
994
  const openedDetail = await openRecruitCardDetail(client, cardNodeId);
995
+ detailActionRootNodeIds = (openedDetail.detail_state?.roots || []).map((root) => root.nodeId).filter(Boolean);
818
996
  addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
819
997
  addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
820
998
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
@@ -957,7 +1135,7 @@ export async function runRecruitWorkflow({
957
1135
  capture_target_wait: captureTargetWait
958
1136
  };
959
1137
  screeningCandidate = detailResult.candidate;
960
- if (closeDetail) {
1138
+ if (closeDetail && !postActionEnabled) {
961
1139
  detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
962
1140
  await maybeHumanActionCooldown("after_detail_close", timings);
963
1141
  if (!detailResult.close_result?.closed) {
@@ -1045,6 +1223,70 @@ export async function runRecruitWorkflow({
1045
1223
  : useLlmScreening
1046
1224
  ? llmResultToScreening(llmResult, screeningCandidate)
1047
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
+ }
1048
1290
  timings.total_ms = Date.now() - candidateStarted;
1049
1291
  const compactResult = {
1050
1292
  index,
@@ -1054,8 +1296,12 @@ export async function runRecruitWorkflow({
1054
1296
  detail: compactDetail(detailResult),
1055
1297
  llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
1056
1298
  screening: compactScreening(screening),
1299
+ action_discovery: compactActionDiscovery(actionDiscovery),
1300
+ post_action: postActionResult,
1057
1301
  error: recoverableDetailError
1058
1302
  ? compactRecoverableDetailError(recoverableDetailError)
1303
+ : closeRecoveryFailure
1304
+ ? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
1059
1305
  : detailResult?.image_evidence?.ok === false
1060
1306
  ? compactError({
1061
1307
  code: detailResult.image_evidence.error_code,
@@ -1123,6 +1369,10 @@ export async function runRecruitWorkflow({
1123
1369
  addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1124
1370
  compactResult.timings.total_ms = Date.now() - candidateStarted;
1125
1371
  }
1372
+ if (postActionResult?.stop_run) {
1373
+ listEndReason = postActionResult.reason || "post_action_stop";
1374
+ break;
1375
+ }
1126
1376
  }
1127
1377
 
1128
1378
  runControl.setPhase("recruit:done");
@@ -1143,7 +1393,7 @@ export async function runRecruitWorkflow({
1143
1393
  refresh_rounds: refreshRounds,
1144
1394
  refresh_attempts: refreshAttempts,
1145
1395
  context_recoveries: contextRecoveryAttempts,
1146
- ...countRecruitResultStatuses(results),
1396
+ ...countRecruitResultStatuses(results, { greetCount }),
1147
1397
  results
1148
1398
  };
1149
1399
  }
@@ -1189,11 +1439,18 @@ export function createRecruitRunService({
1189
1439
  imageOutputDir = "",
1190
1440
  humanRestEnabled = false,
1191
1441
  humanBehavior = null,
1442
+ postAction = "none",
1443
+ maxGreetCount = null,
1444
+ executePostAction = true,
1445
+ actionTimeoutMs = 8000,
1446
+ actionIntervalMs = 400,
1447
+ actionAfterClickDelayMs = 900,
1192
1448
  name = "recruit-domain-run"
1193
1449
  } = {}) {
1194
1450
  if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
1195
1451
  const normalizedSearchParams = normalizeSearchParams(searchParams);
1196
1452
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1453
+ const normalizedPostAction = normalizeRecruitPostAction(postAction);
1197
1454
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1198
1455
  const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
1199
1456
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
@@ -1235,7 +1492,13 @@ export function createRecruitRunService({
1235
1492
  human_behavior_profile: effectiveHumanBehavior.profile,
1236
1493
  human_behavior: effectiveHumanBehavior,
1237
1494
  human_rest_level: effectiveHumanBehavior.restLevel,
1238
- 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
1239
1502
  },
1240
1503
  progress: {
1241
1504
  card_count: 0,
@@ -1255,6 +1518,8 @@ export function createRecruitRunService({
1255
1518
  human_rest_enabled: effectiveHumanRestEnabled,
1256
1519
  human_rest_count: 0,
1257
1520
  human_rest_ms: 0,
1521
+ greet_count: 0,
1522
+ post_action_clicked: 0,
1258
1523
  last_human_event: null
1259
1524
  },
1260
1525
  checkpoint: {},
@@ -1289,7 +1554,13 @@ export function createRecruitRunService({
1289
1554
  llmImageDetail,
1290
1555
  imageOutputDir,
1291
1556
  humanRestEnabled: effectiveHumanRestEnabled,
1292
- humanBehavior: effectiveHumanBehavior
1557
+ humanBehavior: effectiveHumanBehavior,
1558
+ postAction: normalizedPostAction,
1559
+ maxGreetCount,
1560
+ executePostAction,
1561
+ actionTimeoutMs,
1562
+ actionIntervalMs,
1563
+ actionAfterClickDelayMs
1293
1564
  }, runControl)
1294
1565
  });
1295
1566
  }