@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25

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.
Files changed (67) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/chat-runtime-config.js +7 -5
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +302 -145
  16. package/src/core/capture/index.js +2930 -1201
  17. package/src/core/cv-acquisition/index.js +512 -238
  18. package/src/core/cv-capture-target/index.js +513 -299
  19. package/src/core/greet-quota/index.js +71 -71
  20. package/src/core/infinite-list/index.js +31 -31
  21. package/src/core/reporting/legacy-csv.js +12 -12
  22. package/src/core/run/detached-launcher.js +305 -0
  23. package/src/core/run/index.js +109 -55
  24. package/src/core/run/timing.js +33 -33
  25. package/src/core/run/windows-detached-worker.ps1 +106 -0
  26. package/src/core/screening/index.js +2400 -2135
  27. package/src/core/self-heal/index.js +989 -973
  28. package/src/core/self-heal/viewport.js +1505 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/action-journal.js +536 -0
  31. package/src/domains/chat/cards.js +137 -137
  32. package/src/domains/chat/constants.js +9 -9
  33. package/src/domains/chat/detail.js +1684 -401
  34. package/src/domains/chat/index.js +8 -7
  35. package/src/domains/chat/jobs.js +620 -620
  36. package/src/domains/chat/page-guard.js +157 -122
  37. package/src/domains/chat/roots.js +56 -56
  38. package/src/domains/chat/run-service.js +1801 -760
  39. package/src/domains/common/account-rights-panel.js +314 -314
  40. package/src/domains/common/recovery-settle.js +159 -159
  41. package/src/domains/recommend/actions.js +1219 -472
  42. package/src/domains/recommend/cards.js +243 -243
  43. package/src/domains/recommend/colleague-contact.js +1079 -333
  44. package/src/domains/recommend/constants.js +92 -92
  45. package/src/domains/recommend/detail.js +4037 -136
  46. package/src/domains/recommend/filters.js +608 -590
  47. package/src/domains/recommend/index.js +9 -9
  48. package/src/domains/recommend/jobs.js +571 -542
  49. package/src/domains/recommend/location.js +754 -707
  50. package/src/domains/recommend/refresh.js +677 -392
  51. package/src/domains/recommend/roots.js +80 -80
  52. package/src/domains/recommend/run-service.js +4048 -1447
  53. package/src/domains/recommend/scopes.js +246 -246
  54. package/src/domains/recruit/actions.js +277 -277
  55. package/src/domains/recruit/cards.js +74 -74
  56. package/src/domains/recruit/constants.js +236 -236
  57. package/src/domains/recruit/detail.js +588 -588
  58. package/src/domains/recruit/index.js +9 -9
  59. package/src/domains/recruit/instruction-parser.js +866 -866
  60. package/src/domains/recruit/refresh.js +45 -45
  61. package/src/domains/recruit/roots.js +68 -68
  62. package/src/domains/recruit/run-service.js +1817 -1620
  63. package/src/domains/recruit/search.js +3229 -3229
  64. package/src/index.js +16 -1
  65. package/src/parser.js +1296 -1296
  66. package/src/recommend-mcp.js +1061 -450
  67. package/src/recommend-scheduler.js +75 -75
@@ -1,19 +1,21 @@
1
- import fs from "node:fs";
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import process from "node:process";
4
- import {
5
- assertNoForbiddenCdpCalls,
6
- bringPageToFront,
7
- connectToChromeTargetOrOpen,
8
- createBossLoginRequiredError,
9
- detectBossLoginState,
10
- enableDomains,
11
- FORBIDDEN_CDP_METHODS,
5
+ import {
6
+ assertNoForbiddenCdpCalls,
7
+ bringPageToFront,
8
+ connectToChromeTargetOrOpen,
9
+ createBossLoginRequiredError,
10
+ detectBossLoginState,
11
+ enableDomains,
12
+ FORBIDDEN_CDP_METHODS,
12
13
  getMainFrameUrl,
13
- isBossLoginUrl,
14
- waitForMainFrameUrl,
15
- sleep
16
- } from "./core/browser/index.js";
14
+ inspectChromeDebugCommandLine,
15
+ isBossLoginUrl,
16
+ waitForMainFrameUrl,
17
+ sleep
18
+ } from "./core/browser/index.js";
17
19
  import {
18
20
  RUN_STATUS_CANCELING,
19
21
  RUN_STATUS_CANCELED,
@@ -33,14 +35,15 @@ import {
33
35
  runSelfHealCheck
34
36
  } from "./core/self-heal/index.js";
35
37
  import {
36
- closeRecommendJobDropdown,
37
- closeRecommendDetail,
38
- createRecommendRunService,
39
- getRecommendRoots,
40
- listRecommendJobOptions,
41
- RECOMMEND_TARGET_URL,
42
- runRecommendWorkflow
43
- } from "./domains/recommend/index.js";
38
+ closeRecommendJobDropdown,
39
+ closeRecommendDetail,
40
+ createRecommendRunService,
41
+ getRecommendRoots,
42
+ inspectRecommendFilteredEmptyState,
43
+ listRecommendJobOptions,
44
+ RECOMMEND_TARGET_URL,
45
+ runRecommendWorkflow
46
+ } from "./domains/recommend/index.js";
44
47
  import {
45
48
  parseRecommendInstruction
46
49
  } from "./parser.js";
@@ -77,7 +80,7 @@ let recommendRunService = createRecommendRunService({
77
80
  });
78
81
  const recommendRunMeta = new Map();
79
82
 
80
- function normalizeText(value) {
83
+ function normalizeText(value) {
81
84
  return String(value || "").replace(/\s+/g, " ").trim();
82
85
  }
83
86
 
@@ -278,22 +281,22 @@ function normalizeScreeningModeArg(args = {}) {
278
281
  : "llm";
279
282
  }
280
283
 
281
- function collectRecommendDebugTestOptions(args = {}, normalized = {}) {
282
- const reasons = [];
284
+ function collectRecommendDebugTestOptions(args = {}, normalized = {}) {
285
+ const reasons = [];
283
286
  if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
284
287
  if (args.allow_card_only_screening === true) reasons.push("allow_card_only_screening");
285
288
  if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
286
289
  if (args.no_filter === true) reasons.push("no_filter");
287
- if (args.filter_enabled === false) reasons.push("filter_enabled=false");
288
- if (args.debug_force_list_end_after_processed != null) {
289
- reasons.push("debug_force_list_end_after_processed");
290
- }
291
- if (args.debug_force_context_recovery_after_processed != null) {
292
- reasons.push("debug_force_context_recovery_after_processed");
293
- }
294
- if (args.debug_force_cdp_reconnect_after_processed != null) {
295
- reasons.push("debug_force_cdp_reconnect_after_processed");
296
- }
290
+ if (args.filter_enabled === false) reasons.push("filter_enabled=false");
291
+ if (args.debug_force_list_end_after_processed != null) {
292
+ reasons.push("debug_force_list_end_after_processed");
293
+ }
294
+ if (args.debug_force_context_recovery_after_processed != null) {
295
+ reasons.push("debug_force_context_recovery_after_processed");
296
+ }
297
+ if (args.debug_force_cdp_reconnect_after_processed != null) {
298
+ reasons.push("debug_force_cdp_reconnect_after_processed");
299
+ }
297
300
  if (args.dry_run_post_action === true) reasons.push("dry_run_post_action");
298
301
  if (args.execute_post_action === false && normalized.postAction && normalized.postAction !== "none") {
299
302
  reasons.push("execute_post_action=false");
@@ -313,177 +316,347 @@ function resolveRecommendDetailLimit(args = {}, normalized = {}) {
313
316
  return requested;
314
317
  }
315
318
 
316
- function methodSummary(methodLog = []) {
317
- const summary = {};
318
- for (const entry of methodLog || []) {
319
- const method = compactCdpMethodName(entry?.method || entry || "");
320
- if (!method) continue;
321
- summary[method] = (summary[method] || 0) + 1;
322
- }
323
- return summary;
324
- }
325
-
326
- function compactCdpMethodName(value) {
327
- const method = normalizeText(value);
328
- return /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*(?::retry_after_reconnect)?$/.test(method)
329
- ? method
330
- : "";
331
- }
332
-
333
- function compactCdpMethodTimestamp(value) {
334
- const timestamp = normalizeText(value);
335
- const parsed = Date.parse(timestamp);
336
- return Number.isFinite(parsed) ? new Date(parsed).toISOString() : "";
337
- }
338
-
339
- function compactMethodLogForStatus(methodLog = []) {
340
- if (!Array.isArray(methodLog)) return [];
341
- return methodLog
342
- .map((entry) => ({
343
- method: compactCdpMethodName(entry?.method || entry || ""),
344
- at: compactCdpMethodTimestamp(entry?.at || "")
345
- }))
346
- .filter((entry) => Boolean(entry.method))
347
- .slice(-STATUS_METHOD_LOG_TAIL_LIMIT);
348
- }
349
-
350
- function clonePlain(value, fallback = null) {
351
- try {
352
- return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
353
- } catch {
354
- return fallback;
319
+ function methodSummary(methodLog = []) {
320
+ const summary = {};
321
+ for (const entry of methodLog || []) {
322
+ const method = compactCdpMethodName(entry?.method || entry || "");
323
+ if (!method) continue;
324
+ summary[method] = (summary[method] || 0) + 1;
355
325
  }
326
+ return summary;
356
327
  }
357
328
 
358
- function plainRecord(value) {
359
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
329
+ function compactCdpMethodName(value) {
330
+ const method = normalizeText(value);
331
+ return /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*(?::retry_after_reconnect)?$/.test(method)
332
+ ? method
333
+ : "";
360
334
  }
361
335
 
362
- function nonEmptyRecord(value) {
363
- const record = plainRecord(value);
364
- return Object.keys(record).length ? record : null;
336
+ function compactCdpMethodTimestamp(value) {
337
+ const timestamp = normalizeText(value);
338
+ const parsed = Date.parse(timestamp);
339
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : "";
340
+ }
341
+
342
+ function compactMethodLogForStatus(methodLog = []) {
343
+ if (!Array.isArray(methodLog)) return [];
344
+ return methodLog
345
+ .map((entry) => {
346
+ const compact = {
347
+ method: compactCdpMethodName(entry?.method || entry || ""),
348
+ at: compactCdpMethodTimestamp(entry?.at || "")
349
+ };
350
+ for (const key of ["connection_epoch", "replay_of_connection_epoch"]) {
351
+ if (Number.isInteger(entry?.[key]) && entry[key] >= 0) compact[key] = entry[key];
352
+ }
353
+ const replayPolicy = normalizeText(entry?.replay_policy || "");
354
+ if (replayPolicy) compact.replay_policy = replayPolicy.slice(0, 100);
355
+ return compact;
356
+ })
357
+ .filter((entry) => Boolean(entry.method))
358
+ .slice(-STATUS_METHOD_LOG_TAIL_LIMIT);
365
359
  }
366
360
 
367
- function compactMethodSummaryForStatus(value = {}) {
368
- const compact = {};
369
- for (const [rawMethod, rawCount] of Object.entries(plainRecord(value))) {
370
- const method = compactCdpMethodName(rawMethod);
371
- const count = Number(rawCount);
372
- if (!method || !Number.isInteger(count) || count < 1) continue;
373
- compact[method] = count;
374
- }
375
- return compact;
361
+ function canonicalRecommendChromeHost(host = "") {
362
+ const normalized = String(host || "").trim().toLowerCase();
363
+ return ["", "localhost", "127.0.0.1", "::1"].includes(normalized)
364
+ ? "127.0.0.1"
365
+ : normalized;
376
366
  }
377
367
 
378
- function compactChromeEvidenceUrl(value) {
379
- const text = normalizeText(value);
380
- if (!text) return null;
381
- try {
382
- const parsed = new URL(text);
383
- parsed.username = "";
384
- parsed.password = "";
385
- parsed.search = "";
386
- parsed.hash = "";
387
- return parsed.toString();
388
- } catch {
389
- return text.split(/[?#]/, 1)[0] || null;
368
+ function readChromeCommandLineValue(args = [], name = "") {
369
+ const prefix = `--${name}=`;
370
+ for (let index = 0; index < args.length; index += 1) {
371
+ const value = String(args[index] || "");
372
+ if (value.startsWith(prefix)) return value.slice(prefix.length).trim();
373
+ if (value === `--${name}`) return String(args[index + 1] || "").trim();
390
374
  }
375
+ return "";
391
376
  }
392
377
 
393
- function compactChromeAutoLaunchEvidence(value = {}) {
394
- const source = plainRecord(value);
395
- if (!Object.keys(source).length) return null;
396
- const compact = {};
397
- const booleanKeys = [
398
- "launched",
399
- "reused",
400
- "guard_checked",
401
- "required_flags_ok",
402
- "replaced",
403
- "target_created",
404
- "fallback_target",
405
- "fallback_any_page"
406
- ];
407
- for (const key of booleanKeys) {
408
- if (typeof source[key] === "boolean") compact[key] = source[key];
409
- }
410
- for (const key of ["port", "target_count", "command_line_args_count"]) {
411
- if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
378
+ function resolveExactChromeProfileDirectory(userDataDir, explicitProfileDirectory = "") {
379
+ const resolvedUserDataDir = fs.realpathSync(path.resolve(userDataDir));
380
+ let profileDirectory = String(explicitProfileDirectory || "").trim();
381
+ if (!profileDirectory) {
382
+ const localStatePath = path.join(resolvedUserDataDir, "Local State");
383
+ const localState = JSON.parse(fs.readFileSync(localStatePath, "utf8"));
384
+ const lastActive = Array.isArray(localState?.profile?.last_active_profiles)
385
+ ? localState.profile.last_active_profiles.map((item) => String(item || "").trim()).filter(Boolean)
386
+ : [];
387
+ const cachedProfiles = Object.keys(localState?.profile?.info_cache || {}).filter(Boolean);
388
+ const candidates = Array.from(new Set([...lastActive, ...cachedProfiles]));
389
+ if (candidates.length !== 1) {
390
+ const error = new Error(
391
+ "Chrome profile directory is not explicit and Local State does not prove exactly one active profile"
392
+ );
393
+ error.code = "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED";
394
+ throw error;
395
+ }
396
+ profileDirectory = candidates[0];
412
397
  }
413
- for (const key of ["close_method", "command_line_source"]) {
414
- const text = normalizeText(source[key]);
415
- if (text) compact[key] = text;
398
+ const resolvedProfileDir = fs.realpathSync(path.resolve(resolvedUserDataDir, profileDirectory));
399
+ const relativeProfile = path.relative(resolvedUserDataDir, resolvedProfileDir);
400
+ if (!relativeProfile || relativeProfile.startsWith("..") || path.isAbsolute(relativeProfile)) {
401
+ const error = new Error("Chrome profile directory is not a child of the exact user-data directory");
402
+ error.code = "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED";
403
+ throw error;
416
404
  }
417
- return Object.keys(compact).length ? compact : null;
418
- }
419
-
420
- function compactChromeEvidence(value = {}) {
421
- const source = plainRecord(value);
422
- if (!Object.keys(source).length) return null;
423
- const compact = {};
424
- const host = normalizeText(source.host);
425
- const targetId = normalizeText(source.target_id);
426
- const targetUrl = compactChromeEvidenceUrl(source.target_url);
427
- if (host) compact.host = host;
428
- if (Number.isInteger(source.port) && source.port > 0) compact.port = source.port;
429
- if (targetUrl) compact.target_url = targetUrl;
430
- if (targetId) compact.target_id = targetId;
431
- const autoLaunch = compactChromeAutoLaunchEvidence(source.auto_launch);
432
- if (autoLaunch) compact.auto_launch = autoLaunch;
433
- return Object.keys(compact).length ? compact : null;
434
- }
435
-
436
- function methodEvidenceFlags(methodSummaryValue = {}) {
437
- const methods = Object.keys(methodSummaryValue);
438
- return {
439
- runtime_evaluate_used: methods.some((method) => method.split(".", 1)[0] === "Runtime"),
440
- script_injection_used: methods.some((method) => (
441
- FORBIDDEN_CDP_METHODS.has(method.replace(/:retry_after_reconnect$/, ""))
442
- ))
443
- };
444
- }
445
-
446
- function buildPersistedCdpEvidence({ meta = {}, persisted = {} } = {}) {
447
- const hasLiveMethodLog = Array.isArray(meta.methodLog);
448
- const persistedMethodLog = compactMethodLogForStatus(persisted.method_log);
449
- const methodLog = hasLiveMethodLog
450
- ? compactMethodLogForStatus(meta.methodLog)
451
- : persistedMethodLog;
452
- const persistedSummary = compactMethodSummaryForStatus(persisted.method_summary);
453
- const summary = hasLiveMethodLog
454
- ? methodSummary(meta.methodLog)
455
- : Object.keys(persistedSummary).length
456
- ? persistedSummary
457
- : methodSummary(methodLog);
458
- const liveTotal = hasLiveMethodLog ? meta.methodLog.length : null;
459
- const persistedTotal = Number(persisted.method_log_total);
460
- const methodLogTotal = Number.isInteger(liveTotal)
461
- ? liveTotal
462
- : Number.isInteger(persistedTotal) && persistedTotal >= methodLog.length
463
- ? persistedTotal
464
- : methodLog.length;
465
- const inferredFlags = methodEvidenceFlags(summary);
466
405
  return {
467
- runtime_evaluate_used: hasLiveMethodLog
468
- ? inferredFlags.runtime_evaluate_used
469
- : persisted.runtime_evaluate_used === true || inferredFlags.runtime_evaluate_used,
470
- script_injection_used: hasLiveMethodLog
471
- ? inferredFlags.script_injection_used
472
- : persisted.script_injection_used === true || inferredFlags.script_injection_used,
473
- method_summary: summary,
474
- method_log: methodLog,
475
- method_log_total: methodLogTotal,
476
- chrome: compactChromeEvidence(meta.chrome || persisted.chrome)
406
+ user_data_dir: process.platform === "win32" ? resolvedUserDataDir.toLowerCase() : resolvedUserDataDir,
407
+ profile_directory: relativeProfile.replaceAll("\\", "/")
477
408
  };
478
409
  }
479
410
 
480
- function attachPersistedCdpEvidence(payload, persisted = {}) {
411
+ export async function resolveRecommendActionJournalScope({
412
+ host = DEFAULT_RECOMMEND_HOST,
413
+ port = DEFAULT_RECOMMEND_PORT,
414
+ session = null,
415
+ inspectCommandLine = inspectChromeDebugCommandLine,
416
+ strictFresh = false
417
+ } = {}) {
418
+ const canonicalHost = canonicalRecommendChromeHost(host);
419
+ if (canonicalHost !== "127.0.0.1") {
420
+ const error = new Error("Live recommend greetings require a local canonical Chrome profile identity");
421
+ error.code = "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED";
422
+ throw error;
423
+ }
424
+ let userDataDir = "";
425
+ let profileDirectory = "";
426
+ let source = "";
427
+ if (strictFresh === true) {
428
+ let commandLine = null;
429
+ try {
430
+ commandLine = await inspectCommandLine({ host: canonicalHost, port });
431
+ } catch (error) {
432
+ const inspectionError = new Error(
433
+ "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED: fresh Chrome command-line inspection failed"
434
+ );
435
+ inspectionError.code = "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED";
436
+ inspectionError.retryable = false;
437
+ inspectionError.cause = error;
438
+ throw inspectionError;
439
+ }
440
+ const observedPort = Number(readChromeCommandLineValue(
441
+ commandLine?.arguments,
442
+ "remote-debugging-port"
443
+ ));
444
+ if (commandLine?.ok !== true || observedPort !== Number(port)) {
445
+ const inspectionError = new Error(
446
+ "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED: fresh Chrome process/port identity could not be proven"
447
+ );
448
+ inspectionError.code = "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED";
449
+ inspectionError.retryable = false;
450
+ inspectionError.expected_port = Number(port);
451
+ inspectionError.observed_port = Number.isFinite(observedPort) ? observedPort : null;
452
+ throw inspectionError;
453
+ }
454
+ userDataDir = readChromeCommandLineValue(commandLine.arguments, "user-data-dir");
455
+ profileDirectory = readChromeCommandLineValue(commandLine.arguments, "profile-directory");
456
+ source = `fresh_${commandLine.source || "chrome_command_line"}`;
457
+ } else {
458
+ const injected = session?.profile_identity;
459
+ if (injected?.verified === true) {
460
+ userDataDir = String(injected.user_data_dir || "").trim();
461
+ profileDirectory = String(injected.profile_directory || "").trim();
462
+ source = "session_verified_profile_identity";
463
+ } else {
464
+ let commandLine = null;
465
+ try {
466
+ commandLine = await inspectCommandLine({ host: canonicalHost, port });
467
+ } catch {
468
+ commandLine = null;
469
+ }
470
+ if (commandLine?.ok === true) {
471
+ userDataDir = readChromeCommandLineValue(commandLine.arguments, "user-data-dir");
472
+ profileDirectory = readChromeCommandLineValue(commandLine.arguments, "profile-directory");
473
+ source = commandLine.source || "chrome_command_line";
474
+ }
475
+ if (!userDataDir && session?.chrome?.launched === true) {
476
+ userDataDir = String(session.chrome.user_data_dir || "").trim();
477
+ source = "same_call_chrome_launch";
478
+ }
479
+ }
480
+ }
481
+ if (!userDataDir) {
482
+ const error = new Error(
483
+ "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED: exact Chrome user-data/profile directory could not be proven"
484
+ );
485
+ error.code = "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED";
486
+ error.retryable = false;
487
+ throw error;
488
+ }
489
+ let exactProfile;
490
+ try {
491
+ exactProfile = resolveExactChromeProfileDirectory(userDataDir, profileDirectory);
492
+ } catch (error) {
493
+ const safeError = new Error(
494
+ "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED: exact Chrome profile directory could not be proven"
495
+ );
496
+ safeError.code = "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED";
497
+ safeError.retryable = false;
498
+ throw safeError;
499
+ }
500
+ if (!exactProfile.user_data_dir || !exactProfile.profile_directory) {
501
+ const error = new Error("Exact Chrome user-data/profile directory could not be proven");
502
+ error.code = "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED";
503
+ throw error;
504
+ }
505
+ const profileHash = crypto.createHash("sha256")
506
+ .update(
507
+ `boss-chrome-profile-v1\u0000${exactProfile.user_data_dir}\u0000${exactProfile.profile_directory}`,
508
+ "utf8"
509
+ )
510
+ .digest("hex");
481
511
  return {
482
- ...payload,
483
- ...buildPersistedCdpEvidence({ persisted })
512
+ scope: `boss-recommend-profile-v2:${canonicalHost}:profile-sha256:${profileHash}`,
513
+ identity: {
514
+ verified: true,
515
+ source,
516
+ canonical_host: canonicalHost,
517
+ profile_sha256: profileHash,
518
+ profile_directory: exactProfile.profile_directory
519
+ }
484
520
  };
485
521
  }
486
522
 
523
+ function clonePlain(value, fallback = null) {
524
+ try {
525
+ return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
526
+ } catch {
527
+ return fallback;
528
+ }
529
+ }
530
+
531
+ function plainRecord(value) {
532
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
533
+ }
534
+
535
+ function nonEmptyRecord(value) {
536
+ const record = plainRecord(value);
537
+ return Object.keys(record).length ? record : null;
538
+ }
539
+
540
+ function compactMethodSummaryForStatus(value = {}) {
541
+ const compact = {};
542
+ for (const [rawMethod, rawCount] of Object.entries(plainRecord(value))) {
543
+ const method = compactCdpMethodName(rawMethod);
544
+ const count = Number(rawCount);
545
+ if (!method || !Number.isInteger(count) || count < 1) continue;
546
+ compact[method] = count;
547
+ }
548
+ return compact;
549
+ }
550
+
551
+ function compactChromeEvidenceUrl(value) {
552
+ const text = normalizeText(value);
553
+ if (!text) return null;
554
+ try {
555
+ const parsed = new URL(text);
556
+ parsed.username = "";
557
+ parsed.password = "";
558
+ parsed.search = "";
559
+ parsed.hash = "";
560
+ return parsed.toString();
561
+ } catch {
562
+ return text.split(/[?#]/, 1)[0] || null;
563
+ }
564
+ }
565
+
566
+ function compactChromeAutoLaunchEvidence(value = {}) {
567
+ const source = plainRecord(value);
568
+ if (!Object.keys(source).length) return null;
569
+ const compact = {};
570
+ const booleanKeys = [
571
+ "launched",
572
+ "reused",
573
+ "guard_checked",
574
+ "required_flags_ok",
575
+ "replaced",
576
+ "target_created",
577
+ "fallback_target",
578
+ "fallback_any_page"
579
+ ];
580
+ for (const key of booleanKeys) {
581
+ if (typeof source[key] === "boolean") compact[key] = source[key];
582
+ }
583
+ for (const key of ["port", "target_count", "command_line_args_count"]) {
584
+ if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
585
+ }
586
+ for (const key of ["close_method", "command_line_source"]) {
587
+ const text = normalizeText(source[key]);
588
+ if (text) compact[key] = text;
589
+ }
590
+ return Object.keys(compact).length ? compact : null;
591
+ }
592
+
593
+ function compactChromeEvidence(value = {}) {
594
+ const source = plainRecord(value);
595
+ if (!Object.keys(source).length) return null;
596
+ const compact = {};
597
+ const host = normalizeText(source.host);
598
+ const targetId = normalizeText(source.target_id);
599
+ const targetUrl = compactChromeEvidenceUrl(source.target_url);
600
+ if (host) compact.host = host;
601
+ if (Number.isInteger(source.port) && source.port > 0) compact.port = source.port;
602
+ if (targetUrl) compact.target_url = targetUrl;
603
+ if (targetId) compact.target_id = targetId;
604
+ const autoLaunch = compactChromeAutoLaunchEvidence(source.auto_launch);
605
+ if (autoLaunch) compact.auto_launch = autoLaunch;
606
+ return Object.keys(compact).length ? compact : null;
607
+ }
608
+
609
+ function methodEvidenceFlags(methodSummaryValue = {}) {
610
+ const methods = Object.keys(methodSummaryValue);
611
+ return {
612
+ runtime_evaluate_used: methods.some((method) => method.split(".", 1)[0] === "Runtime"),
613
+ script_injection_used: methods.some((method) => (
614
+ FORBIDDEN_CDP_METHODS.has(method.replace(/:retry_after_reconnect$/, ""))
615
+ ))
616
+ };
617
+ }
618
+
619
+ function buildPersistedCdpEvidence({ meta = {}, persisted = {} } = {}) {
620
+ const hasLiveMethodLog = Array.isArray(meta.methodLog);
621
+ const persistedMethodLog = compactMethodLogForStatus(persisted.method_log);
622
+ const methodLog = hasLiveMethodLog
623
+ ? compactMethodLogForStatus(meta.methodLog)
624
+ : persistedMethodLog;
625
+ const persistedSummary = compactMethodSummaryForStatus(persisted.method_summary);
626
+ const summary = hasLiveMethodLog
627
+ ? methodSummary(meta.methodLog)
628
+ : Object.keys(persistedSummary).length
629
+ ? persistedSummary
630
+ : methodSummary(methodLog);
631
+ const liveTotal = hasLiveMethodLog ? meta.methodLog.length : null;
632
+ const persistedTotal = Number(persisted.method_log_total);
633
+ const methodLogTotal = Number.isInteger(liveTotal)
634
+ ? liveTotal
635
+ : Number.isInteger(persistedTotal) && persistedTotal >= methodLog.length
636
+ ? persistedTotal
637
+ : methodLog.length;
638
+ const inferredFlags = methodEvidenceFlags(summary);
639
+ return {
640
+ runtime_evaluate_used: hasLiveMethodLog
641
+ ? inferredFlags.runtime_evaluate_used
642
+ : persisted.runtime_evaluate_used === true || inferredFlags.runtime_evaluate_used,
643
+ script_injection_used: hasLiveMethodLog
644
+ ? inferredFlags.script_injection_used
645
+ : persisted.script_injection_used === true || inferredFlags.script_injection_used,
646
+ method_summary: summary,
647
+ method_log: methodLog,
648
+ method_log_total: methodLogTotal,
649
+ chrome: compactChromeEvidence(meta.chrome || persisted.chrome)
650
+ };
651
+ }
652
+
653
+ function attachPersistedCdpEvidence(payload, persisted = {}) {
654
+ return {
655
+ ...payload,
656
+ ...buildPersistedCdpEvidence({ persisted })
657
+ };
658
+ }
659
+
487
660
  function normalizeRunId(runId) {
488
661
  const normalized = normalizeText(runId);
489
662
  if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
@@ -530,12 +703,12 @@ function readJsonFile(filePath) {
530
703
 
531
704
  function recommendSearchParamsForCsv(searchParams = {}) {
532
705
  return {
533
- school_tag: Object.prototype.hasOwnProperty.call(searchParams, "school_tag") ? searchParams.school_tag : "不限",
534
- degree: Object.prototype.hasOwnProperty.call(searchParams, "degree") ? searchParams.degree : "不限",
535
- gender: Object.prototype.hasOwnProperty.call(searchParams, "gender") ? searchParams.gender : "不限",
536
- recent_not_view: Object.prototype.hasOwnProperty.call(searchParams, "recent_not_view") ? searchParams.recent_not_view : "不限",
537
- current_city_only: Object.prototype.hasOwnProperty.call(searchParams, "current_city_only") ? searchParams.current_city_only : false,
538
- activity_level: Object.prototype.hasOwnProperty.call(searchParams, "activity_level") ? searchParams.activity_level : "不限"
706
+ school_tag: Object.prototype.hasOwnProperty.call(searchParams, "school_tag") ? searchParams.school_tag : "不限",
707
+ degree: Object.prototype.hasOwnProperty.call(searchParams, "degree") ? searchParams.degree : "不限",
708
+ gender: Object.prototype.hasOwnProperty.call(searchParams, "gender") ? searchParams.gender : "不限",
709
+ recent_not_view: Object.prototype.hasOwnProperty.call(searchParams, "recent_not_view") ? searchParams.recent_not_view : "不限",
710
+ current_city_only: Object.prototype.hasOwnProperty.call(searchParams, "current_city_only") ? searchParams.current_city_only : false,
711
+ activity_level: Object.prototype.hasOwnProperty.call(searchParams, "activity_level") ? searchParams.activity_level : "不限"
539
712
  };
540
713
  }
541
714
 
@@ -574,12 +747,12 @@ function selectedRecommendJobForCsv(meta = {}, snapshot = {}) {
574
747
  function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
575
748
  const { context, confirmation, overrides, followUp, shared } = getSnapshotRequestContext(snapshot);
576
749
  const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {
577
- school_tag: overrides.school_tag ?? confirmation.school_tag_value,
578
- degree: overrides.degree ?? confirmation.degree_value,
579
- gender: overrides.gender ?? confirmation.gender_value,
580
- recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value,
581
- current_city_only: overrides.current_city_only ?? false,
582
- activity_level: overrides.activity_level ?? "不限"
750
+ school_tag: overrides.school_tag ?? confirmation.school_tag_value,
751
+ degree: overrides.degree ?? confirmation.degree_value,
752
+ gender: overrides.gender ?? confirmation.gender_value,
753
+ recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value,
754
+ current_city_only: overrides.current_city_only ?? false,
755
+ activity_level: overrides.activity_level ?? "不限"
583
756
  });
584
757
  const parsedScreenParams = meta.parsed?.screenParams || {};
585
758
  const screenParams = {
@@ -654,26 +827,36 @@ function normalizeLegacyProgress(progress = {}, summary = null) {
654
827
  : Number.isInteger(summary?.passed)
655
828
  ? summary.passed
656
829
  : 0;
657
- const normalized = {
658
- ...progress,
830
+ const normalized = {
831
+ ...progress,
659
832
  processed,
660
833
  inspected: processed,
661
834
  screened,
662
835
  passed,
663
836
  skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
664
- greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
665
- post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
666
- };
667
- if (Object.prototype.hasOwnProperty.call(normalized, "last_list_read_stale_diagnostic")) {
668
- normalized.last_list_read_stale_diagnostic = compactListReadStaleDiagnosticForStatus(
669
- normalized.last_list_read_stale_diagnostic
670
- );
671
- }
837
+ greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
838
+ post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
839
+ };
840
+ if (Object.prototype.hasOwnProperty.call(normalized, "last_list_read_stale_diagnostic")) {
841
+ normalized.last_list_read_stale_diagnostic = compactListReadStaleDiagnosticForStatus(
842
+ normalized.last_list_read_stale_diagnostic
843
+ );
844
+ }
672
845
  if (Object.prototype.hasOwnProperty.call(normalized, "list_read_stale_diagnostics")) {
673
846
  normalized.list_read_stale_diagnostics = compactListReadStaleDiagnosticsForStatus(
674
847
  normalized.list_read_stale_diagnostics
675
848
  );
676
849
  }
850
+ if (Object.prototype.hasOwnProperty.call(normalized, "dom_stale_forensic")) {
851
+ normalized.dom_stale_forensic = compactDomStaleForensicForStatus(
852
+ normalized.dom_stale_forensic
853
+ );
854
+ }
855
+ if (Object.prototype.hasOwnProperty.call(normalized, "dom_stale_forensics")) {
856
+ normalized.dom_stale_forensics = compactDomStaleForensicsForStatus(
857
+ normalized.dom_stale_forensics
858
+ );
859
+ }
677
860
  return normalized;
678
861
  }
679
862
 
@@ -703,73 +886,236 @@ function compactPositiveInteger(value, fallback = null) {
703
886
  return Number.isInteger(value) && value >= 0 ? value : fallback;
704
887
  }
705
888
 
706
- function compactSmallRecord(value, fallback = null) {
889
+ function compactSmallRecord(value, fallback = null) {
707
890
  const record = plainRecord(value);
708
891
  if (!Object.keys(record).length) return fallback;
709
- return clonePlain(record, fallback);
710
- }
711
-
892
+ return clonePlain(record, fallback);
893
+ }
894
+
712
895
  function compactListReadStaleDiagnosticForStatus(value) {
713
896
  const source = plainRecord(value);
714
897
  if (!Object.keys(source).length) return null;
715
898
  const compact = {};
716
- for (const key of ["code", "message", "phase", "cdp_method", "cdp_at", "recovery_mode"]) {
899
+ for (const key of [
900
+ "name",
901
+ "code",
902
+ "message",
903
+ "phase",
904
+ "cdp_method",
905
+ "cdp_at",
906
+ "cdp_search_id",
907
+ "cdp_replay_policy",
908
+ "recovery_mode"
909
+ ]) {
717
910
  const text = normalizeText(source[key] || "");
718
911
  if (text) compact[key] = text.slice(0, key === "message" ? 500 : 200);
719
912
  }
720
- for (const key of ["cdp_node_id", "cdp_backend_node_id", "attempt"]) {
913
+ for (const key of [
914
+ "cdp_node_id",
915
+ "cdp_backend_node_id",
916
+ "cdp_connection_epoch",
917
+ "cdp_reconnected_epoch",
918
+ "attempt"
919
+ ]) {
721
920
  if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
722
921
  }
723
- for (const key of ["exhausted", "recovery_applied", "recovered"]) {
922
+ for (const key of [
923
+ "exhausted",
924
+ "recovery_applied",
925
+ "recovered",
926
+ "cdp_replay_suppressed",
927
+ "cdp_outcome_unknown",
928
+ "cdp_reconnected",
929
+ "cdp_replayed_after_reconnect"
930
+ ]) {
724
931
  if (typeof source[key] === "boolean") compact[key] = source[key];
725
932
  }
726
- for (const key of ["at", "recovery_applied_at", "recovered_at"]) {
727
- const text = normalizeText(source[key] || "");
728
- if (text && Number.isFinite(Date.parse(text))) compact[key] = new Date(text).toISOString();
729
- }
933
+ for (const key of ["at", "recovery_applied_at", "recovered_at"]) {
934
+ const text = normalizeText(source[key] || "");
935
+ if (text && Number.isFinite(Date.parse(text))) compact[key] = new Date(text).toISOString();
936
+ }
730
937
  if (Array.isArray(source.cdp_param_keys)) {
731
- compact.cdp_param_keys = source.cdp_param_keys
732
- .map((key) => normalizeText(key))
938
+ compact.cdp_param_keys = source.cdp_param_keys
939
+ .map((key) => normalizeText(key))
733
940
  .filter((key) => /^[A-Za-z][A-Za-z0-9_]*$/.test(key))
734
941
  .slice(0, 20);
735
942
  }
943
+ if (source.cdp_reconnect_error !== undefined && source.cdp_reconnect_error !== null) {
944
+ const reconnectError = plainRecord(source.cdp_reconnect_error);
945
+ if (Object.keys(reconnectError).length) {
946
+ const safeReconnectError = {};
947
+ for (const key of ["name", "code", "message"]) {
948
+ const text = normalizeText(reconnectError[key] || "");
949
+ if (text) safeReconnectError[key] = text.slice(0, key === "message" ? 500 : 200);
950
+ }
951
+ if (Object.keys(safeReconnectError).length) compact.cdp_reconnect_error = safeReconnectError;
952
+ } else {
953
+ const reconnectErrorText = normalizeText(source.cdp_reconnect_error);
954
+ if (reconnectErrorText) compact.cdp_reconnect_error = reconnectErrorText.slice(0, 500);
955
+ }
956
+ }
957
+ return Object.keys(compact).length ? compact : null;
958
+ }
959
+
960
+ function compactDomStaleRootIdentityForStatus(value) {
961
+ const source = plainRecord(value);
962
+ if (!Object.keys(source).length) return null;
963
+ const compact = {};
964
+ for (const key of [
965
+ "connection_epoch",
966
+ "top_document_node_id",
967
+ "iframe_owner_node_id",
968
+ "iframe_document_node_id"
969
+ ]) {
970
+ if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
971
+ }
972
+ const iframeSelector = normalizeText(source.iframe_selector || "");
973
+ if (iframeSelector) compact.iframe_selector = iframeSelector.slice(0, 300);
736
974
  return Object.keys(compact).length ? compact : null;
737
975
  }
738
976
 
739
- function compactListReadStaleDiagnosticsForStatus(value) {
977
+ function compactDomStaleNumericRecordForStatus(value) {
978
+ const source = plainRecord(value);
979
+ if (!Object.keys(source).length) return null;
980
+ const compact = {};
981
+ for (const [key, rawValue] of Object.entries(source)) {
982
+ if (!/^[a-z][a-z0-9_]*$/i.test(key)) continue;
983
+ if (!Number.isInteger(rawValue) || rawValue < 0) continue;
984
+ compact[key] = rawValue;
985
+ if (Object.keys(compact).length >= 30) break;
986
+ }
987
+ return Object.keys(compact).length ? compact : null;
988
+ }
989
+
990
+ function compactDomStaleLifecycleTimelineForStatus(value) {
740
991
  if (!Array.isArray(value)) return [];
741
992
  return value
742
- .map((item) => compactListReadStaleDiagnosticForStatus(item))
993
+ .map((rawEntry) => {
994
+ const entry = plainRecord(rawEntry);
995
+ const compact = {};
996
+ const at = compactCdpMethodTimestamp(entry.at || "");
997
+ if (at) compact.at = at;
998
+ for (const key of ["type", "operation", "frame_id", "parent_frame_id", "loader_id"]) {
999
+ const text = normalizeText(entry[key] || "");
1000
+ if (text) compact[key] = text.slice(0, 300);
1001
+ }
1002
+ if (Number.isInteger(entry.connection_epoch) && entry.connection_epoch >= 0) {
1003
+ compact.connection_epoch = entry.connection_epoch;
1004
+ }
1005
+ const url = compactChromeEvidenceUrl(entry.url);
1006
+ if (url) compact.url = url;
1007
+ return Object.keys(compact).length ? compact : null;
1008
+ })
743
1009
  .filter(Boolean)
744
- .slice(-12);
1010
+ .slice(-20);
745
1011
  }
746
1012
 
747
- function compactListReadStaleCheckpointEvent(value) {
1013
+ function compactDomStaleCandidateForStatus(value) {
748
1014
  const source = plainRecord(value);
749
1015
  if (!Object.keys(source).length) return null;
750
1016
  const compact = {};
751
- const diagnostic = compactListReadStaleDiagnosticForStatus(source.diagnostic || source.trigger);
752
- if (diagnostic) compact.diagnostic = diagnostic;
753
- const recentDiagnostics = compactListReadStaleDiagnosticsForStatus(source.recent_diagnostics);
754
- if (recentDiagnostics.length) compact.recent_diagnostics = recentDiagnostics;
755
- for (const key of ["recovery_count", "recovery_applied_count"]) {
1017
+ for (const key of ["index", "card_node_id", "visible_index", "failing_list_node_id"]) {
756
1018
  if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
757
1019
  }
758
- if (source.recovery_error) {
759
- const recoveryError = compactListReadStaleDiagnosticForStatus(source.recovery_error);
760
- if (recoveryError) compact.recovery_error = recoveryError;
1020
+ const candidateKey = normalizeText(source.key || "");
1021
+ if (candidateKey) compact.key = candidateKey.slice(0, 300);
1022
+ return Object.keys(compact).length ? compact : null;
1023
+ }
1024
+
1025
+ function compactDomStaleRecoveryForStatus(value) {
1026
+ const source = plainRecord(value);
1027
+ if (!Object.keys(source).length) return null;
1028
+ const compact = {};
1029
+ for (const key of ["status", "mode", "escalated_from", "method"]) {
1030
+ const text = normalizeText(source[key] || "");
1031
+ if (text) compact[key] = text.slice(0, 200);
1032
+ }
1033
+ const at = compactCdpMethodTimestamp(source.at || "");
1034
+ if (at) compact.at = at;
1035
+ if (typeof source.ok === "boolean") compact.ok = source.ok;
1036
+ if (Number.isInteger(source.card_count) && source.card_count >= 0) {
1037
+ compact.card_count = source.card_count;
761
1038
  }
762
- if (source.candidate_list) {
763
- const candidateList = plainRecord(source.candidate_list);
764
- compact.candidate_list = {};
765
- for (const key of ["seen_count", "queued_count", "processed_count", "read_error_count", "scroll_count"]) {
766
- if (Number.isInteger(candidateList[key]) && candidateList[key] >= 0) {
767
- compact.candidate_list[key] = candidateList[key];
768
- }
769
- }
1039
+ const error = compactListReadStaleDiagnosticForStatus(source.error);
1040
+ if (error) compact.error = error;
1041
+ return Object.keys(compact).length ? compact : null;
1042
+ }
1043
+
1044
+ function compactDomStaleForensicForStatus(value) {
1045
+ const source = plainRecord(value);
1046
+ if (!Object.keys(source).length) return null;
1047
+ const compact = {};
1048
+ if (Number.isInteger(source.schema_version) && source.schema_version >= 0) {
1049
+ compact.schema_version = source.schema_version;
1050
+ }
1051
+ for (const key of ["event_id", "event_type", "phase", "operation", "detail_step"]) {
1052
+ const text = normalizeText(source[key] || "");
1053
+ if (text) compact[key] = text.slice(0, key === "event_id" ? 300 : 200);
770
1054
  }
1055
+ const at = compactCdpMethodTimestamp(source.at || "");
1056
+ if (at) compact.at = at;
1057
+
1058
+ const candidate = compactDomStaleCandidateForStatus(source.candidate);
1059
+ if (candidate) compact.candidate = candidate;
1060
+ const error = compactListReadStaleDiagnosticForStatus(source.error);
1061
+ if (error) compact.error = error;
1062
+ const preRecoveryRoots = compactDomStaleRootIdentityForStatus(source.pre_recovery_roots);
1063
+ if (preRecoveryRoots) compact.pre_recovery_roots = preRecoveryRoots;
1064
+ const postRecoveryRoots = compactDomStaleRootIdentityForStatus(source.post_recovery_roots);
1065
+ if (postRecoveryRoots) compact.post_recovery_roots = postRecoveryRoots;
1066
+ const candidateList = compactDomStaleNumericRecordForStatus(source.candidate_list);
1067
+ if (candidateList) compact.candidate_list = candidateList;
1068
+ const counters = compactDomStaleNumericRecordForStatus(source.counters);
1069
+ if (counters) compact.counters = counters;
1070
+ const lifecycleTimeline = compactDomStaleLifecycleTimelineForStatus(source.lifecycle_timeline);
1071
+ if (lifecycleTimeline.length) compact.lifecycle_timeline = lifecycleTimeline;
1072
+ const recovery = compactDomStaleRecoveryForStatus(source.recovery);
1073
+ if (recovery) compact.recovery = recovery;
771
1074
  return Object.keys(compact).length ? compact : null;
772
1075
  }
1076
+
1077
+ function compactDomStaleForensicsForStatus(value) {
1078
+ if (!Array.isArray(value)) return [];
1079
+ return value
1080
+ .map((item) => compactDomStaleForensicForStatus(item))
1081
+ .filter(Boolean)
1082
+ .slice(-12);
1083
+ }
1084
+
1085
+ function compactListReadStaleDiagnosticsForStatus(value) {
1086
+ if (!Array.isArray(value)) return [];
1087
+ return value
1088
+ .map((item) => compactListReadStaleDiagnosticForStatus(item))
1089
+ .filter(Boolean)
1090
+ .slice(-12);
1091
+ }
1092
+
1093
+ function compactListReadStaleCheckpointEvent(value) {
1094
+ const source = plainRecord(value);
1095
+ if (!Object.keys(source).length) return null;
1096
+ const compact = {};
1097
+ const diagnostic = compactListReadStaleDiagnosticForStatus(source.diagnostic || source.trigger);
1098
+ if (diagnostic) compact.diagnostic = diagnostic;
1099
+ const recentDiagnostics = compactListReadStaleDiagnosticsForStatus(source.recent_diagnostics);
1100
+ if (recentDiagnostics.length) compact.recent_diagnostics = recentDiagnostics;
1101
+ for (const key of ["recovery_count", "recovery_applied_count"]) {
1102
+ if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
1103
+ }
1104
+ if (source.recovery_error) {
1105
+ const recoveryError = compactListReadStaleDiagnosticForStatus(source.recovery_error);
1106
+ if (recoveryError) compact.recovery_error = recoveryError;
1107
+ }
1108
+ if (source.candidate_list) {
1109
+ const candidateList = plainRecord(source.candidate_list);
1110
+ compact.candidate_list = {};
1111
+ for (const key of ["seen_count", "queued_count", "processed_count", "read_error_count", "scroll_count"]) {
1112
+ if (Number.isInteger(candidateList[key]) && candidateList[key] >= 0) {
1113
+ compact.candidate_list[key] = candidateList[key];
1114
+ }
1115
+ }
1116
+ }
1117
+ return Object.keys(compact).length ? compact : null;
1118
+ }
773
1119
 
774
1120
  function compactRecommendSummaryForStatus(summary) {
775
1121
  if (!summary || typeof summary !== "object" || Array.isArray(summary)) return null;
@@ -817,7 +1163,7 @@ function compactRecommendSummaryForStatus(summary) {
817
1163
  restLevel: normalizeText(summary.human_behavior.restLevel || summary.human_behavior.rest_level || "")
818
1164
  };
819
1165
  }
820
- if (summary.human_rest) {
1166
+ if (summary.human_rest) {
821
1167
  const humanRest = plainRecord(summary.human_rest);
822
1168
  compact.human_rest = {
823
1169
  enabled: humanRest.enabled === true,
@@ -825,23 +1171,27 @@ function compactRecommendSummaryForStatus(summary) {
825
1171
  rest_count: compactPositiveInteger(humanRest.rest_count ?? humanRest.count, null),
826
1172
  total_pause_ms: compactPositiveInteger(humanRest.total_pause_ms ?? humanRest.pause_ms, null)
827
1173
  };
828
- }
829
- for (const key of [
830
- "list_read_stale_recovery_attempts",
831
- "list_read_stale_recovery_applied",
832
- "list_read_stale_recoveries"
833
- ]) {
834
- if (Number.isInteger(summary[key]) && summary[key] >= 0) compact[key] = summary[key];
835
- }
836
- if (summary.last_list_read_recovery_mode) {
837
- compact.last_list_read_recovery_mode = normalizeText(summary.last_list_read_recovery_mode).slice(0, 100);
838
- }
839
- const lastStaleDiagnostic = compactListReadStaleDiagnosticForStatus(
840
- summary.last_list_read_stale_diagnostic
841
- );
842
- if (lastStaleDiagnostic) compact.last_list_read_stale_diagnostic = lastStaleDiagnostic;
1174
+ }
1175
+ for (const key of [
1176
+ "list_read_stale_recovery_attempts",
1177
+ "list_read_stale_recovery_applied",
1178
+ "list_read_stale_recoveries"
1179
+ ]) {
1180
+ if (Number.isInteger(summary[key]) && summary[key] >= 0) compact[key] = summary[key];
1181
+ }
1182
+ if (summary.last_list_read_recovery_mode) {
1183
+ compact.last_list_read_recovery_mode = normalizeText(summary.last_list_read_recovery_mode).slice(0, 100);
1184
+ }
1185
+ const lastStaleDiagnostic = compactListReadStaleDiagnosticForStatus(
1186
+ summary.last_list_read_stale_diagnostic
1187
+ );
1188
+ if (lastStaleDiagnostic) compact.last_list_read_stale_diagnostic = lastStaleDiagnostic;
843
1189
  const staleDiagnostics = compactListReadStaleDiagnosticsForStatus(summary.list_read_stale_diagnostics);
844
1190
  if (staleDiagnostics.length) compact.list_read_stale_diagnostics = staleDiagnostics;
1191
+ const domStaleForensic = compactDomStaleForensicForStatus(summary.dom_stale_forensic);
1192
+ if (domStaleForensic) compact.dom_stale_forensic = domStaleForensic;
1193
+ const domStaleForensics = compactDomStaleForensicsForStatus(summary.dom_stale_forensics);
1194
+ if (domStaleForensics.length) compact.dom_stale_forensics = domStaleForensics;
845
1195
  return compact;
846
1196
  }
847
1197
 
@@ -856,19 +1206,23 @@ function compactRecommendCheckpointForStatus(checkpoint) {
856
1206
  } else if (Number.isInteger(checkpoint.results_count)) {
857
1207
  compact.results_count = checkpoint.results_count;
858
1208
  }
859
- for (const key of STATUS_COUNT_KEYS) {
860
- if (Number.isInteger(checkpoint[key])) compact[key] = checkpoint[key];
861
- }
1209
+ for (const key of STATUS_COUNT_KEYS) {
1210
+ if (Number.isInteger(checkpoint[key])) compact[key] = checkpoint[key];
1211
+ }
862
1212
  for (const key of [
863
- "list_read_stale_recovery",
864
- "list_read_stale_recovery_applied",
865
- "list_read_stale_recovered",
866
- "list_read_stale_recovery_exhausted",
867
- "list_read_stale_recovery_failed"
868
- ]) {
1213
+ "list_read_stale_recovery",
1214
+ "list_read_stale_recovery_applied",
1215
+ "list_read_stale_recovered",
1216
+ "list_read_stale_recovery_exhausted",
1217
+ "list_read_stale_recovery_failed"
1218
+ ]) {
869
1219
  const event = compactListReadStaleCheckpointEvent(checkpoint[key]);
870
1220
  if (event) compact[key] = event;
871
1221
  }
1222
+ const domStaleForensic = compactDomStaleForensicForStatus(checkpoint.dom_stale_forensic);
1223
+ if (domStaleForensic) compact.dom_stale_forensic = domStaleForensic;
1224
+ const domStaleForensics = compactDomStaleForensicsForStatus(checkpoint.dom_stale_forensics);
1225
+ if (domStaleForensics.length) compact.dom_stale_forensics = domStaleForensics;
872
1226
  return compact;
873
1227
  }
874
1228
 
@@ -1268,16 +1622,16 @@ function persistRecommendRunSnapshot(snapshot, {
1268
1622
  const artifacts = getRecommendRunArtifacts(normalized.run_id);
1269
1623
  if (!artifacts) return normalized;
1270
1624
  const existing = readJsonFile(artifacts.run_state_path);
1271
- normalized.control = mergePersistedControlRequest(normalized, existing);
1272
- normalized = coerceCanceledTerminalSnapshot(normalized, existing);
1273
- if (persistActiveCheckpoint) {
1274
- persistRecommendCheckpointSnapshot(snapshot);
1275
- }
1276
- const cdpEvidence = buildPersistedCdpEvidence({
1277
- meta: getRecommendRunMeta(normalized.run_id),
1278
- persisted: existing || {}
1279
- });
1280
- const payload = {
1625
+ normalized.control = mergePersistedControlRequest(normalized, existing);
1626
+ normalized = coerceCanceledTerminalSnapshot(normalized, existing);
1627
+ if (persistActiveCheckpoint) {
1628
+ persistRecommendCheckpointSnapshot(snapshot);
1629
+ }
1630
+ const cdpEvidence = buildPersistedCdpEvidence({
1631
+ meta: getRecommendRunMeta(normalized.run_id),
1632
+ persisted: existing || {}
1633
+ });
1634
+ const payload = {
1281
1635
  run_id: normalized.run_id,
1282
1636
  mode: normalized.mode,
1283
1637
  state: normalized.state,
@@ -1293,14 +1647,14 @@ function persistRecommendRunSnapshot(snapshot, {
1293
1647
  context: normalized.context,
1294
1648
  control: normalized.control,
1295
1649
  resume: normalized.resume,
1296
- error: normalized.error,
1297
- recovery: normalized.recovery,
1298
- result: normalized.result,
1299
- summary: normalized.summary,
1300
- checkpoint: normalized.checkpoint,
1301
- artifacts: normalized.artifacts,
1302
- ...cdpEvidence
1303
- };
1650
+ error: normalized.error,
1651
+ recovery: normalized.recovery,
1652
+ result: normalized.result,
1653
+ summary: normalized.summary,
1654
+ checkpoint: normalized.checkpoint,
1655
+ artifacts: normalized.artifacts,
1656
+ ...cdpEvidence
1657
+ };
1304
1658
  writeJsonAtomic(artifacts.run_state_path, payload);
1305
1659
  return normalized;
1306
1660
  }
@@ -1373,18 +1727,18 @@ function persistRecommendLifecycleSnapshot(snapshot, event = {}) {
1373
1727
  });
1374
1728
  }
1375
1729
 
1376
- function attachMethodEvidence(payload, runId) {
1377
- const meta = getRecommendRunMeta(runId);
1378
- const methodLog = meta.methodLog || [];
1379
- assertNoForbiddenCdpCalls(methodLog);
1380
- return {
1381
- ...payload,
1382
- ...buildPersistedCdpEvidence({
1383
- meta,
1384
- persisted: readRecommendRunState(runId) || {}
1385
- })
1386
- };
1387
- }
1730
+ function attachMethodEvidence(payload, runId) {
1731
+ const meta = getRecommendRunMeta(runId);
1732
+ const methodLog = meta.methodLog || [];
1733
+ assertNoForbiddenCdpCalls(methodLog);
1734
+ return {
1735
+ ...payload,
1736
+ ...buildPersistedCdpEvidence({
1737
+ meta,
1738
+ persisted: readRecommendRunState(runId) || {}
1739
+ })
1740
+ };
1741
+ }
1388
1742
 
1389
1743
  function compactRecommendJobListOption(option, index) {
1390
1744
  const label = normalizeText(option?.label);
@@ -1539,51 +1893,239 @@ export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } =
1539
1893
  }
1540
1894
  }
1541
1895
 
1542
- function compactHealth(check) {
1543
- if (!check) return null;
1544
- return {
1545
- status: check.status,
1546
- summary: check.summary,
1547
- drift_report: check.drift_report,
1548
- probes: (check.probes || []).map((probe) => ({
1549
- id: probe.id,
1550
- type: probe.type,
1551
- status: probe.status,
1896
+ function compactHealth(check) {
1897
+ if (!check) return null;
1898
+ return {
1899
+ status: check.status,
1900
+ summary: check.summary,
1901
+ drift_report: check.drift_report,
1902
+ empty_bootstrap_refresh: check.empty_bootstrap_refresh || null,
1903
+ accepted_empty_bootstrap: check.accepted_empty_bootstrap || null,
1904
+ probes: (check.probes || []).map((probe) => ({
1905
+ id: probe.id,
1906
+ type: probe.type,
1907
+ status: probe.status,
1552
1908
  count: probe.count,
1553
1909
  required: probe.required
1554
1910
  }))
1555
- };
1556
- }
1557
-
1558
- async function waitForHealthyRecommend(client, config, {
1559
- timeoutMs = 90000,
1560
- intervalMs = 1000
1561
- } = {}) {
1562
- const started = Date.now();
1563
- let lastCheck = null;
1564
- while (Date.now() - started <= timeoutMs) {
1565
- const loginDetection = await detectBossLoginState(client).catch(() => null);
1566
- if (loginDetection?.requires_login) {
1567
- return {
1568
- status: "login_required",
1569
- summary: "Boss login is required",
1570
- loginDetection
1571
- };
1572
- }
1573
- const roots = await resolveRecommendSelfHealRoots(client, config);
1911
+ };
1912
+ }
1913
+
1914
+ function hasOnlyCandidateCardsRequiredFailure(check) {
1915
+ const failedRequiredIds = Array.isArray(check?.summary?.failed_required_ids)
1916
+ ? check.summary.failed_required_ids
1917
+ : [];
1918
+ const blockedRequiredIds = Array.isArray(check?.summary?.blocked_required_ids)
1919
+ ? check.summary.blocked_required_ids
1920
+ : [];
1921
+ return check?.status === HEALTH_STATUS.DEGRADED
1922
+ && failedRequiredIds.length === 1
1923
+ && failedRequiredIds[0] === "candidate_cards"
1924
+ && blockedRequiredIds.length === 0;
1925
+ }
1926
+
1927
+ function compactRecommendEmptyStateEvidence(emptyState) {
1928
+ if (!emptyState) return null;
1929
+ return {
1930
+ verified: emptyState.verified === true,
1931
+ reason: emptyState.reason || null,
1932
+ text: emptyState.text || null,
1933
+ node_id: emptyState.node_id ?? null,
1934
+ box: emptyState.box || null,
1935
+ accessibility: emptyState.accessibility ? {
1936
+ available: emptyState.accessibility.available ?? null,
1937
+ verified: emptyState.accessibility.verified === true,
1938
+ reason: emptyState.accessibility.reason || null
1939
+ } : null
1940
+ };
1941
+ }
1942
+
1943
+ function hasExactRecommendFilteredEmptyState(emptyState) {
1944
+ return emptyState?.verified === true
1945
+ && emptyState?.reason === "exact_visible_filtered_empty_state"
1946
+ && emptyState?.accessibility?.verified === true;
1947
+ }
1948
+
1949
+ function attachRecommendEmptyBootstrapRefreshEvidence(check, refreshEvidence, emptyState = null) {
1950
+ if (!check || refreshEvidence?.attempted !== true) return check;
1951
+ return {
1952
+ ...check,
1953
+ empty_bootstrap_refresh: {
1954
+ ...refreshEvidence,
1955
+ after: {
1956
+ health: compactHealth(check),
1957
+ empty_state: compactRecommendEmptyStateEvidence(emptyState)
1958
+ }
1959
+ }
1960
+ };
1961
+ }
1962
+
1963
+ export function getRecommendEmptyBootstrapPreflightAction(
1964
+ check,
1965
+ emptyState = null,
1966
+ refreshEvidence = null
1967
+ ) {
1968
+ if (check?.status === HEALTH_STATUS.HEALTHY) return "accept_healthy";
1969
+ if (!hasOnlyCandidateCardsRequiredFailure(check)) return "wait";
1970
+ if (!hasExactRecommendFilteredEmptyState(emptyState)) return "wait";
1971
+ if (refreshEvidence?.attempted !== true) return "refresh";
1972
+ if (
1973
+ refreshEvidence.completed !== true
1974
+ || refreshEvidence.ok !== true
1975
+ || refreshEvidence.method !== "Page.navigate"
1976
+ || refreshEvidence.target_url !== RECOMMEND_TARGET_URL
1977
+ ) return "wait";
1978
+ return "accept_empty_bootstrap";
1979
+ }
1980
+
1981
+ export function acceptRecommendEmptyBootstrapHealth(check, emptyState, refreshEvidence = null) {
1982
+ if (
1983
+ getRecommendEmptyBootstrapPreflightAction(check, emptyState, refreshEvidence)
1984
+ !== "accept_empty_bootstrap"
1985
+ ) return null;
1986
+
1987
+ const healthWithRefresh = attachRecommendEmptyBootstrapRefreshEvidence(
1988
+ check,
1989
+ refreshEvidence,
1990
+ emptyState
1991
+ );
1992
+ return {
1993
+ ...healthWithRefresh,
1994
+ accepted_empty_bootstrap: {
1995
+ accepted: true,
1996
+ mode: "exact_filtered_empty_bootstrap",
1997
+ reason: emptyState.reason,
1998
+ original_health_status: check.status,
1999
+ failed_required_ids: [...check.summary.failed_required_ids],
2000
+ blocked_required_ids: [...check.summary.blocked_required_ids],
2001
+ root: "frame",
2002
+ empty_state: compactRecommendEmptyStateEvidence(emptyState),
2003
+ refresh: healthWithRefresh.empty_bootstrap_refresh
2004
+ }
2005
+ };
2006
+ }
2007
+
2008
+ export function isRecommendConnectorHealthAccepted(health) {
2009
+ if (health?.status === HEALTH_STATUS.HEALTHY) return true;
2010
+ const accepted = health?.accepted_empty_bootstrap;
2011
+ return accepted?.accepted === true
2012
+ && accepted.mode === "exact_filtered_empty_bootstrap"
2013
+ && accepted.original_health_status === HEALTH_STATUS.DEGRADED
2014
+ && getRecommendEmptyBootstrapPreflightAction(
2015
+ health,
2016
+ accepted.empty_state,
2017
+ health.empty_bootstrap_refresh
2018
+ ) === "accept_empty_bootstrap";
2019
+ }
2020
+
2021
+ async function waitForHealthyRecommend(client, config, {
2022
+ timeoutMs = 90000,
2023
+ intervalMs = 1000,
2024
+ refreshSettleMs = 5000,
2025
+ refreshNavigationTimeoutMs = 20000
2026
+ } = {}) {
2027
+ const started = Date.now();
2028
+ let lastCheck = null;
2029
+ let refreshEvidence = null;
2030
+ while (Date.now() - started <= timeoutMs) {
2031
+ const loginDetection = await detectBossLoginState(client).catch(() => null);
2032
+ if (loginDetection?.requires_login) {
2033
+ return attachRecommendEmptyBootstrapRefreshEvidence({
2034
+ status: "login_required",
2035
+ summary: "Boss login is required",
2036
+ loginDetection
2037
+ }, refreshEvidence);
2038
+ }
2039
+ const roots = await resolveRecommendSelfHealRoots(client, config);
1574
2040
  lastCheck = await runSelfHealCheck({
1575
2041
  client,
1576
2042
  domain: "recommend",
1577
2043
  roots: roots.roots,
1578
2044
  selectorProbes: config.selectorProbes,
1579
- accessibilityProbes: config.accessibilityProbes,
1580
- viewportProbes: config.viewportProbes
1581
- });
1582
- if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
1583
- await sleep(intervalMs);
1584
- }
1585
- return lastCheck;
1586
- }
2045
+ accessibilityProbes: config.accessibilityProbes,
2046
+ viewportProbes: config.viewportProbes
2047
+ });
2048
+ if (lastCheck.status === HEALTH_STATUS.HEALTHY) {
2049
+ return attachRecommendEmptyBootstrapRefreshEvidence(lastCheck, refreshEvidence);
2050
+ }
2051
+ let emptyState = null;
2052
+ if (hasOnlyCandidateCardsRequiredFailure(lastCheck) && roots.roots.frame) {
2053
+ emptyState = await inspectRecommendFilteredEmptyState(
2054
+ client,
2055
+ roots.roots.frame
2056
+ ).catch((error) => ({
2057
+ verified: false,
2058
+ reason: "filtered_empty_state_probe_failed",
2059
+ error: error?.message || String(error)
2060
+ }));
2061
+ }
2062
+ const action = getRecommendEmptyBootstrapPreflightAction(
2063
+ lastCheck,
2064
+ emptyState,
2065
+ refreshEvidence
2066
+ );
2067
+ if (action === "refresh") {
2068
+ const refreshStarted = Date.now();
2069
+ refreshEvidence = {
2070
+ attempted: true,
2071
+ completed: false,
2072
+ ok: false,
2073
+ method: "Page.navigate",
2074
+ target_url: RECOMMEND_TARGET_URL,
2075
+ before: {
2076
+ health: compactHealth(lastCheck),
2077
+ empty_state: compactRecommendEmptyStateEvidence(emptyState)
2078
+ }
2079
+ };
2080
+ try {
2081
+ const navigationResult = await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
2082
+ if (navigationResult?.errorText) {
2083
+ throw new Error(navigationResult.errorText);
2084
+ }
2085
+ const waited = await waitForMainFrameUrl(
2086
+ client,
2087
+ (url) => isBossLoginUrl(url) || !shouldNavigateToRecommend(url),
2088
+ {
2089
+ timeoutMs: refreshNavigationTimeoutMs,
2090
+ intervalMs: Math.max(250, Math.min(1000, intervalMs))
2091
+ }
2092
+ );
2093
+ if (refreshSettleMs > 0) await sleep(refreshSettleMs);
2094
+ refreshEvidence = {
2095
+ ...refreshEvidence,
2096
+ completed: true,
2097
+ ok: waited?.ok === true,
2098
+ navigation: {
2099
+ frame_id: navigationResult?.frameId || null,
2100
+ loader_id: navigationResult?.loaderId || null,
2101
+ observed_url: waited?.url || null,
2102
+ observed_url_ok: waited?.ok === true
2103
+ },
2104
+ elapsed_ms: Date.now() - refreshStarted
2105
+ };
2106
+ } catch (error) {
2107
+ refreshEvidence = {
2108
+ ...refreshEvidence,
2109
+ completed: true,
2110
+ ok: false,
2111
+ error: error?.message || String(error),
2112
+ elapsed_ms: Date.now() - refreshStarted
2113
+ };
2114
+ }
2115
+ continue;
2116
+ }
2117
+ if (action === "accept_empty_bootstrap") {
2118
+ const accepted = acceptRecommendEmptyBootstrapHealth(
2119
+ lastCheck,
2120
+ emptyState,
2121
+ refreshEvidence
2122
+ );
2123
+ if (accepted) return accepted;
2124
+ }
2125
+ await sleep(intervalMs);
2126
+ }
2127
+ return attachRecommendEmptyBootstrapRefreshEvidence(lastCheck, refreshEvidence);
2128
+ }
1587
2129
 
1588
2130
  function shouldNavigateToRecommend(url) {
1589
2131
  return !String(url || "").includes("/web/chat/recommend");
@@ -1677,10 +2219,12 @@ async function connectRecommendChromeSession({
1677
2219
  }
1678
2220
 
1679
2221
  const selfHealConfig = buildRecommendSelfHealConfig();
1680
- const health = await waitForHealthyRecommend(client, selfHealConfig, {
1681
- timeoutMs: slowLive ? 180000 : 90000,
1682
- intervalMs: slowLive ? 1200 : 800
1683
- });
2222
+ const health = await waitForHealthyRecommend(client, selfHealConfig, {
2223
+ timeoutMs: slowLive ? 180000 : 90000,
2224
+ intervalMs: slowLive ? 1200 : 800,
2225
+ refreshSettleMs: slowLive ? 12000 : 5000,
2226
+ refreshNavigationTimeoutMs: slowLive ? 45000 : 20000
2227
+ });
1684
2228
  if (health?.loginDetection?.requires_login) {
1685
2229
  await session.close?.();
1686
2230
  throw createBossLoginRequiredError({
@@ -1691,7 +2235,7 @@ async function connectRecommendChromeSession({
1691
2235
  chrome: session.chrome || null
1692
2236
  });
1693
2237
  }
1694
- if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
2238
+ if (!isRecommendConnectorHealthAccepted(health)) {
1695
2239
  const latestUrl = await getMainFrameUrl(client).catch(() => currentUrl);
1696
2240
  const latestLoginDetection = await detectBossLoginState(client, { currentUrl: latestUrl }).catch(() => ({
1697
2241
  requires_login: isBossLoginUrl(latestUrl),
@@ -1935,55 +2479,59 @@ function buildRecommendFilter(parsed, args = {}) {
1935
2479
  return { enabled: false };
1936
2480
  }
1937
2481
 
1938
- const groups = [{
1939
- group: "activity",
1940
- labels: [parsed.searchParams?.activity_level || "不限"],
1941
- selectAllLabels: false,
1942
- allowUnlimited: true,
1943
- verifySticky: true
1944
- }];
2482
+ const groups = [{
2483
+ group: "activity",
2484
+ labels: [parsed.searchParams?.activity_level || "不限"],
2485
+ selectAllLabels: false,
2486
+ allowUnlimited: true,
2487
+ verifySticky: true
2488
+ }];
1945
2489
  const recentNotView = withoutUnlimited(parsed.searchParams?.recent_not_view);
1946
2490
  if (recentNotView.length) {
1947
- groups.push({
1948
- group: "recentNotView",
1949
- labels: recentNotView,
1950
- selectAllLabels: true
1951
- });
2491
+ groups.push({
2492
+ group: "recentNotView",
2493
+ labels: recentNotView,
2494
+ selectAllLabels: true,
2495
+ verifySticky: true
2496
+ });
1952
2497
  }
1953
2498
 
1954
2499
  const degree = withoutUnlimited(parsed.searchParams?.degree);
1955
2500
  if (degree.length) {
1956
- groups.push({
1957
- group: "degree",
1958
- labels: degree,
1959
- selectAllLabels: true
1960
- });
2501
+ groups.push({
2502
+ group: "degree",
2503
+ labels: degree,
2504
+ selectAllLabels: true,
2505
+ verifySticky: true
2506
+ });
1961
2507
  }
1962
2508
 
1963
2509
  const gender = withoutUnlimited(parsed.searchParams?.gender);
1964
2510
  if (gender.length) {
1965
- groups.push({
1966
- group: "gender",
1967
- labels: gender,
1968
- selectAllLabels: true
1969
- });
2511
+ groups.push({
2512
+ group: "gender",
2513
+ labels: gender,
2514
+ selectAllLabels: true,
2515
+ verifySticky: true
2516
+ });
1970
2517
  }
1971
2518
 
1972
2519
  const school = withoutUnlimited(parsed.searchParams?.school_tag);
1973
2520
  if (school.length) {
1974
- groups.push({
1975
- group: "school",
1976
- labels: school,
1977
- selectAllLabels: true
1978
- });
2521
+ groups.push({
2522
+ group: "school",
2523
+ labels: school,
2524
+ selectAllLabels: true,
2525
+ verifySticky: true
2526
+ });
1979
2527
  }
1980
2528
 
1981
- return {
1982
- enabled: true,
1983
- currentCityOnly: parsed.searchParams?.current_city_only === true,
1984
- filterGroups: groups
1985
- };
1986
- }
2529
+ return {
2530
+ enabled: true,
2531
+ currentCityOnly: parsed.searchParams?.current_city_only === true,
2532
+ filterGroups: groups
2533
+ };
2534
+ }
1987
2535
 
1988
2536
  function normalizeRecommendStartInput(args = {}, parsed, configResolution = null) {
1989
2537
  const confirmation = args.confirmation || {};
@@ -2017,7 +2565,14 @@ function normalizeRecommendStartInput(args = {}, parsed, configResolution = null
2017
2565
  };
2018
2566
  }
2019
2567
 
2020
- function getRunOptions(args, parsed, normalized, session, configResolution = null) {
2568
+ function getRunOptions(
2569
+ args,
2570
+ parsed,
2571
+ normalized,
2572
+ session,
2573
+ configResolution = null,
2574
+ actionJournalScope = ""
2575
+ ) {
2021
2576
  const slowLive = args.slow_live === true;
2022
2577
  const executePostAction = args.dry_run_post_action === true
2023
2578
  ? false
@@ -2052,8 +2607,16 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
2052
2607
  maxGreetCount: normalized.maxGreetCount,
2053
2608
  executePostAction,
2054
2609
  actionTimeoutMs: parsePositiveInteger(args.action_timeout_ms, slowLive ? 12000 : 8000),
2055
- actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
2056
- actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
2610
+ actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
2611
+ actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
2612
+ actionJournalScope,
2613
+ reverifyActionJournalScope: actionJournalScope
2614
+ ? async () => resolveRecommendActionJournalScope({
2615
+ host: normalized.host,
2616
+ port: normalized.port,
2617
+ strictFresh: true
2618
+ })
2619
+ : null,
2057
2620
  screeningMode: normalized.screeningMode,
2058
2621
  llmConfig: normalized.screeningMode === "llm" && configResolution?.ok ? {
2059
2622
  ...configResolution.config
@@ -2071,23 +2634,23 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
2071
2634
  ) || "low",
2072
2635
  imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
2073
2636
  humanRestEnabled: humanBehavior.restEnabled,
2074
- humanBehavior,
2075
- skipRecentColleagueContacted: normalized.skipRecentColleagueContacted,
2076
- colleagueContactWindowDays: normalized.colleagueContactWindowDays,
2077
- debugTestMode: isDebugTestMode(args),
2078
- debugForceListEndAfterProcessed: parsePositiveInteger(
2079
- args.debug_force_list_end_after_processed,
2080
- null
2081
- ),
2082
- debugForceContextRecoveryAfterProcessed: parsePositiveInteger(
2083
- args.debug_force_context_recovery_after_processed,
2084
- null
2085
- ),
2086
- debugForceCdpReconnectAfterProcessed: parsePositiveInteger(
2087
- args.debug_force_cdp_reconnect_after_processed,
2088
- null
2089
- ),
2090
- name: "mcp-recommend-pipeline-run",
2637
+ humanBehavior,
2638
+ skipRecentColleagueContacted: normalized.skipRecentColleagueContacted,
2639
+ colleagueContactWindowDays: normalized.colleagueContactWindowDays,
2640
+ debugTestMode: isDebugTestMode(args),
2641
+ debugForceListEndAfterProcessed: parsePositiveInteger(
2642
+ args.debug_force_list_end_after_processed,
2643
+ null
2644
+ ),
2645
+ debugForceContextRecoveryAfterProcessed: parsePositiveInteger(
2646
+ args.debug_force_context_recovery_after_processed,
2647
+ null
2648
+ ),
2649
+ debugForceCdpReconnectAfterProcessed: parsePositiveInteger(
2650
+ args.debug_force_cdp_reconnect_after_processed,
2651
+ null
2652
+ ),
2653
+ name: "mcp-recommend-pipeline-run",
2091
2654
  parsed
2092
2655
  };
2093
2656
  }
@@ -2135,14 +2698,27 @@ function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
2135
2698
  };
2136
2699
  }
2137
2700
 
2138
- async function closeRecommendRunSession(runId) {
2139
- const meta = recommendRunMeta.get(runId);
2140
- if (!meta || meta.closed) return;
2141
- try {
2142
- try {
2143
- if (meta.session?.client) {
2144
- await closeRecommendDetail(meta.session.client, { attemptsLimit: 2 });
2145
- }
2701
+ export function shouldPreserveRecommendDetailOnTerminal(snapshot = null) {
2702
+ return snapshot?.checkpoint?.preserve_detail_on_terminal === true
2703
+ && snapshot?.checkpoint?.action_result_critical_persisted !== true;
2704
+ }
2705
+
2706
+ async function closeRecommendRunSession(runId) {
2707
+ const meta = recommendRunMeta.get(runId);
2708
+ if (!meta || meta.closed) return;
2709
+ try {
2710
+ try {
2711
+ let preserveDetail = false;
2712
+ try {
2713
+ const snapshot = recommendRunService.getRecommendRun(runId);
2714
+ preserveDetail = shouldPreserveRecommendDetailOnTerminal(snapshot);
2715
+ } catch {
2716
+ // If the in-memory run is unavailable, retain the existing best-effort cleanup policy.
2717
+ }
2718
+ meta.preserve_detail_on_terminal = preserveDetail;
2719
+ if (meta.session?.client && !preserveDetail) {
2720
+ await closeRecommendDetail(meta.session.client, { attemptsLimit: 2 });
2721
+ }
2146
2722
  } catch {
2147
2723
  // Cleanup is best-effort once the run has settled.
2148
2724
  }
@@ -2222,12 +2798,46 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
2222
2798
  },
2223
2799
  chrome: error?.chrome || null
2224
2800
  };
2225
- }
2226
-
2227
- let started;
2228
- try {
2229
- started = recommendRunService.startRecommendRun({
2230
- ...getRunOptions(args, parsed, normalized, session, configResolution),
2801
+ }
2802
+
2803
+ let actionJournalScope = "";
2804
+ let actionProfileIdentity = null;
2805
+ const liveGreetingRequested = normalized.postAction === "greet"
2806
+ && args.dry_run_post_action !== true
2807
+ && args.execute_post_action !== false;
2808
+ if (liveGreetingRequested) {
2809
+ try {
2810
+ const resolvedScope = await resolveRecommendActionJournalScope({
2811
+ host: normalized.host,
2812
+ port: normalized.port,
2813
+ session
2814
+ });
2815
+ actionJournalScope = resolvedScope.scope;
2816
+ actionProfileIdentity = resolvedScope.identity;
2817
+ } catch (error) {
2818
+ await session.close?.();
2819
+ return {
2820
+ status: "FAILED",
2821
+ error: {
2822
+ code: error?.code || "RECOMMEND_ACTION_PROFILE_IDENTITY_UNVERIFIED",
2823
+ message: error?.message || "Exact Chrome profile identity could not be proven",
2824
+ retryable: false
2825
+ }
2826
+ };
2827
+ }
2828
+ }
2829
+
2830
+ let started;
2831
+ try {
2832
+ started = recommendRunService.startRecommendRun({
2833
+ ...getRunOptions(
2834
+ args,
2835
+ parsed,
2836
+ normalized,
2837
+ session,
2838
+ configResolution,
2839
+ actionJournalScope
2840
+ ),
2231
2841
  runId: fixedRunId || undefined,
2232
2842
  pid: process.pid
2233
2843
  });
@@ -2254,8 +2864,9 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
2254
2864
  host: normalized.host,
2255
2865
  port: normalized.port,
2256
2866
  target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
2257
- target_id: session.target?.id || null,
2258
- auto_launch: session.chrome || null
2867
+ target_id: session.target?.id || null,
2868
+ auto_launch: session.chrome || null,
2869
+ action_profile_identity: actionProfileIdentity
2259
2870
  },
2260
2871
  health: session.health || null
2261
2872
  });
@@ -2379,19 +2990,19 @@ export function getRecommendPipelineRunTool({ args = {} } = {}) {
2379
2990
  run: compactRecommendRunForStatus(normalizedRun)
2380
2991
  }, runId);
2381
2992
  } catch {
2382
- const persisted = readRecommendRunState(runId);
2383
- if (persisted) {
2384
- const reconciled = compactRecommendRunForStatus(reconcilePersistedRecommendRunIfNeeded(persisted));
2385
- const persistedEvidence = readRecommendRunState(runId) || persisted;
2386
- return attachPersistedCdpEvidence({
2387
- status: "RUN_STATUS",
2388
- run: reconciled,
2389
- persistence: {
2390
- source: "disk",
2391
- active_control_available: false,
2392
- stale_process_reconciled: reconciled?.state !== persisted.state
2393
- }
2394
- }, persistedEvidence);
2993
+ const persisted = readRecommendRunState(runId);
2994
+ if (persisted) {
2995
+ const reconciled = compactRecommendRunForStatus(reconcilePersistedRecommendRunIfNeeded(persisted));
2996
+ const persistedEvidence = readRecommendRunState(runId) || persisted;
2997
+ return attachPersistedCdpEvidence({
2998
+ status: "RUN_STATUS",
2999
+ run: reconciled,
3000
+ persistence: {
3001
+ source: "disk",
3002
+ active_control_available: false,
3003
+ stale_process_reconciled: reconciled?.state !== persisted.state
3004
+ }
3005
+ }, persistedEvidence);
2395
3006
  }
2396
3007
  return {
2397
3008
  status: "FAILED",
@@ -2432,13 +3043,13 @@ export function pauseRecommendPipelineRunTool({ args = {} } = {}) {
2432
3043
  message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
2433
3044
  }, runId);
2434
3045
  } catch {
2435
- const persisted = readRecommendRunState(runId);
2436
- if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2437
- return attachPersistedCdpEvidence({
2438
- status: "PAUSE_IGNORED",
2439
- run: compactRecommendRunForStatus(persisted),
2440
- message: "目标任务已结束,无需暂停。"
2441
- }, persisted);
3046
+ const persisted = readRecommendRunState(runId);
3047
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
3048
+ return attachPersistedCdpEvidence({
3049
+ status: "PAUSE_IGNORED",
3050
+ run: compactRecommendRunForStatus(persisted),
3051
+ message: "目标任务已结束,无需暂停。"
3052
+ }, persisted);
2442
3053
  }
2443
3054
  return getRecommendPipelineRunTool({ args });
2444
3055
  }
@@ -2486,11 +3097,11 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2486
3097
  message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
2487
3098
  }, runId);
2488
3099
  } catch {
2489
- const persisted = readRecommendRunState(runId);
2490
- if (persisted) {
2491
- return attachPersistedCdpEvidence({
2492
- status: "FAILED",
2493
- error: {
3100
+ const persisted = readRecommendRunState(runId);
3101
+ if (persisted) {
3102
+ return attachPersistedCdpEvidence({
3103
+ status: "FAILED",
3104
+ error: {
2494
3105
  code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
2495
3106
  message: TERMINAL_STATUSES.has(persisted.state)
2496
3107
  ? "目标任务已结束,无法继续。"
@@ -2498,11 +3109,11 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2498
3109
  retryable: !TERMINAL_STATUSES.has(persisted.state)
2499
3110
  },
2500
3111
  run: compactRecommendRunForStatus(persisted),
2501
- persistence: {
2502
- source: "disk",
2503
- active_control_available: false
2504
- }
2505
- }, persisted);
3112
+ persistence: {
3113
+ source: "disk",
3114
+ active_control_available: false
3115
+ }
3116
+ }, persisted);
2506
3117
  }
2507
3118
  return getRecommendPipelineRunTool({ args });
2508
3119
  }
@@ -2528,13 +3139,13 @@ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
2528
3139
  message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
2529
3140
  }, runId);
2530
3141
  } catch {
2531
- const persisted = readRecommendRunState(runId);
2532
- if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2533
- return attachPersistedCdpEvidence({
2534
- status: "CANCEL_IGNORED",
2535
- run: compactRecommendRunForStatus(persisted),
2536
- message: "目标任务已结束,无需取消。"
2537
- }, persisted);
3142
+ const persisted = readRecommendRunState(runId);
3143
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
3144
+ return attachPersistedCdpEvidence({
3145
+ status: "CANCEL_IGNORED",
3146
+ run: compactRecommendRunForStatus(persisted),
3147
+ message: "目标任务已结束,无需取消。"
3148
+ }, persisted);
2538
3149
  }
2539
3150
  const cancelMessage = "已收到取消请求,将由 detached worker 在下一个安全边界停止。";
2540
3151
  const patched = patchPersistedRecommendRunControl(runId, {
@@ -2544,18 +3155,18 @@ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
2544
3155
  cancel_requested: true
2545
3156
  }, {
2546
3157
  message: cancelMessage
2547
- });
2548
- if (patched) {
2549
- return attachPersistedCdpEvidence({
2550
- status: "CANCEL_REQUESTED",
2551
- run: compactRecommendRunForStatus(patched),
3158
+ });
3159
+ if (patched) {
3160
+ return attachPersistedCdpEvidence({
3161
+ status: "CANCEL_REQUESTED",
3162
+ run: compactRecommendRunForStatus(patched),
2552
3163
  message: cancelMessage,
2553
3164
  persistence: {
2554
3165
  source: "disk",
2555
- active_control_available: false,
2556
- detached_control_requested: true
2557
- }
2558
- }, patched);
3166
+ active_control_available: false,
3167
+ detached_control_requested: true
3168
+ }
3169
+ }, patched);
2559
3170
  }
2560
3171
  return getRecommendPipelineRunTool({ args });
2561
3172
  }