hunter-mate 0.1.7 → 0.1.8

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 (220) hide show
  1. package/dist/assets/extension/background/service-worker.js +727 -152
  2. package/dist/assets/extension/build-info.json +1 -0
  3. package/dist/assets/extension/content/index.js +22 -4
  4. package/dist/assets/extension/manifest.json +5 -4
  5. package/dist/assets/extension/offscreen/offscreen.html +8 -0
  6. package/dist/assets/extension/offscreen/offscreen.js +149 -0
  7. package/dist/assets/extension/package.json +8 -1
  8. package/dist/assets/extension/sidepanel/index.css +2 -0
  9. package/dist/assets/extension/sidepanel/index.html +7 -394
  10. package/dist/assets/extension/sidepanel/index.js +53 -923
  11. package/dist/assets/server/app.module.js +11 -1
  12. package/dist/assets/server/app.module.js.map +1 -1
  13. package/dist/assets/server/auth/auth.middleware.js +9 -1
  14. package/dist/assets/server/auth/auth.middleware.js.map +1 -1
  15. package/dist/assets/server/build-info.json +1 -1
  16. package/dist/assets/server/customer/customer.controller.d.ts +18 -0
  17. package/dist/assets/server/customer/customer.controller.js +81 -0
  18. package/dist/assets/server/customer/customer.controller.js.map +1 -0
  19. package/dist/assets/server/customer/customer.entity.d.ts +17 -0
  20. package/dist/assets/server/customer/customer.entity.js +82 -0
  21. package/dist/assets/server/customer/customer.entity.js.map +1 -0
  22. package/dist/assets/server/customer/customer.module.d.ts +2 -0
  23. package/dist/assets/server/customer/customer.module.js +28 -0
  24. package/dist/assets/server/customer/customer.module.js.map +1 -0
  25. package/dist/assets/server/customer/customer.service.d.ts +36 -0
  26. package/dist/assets/server/customer/customer.service.js +200 -0
  27. package/dist/assets/server/customer/customer.service.js.map +1 -0
  28. package/dist/assets/server/db/schema-validator.service.d.ts +2 -0
  29. package/dist/assets/server/db/schema-validator.service.js +32 -1
  30. package/dist/assets/server/db/schema-validator.service.js.map +1 -1
  31. package/dist/assets/server/db/sqlite.module.d.ts +1 -0
  32. package/dist/assets/server/db/sqlite.module.js +12 -2
  33. package/dist/assets/server/db/sqlite.module.js.map +1 -1
  34. package/dist/assets/server/devops/devops-sink-init.d.ts +1 -14
  35. package/dist/assets/server/devops/devops-sink-init.js +3 -8
  36. package/dist/assets/server/devops/devops-sink-init.js.map +1 -1
  37. package/dist/assets/server/extension/extension-control.controller.d.ts +12 -0
  38. package/dist/assets/server/extension/extension-control.controller.js +41 -1
  39. package/dist/assets/server/extension/extension-control.controller.js.map +1 -1
  40. package/dist/assets/server/extension/extension-control.service.d.ts +6 -1
  41. package/dist/assets/server/extension/extension-control.service.js +27 -1
  42. package/dist/assets/server/extension/extension-control.service.js.map +1 -1
  43. package/dist/assets/server/extraction/adapter.service.d.ts +3 -0
  44. package/dist/assets/server/extraction/adapter.service.js +127 -6
  45. package/dist/assets/server/extraction/adapter.service.js.map +1 -1
  46. package/dist/assets/server/extraction/anomaly-report.service.js +1 -1
  47. package/dist/assets/server/extraction/anomaly-report.service.js.map +1 -1
  48. package/dist/assets/server/extraction/entities/adapter-rule.entity.d.ts +2 -0
  49. package/dist/assets/server/extraction/entities/adapter-rule.entity.js +10 -0
  50. package/dist/assets/server/extraction/entities/adapter-rule.entity.js.map +1 -1
  51. package/dist/assets/server/extraction/extraction-entity.controller.d.ts +21 -3
  52. package/dist/assets/server/extraction/extraction-entity.controller.js +44 -9
  53. package/dist/assets/server/extraction/extraction-entity.controller.js.map +1 -1
  54. package/dist/assets/server/extraction/extraction.module.js +13 -3
  55. package/dist/assets/server/extraction/extraction.module.js.map +1 -1
  56. package/dist/assets/server/extraction/extraction.service.d.ts +4 -1
  57. package/dist/assets/server/extraction/extraction.service.js +59 -4
  58. package/dist/assets/server/extraction/extraction.service.js.map +1 -1
  59. package/dist/assets/server/extraction/harness-overlay.service.d.ts +41 -0
  60. package/dist/assets/server/extraction/harness-overlay.service.js +319 -0
  61. package/dist/assets/server/extraction/harness-overlay.service.js.map +1 -0
  62. package/dist/assets/server/extraction/liepin-extractor.d.ts +1 -0
  63. package/dist/assets/server/extraction/liepin-extractor.js +172 -0
  64. package/dist/assets/server/extraction/liepin-extractor.js.map +1 -0
  65. package/dist/assets/server/extraction/script-rule.js +4 -0
  66. package/dist/assets/server/extraction/script-rule.js.map +1 -1
  67. package/dist/assets/server/governance/action-audit.entity.d.ts +12 -0
  68. package/dist/assets/server/governance/action-audit.entity.js +66 -0
  69. package/dist/assets/server/governance/action-audit.entity.js.map +1 -0
  70. package/dist/assets/server/governance/governance.controller.d.ts +14 -0
  71. package/dist/assets/server/governance/governance.controller.js +77 -0
  72. package/dist/assets/server/governance/governance.controller.js.map +1 -0
  73. package/dist/assets/server/governance/governance.ingester.d.ts +20 -0
  74. package/dist/assets/server/governance/governance.ingester.js +114 -0
  75. package/dist/assets/server/governance/governance.ingester.js.map +1 -0
  76. package/dist/assets/server/governance/governance.module.d.ts +2 -0
  77. package/dist/assets/server/governance/governance.module.js +26 -0
  78. package/dist/assets/server/governance/governance.module.js.map +1 -0
  79. package/dist/assets/server/governance/governance.service.d.ts +114 -0
  80. package/dist/assets/server/governance/governance.service.js +356 -0
  81. package/dist/assets/server/governance/governance.service.js.map +1 -0
  82. package/dist/assets/server/jobcase/jobcase.controller.d.ts +1 -0
  83. package/dist/assets/server/jobcase/jobcase.entity.d.ts +1 -0
  84. package/dist/assets/server/jobcase/jobcase.entity.js +6 -0
  85. package/dist/assets/server/jobcase/jobcase.entity.js.map +1 -1
  86. package/dist/assets/server/jobcase/jobcase.service.d.ts +2 -0
  87. package/dist/assets/server/jobcase/jobcase.service.js +2 -1
  88. package/dist/assets/server/jobcase/jobcase.service.js.map +1 -1
  89. package/dist/assets/server/main.js +1 -0
  90. package/dist/assets/server/main.js.map +1 -1
  91. package/dist/assets/server/package.json +1 -1
  92. package/dist/assets/server/preference/preference.controller.d.ts +14 -0
  93. package/dist/assets/server/preference/preference.controller.js +66 -0
  94. package/dist/assets/server/preference/preference.controller.js.map +1 -0
  95. package/dist/assets/server/preference/preference.entity.d.ts +13 -0
  96. package/dist/assets/server/preference/preference.entity.js +77 -0
  97. package/dist/assets/server/preference/preference.entity.js.map +1 -0
  98. package/dist/assets/server/preference/preference.module.d.ts +2 -0
  99. package/dist/assets/server/preference/preference.module.js +26 -0
  100. package/dist/assets/server/preference/preference.module.js.map +1 -0
  101. package/dist/assets/server/preference/preference.service.d.ts +33 -0
  102. package/dist/assets/server/preference/preference.service.js +122 -0
  103. package/dist/assets/server/preference/preference.service.js.map +1 -0
  104. package/dist/assets/server/recording/recording-event-queue.service.js +1 -1
  105. package/dist/assets/server/recording/recording-event-queue.service.js.map +1 -1
  106. package/dist/assets/server/recording/recording-retention.service.d.ts +19 -2
  107. package/dist/assets/server/recording/recording-retention.service.js +95 -6
  108. package/dist/assets/server/recording/recording-retention.service.js.map +1 -1
  109. package/dist/assets/server/recording/recording-snapshot.service.d.ts +1 -0
  110. package/dist/assets/server/recording/recording-snapshot.service.js +4 -0
  111. package/dist/assets/server/recording/recording-snapshot.service.js.map +1 -1
  112. package/dist/assets/server/recording/recording.controller.d.ts +10 -0
  113. package/dist/assets/server/recording/recording.controller.js +19 -0
  114. package/dist/assets/server/recording/recording.controller.js.map +1 -1
  115. package/dist/assets/server/recording/recording.gateway.js +11 -0
  116. package/dist/assets/server/recording/recording.gateway.js.map +1 -1
  117. package/dist/assets/server/recording/recording.module.js +6 -1
  118. package/dist/assets/server/recording/recording.module.js.map +1 -1
  119. package/dist/assets/server/recording/recording.service.d.ts +5 -0
  120. package/dist/assets/server/recording/recording.service.js +10 -0
  121. package/dist/assets/server/recording/recording.service.js.map +1 -1
  122. package/dist/assets/server/remark/remark.controller.d.ts +1 -1
  123. package/dist/assets/server/remark/remark.controller.js +1 -10
  124. package/dist/assets/server/remark/remark.controller.js.map +1 -1
  125. package/dist/assets/server/remark/remark.entity.d.ts +1 -1
  126. package/dist/assets/server/remark/remark.entity.js.map +1 -1
  127. package/dist/assets/server/site/entities/site-registry.entity.d.ts +1 -1
  128. package/dist/assets/server/site/entities/site-registry.entity.js +1 -1
  129. package/dist/assets/server/site/entities/site-registry.entity.js.map +1 -1
  130. package/dist/assets/server/site/site.controller.d.ts +1 -1
  131. package/dist/assets/server/site/site.service.d.ts +5 -1
  132. package/dist/assets/server/site/site.service.js +13 -0
  133. package/dist/assets/server/site/site.service.js.map +1 -1
  134. package/dist/assets/server/system/system.controller.d.ts +1 -0
  135. package/dist/assets/server/system/system.controller.js +1 -1
  136. package/dist/assets/server/system/system.controller.js.map +1 -1
  137. package/dist/assets/server/system/system.service.d.ts +1 -0
  138. package/dist/assets/server/system/system.service.js +3 -2
  139. package/dist/assets/server/system/system.service.js.map +1 -1
  140. package/dist/cache/db.js +6 -6
  141. package/dist/commands/addons.js +2 -1
  142. package/dist/commands/cache.js +1 -1
  143. package/dist/commands/case.d.ts +67 -0
  144. package/dist/commands/case.js +101 -0
  145. package/dist/commands/config.d.ts +4 -0
  146. package/dist/commands/config.js +8 -1
  147. package/dist/commands/customer.d.ts +25 -0
  148. package/dist/commands/customer.js +137 -0
  149. package/dist/commands/doctor.js +33 -9
  150. package/dist/commands/extension.js +18 -15
  151. package/dist/commands/extras.d.ts +22 -3
  152. package/dist/commands/extras.js +114 -5
  153. package/dist/commands/init.d.ts +1 -1
  154. package/dist/commands/init.js +3 -4
  155. package/dist/commands/issue-env.d.ts +23 -0
  156. package/dist/commands/issue-env.js +101 -0
  157. package/dist/commands/job.d.ts +2 -1
  158. package/dist/commands/job.js +25 -5
  159. package/dist/commands/login.d.ts +4 -0
  160. package/dist/commands/login.js +30 -3
  161. package/dist/commands/migrate.d.ts +32 -0
  162. package/dist/commands/migrate.js +116 -0
  163. package/dist/commands/note.d.ts +3 -0
  164. package/dist/commands/note.js +75 -0
  165. package/dist/commands/ops.js +64 -13
  166. package/dist/commands/preference.d.ts +6 -0
  167. package/dist/commands/preference.js +80 -0
  168. package/dist/commands/record.js +1 -1
  169. package/dist/commands/report.js +1 -1
  170. package/dist/commands/search.js +1 -1
  171. package/dist/commands/server.d.ts +7 -0
  172. package/dist/commands/server.js +40 -2
  173. package/dist/commands/status.d.ts +4 -0
  174. package/dist/commands/status.js +16 -6
  175. package/dist/commands/update.js +7 -8
  176. package/dist/config.d.ts +21 -0
  177. package/dist/config.js +27 -0
  178. package/dist/i18n/locales/en-US.json +17 -1
  179. package/dist/i18n/locales/zh-CN.json +17 -1
  180. package/dist/index.js +14 -0
  181. package/dist/keychain.d.ts +16 -1
  182. package/dist/keychain.js +43 -16
  183. package/dist/logger.js +2 -2
  184. package/dist/updater.js +2 -2
  185. package/dist/utils/command-error.d.ts +4 -2
  186. package/dist/utils/command-error.js +19 -3
  187. package/dist/utils/link.d.ts +18 -0
  188. package/dist/utils/link.js +83 -0
  189. package/dist/version.js +33 -8
  190. package/dist/watchdog.js +3 -3
  191. package/dist/web/dist/404.html +1 -1
  192. package/dist/web/dist/_next/static/chunks/app/logs/page-d478031d49f7ba00.js +1 -0
  193. package/dist/web/dist/auth/verify.html +1 -1
  194. package/dist/web/dist/auth/verify.txt +1 -1
  195. package/dist/web/dist/companies.html +1 -1
  196. package/dist/web/dist/companies.txt +1 -1
  197. package/dist/web/dist/entity.html +1 -1
  198. package/dist/web/dist/entity.txt +1 -1
  199. package/dist/web/dist/index.html +1 -1
  200. package/dist/web/dist/index.txt +1 -1
  201. package/dist/web/dist/jobs.html +1 -1
  202. package/dist/web/dist/jobs.txt +1 -1
  203. package/dist/web/dist/logs.html +1 -1
  204. package/dist/web/dist/logs.txt +2 -2
  205. package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -1
  206. package/dist/web/dist/matching/jobs/placeholder/talents.txt +1 -1
  207. package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -1
  208. package/dist/web/dist/matching/portraits/placeholder/jobs.txt +1 -1
  209. package/dist/web/dist/parselog.html +1 -1
  210. package/dist/web/dist/parselog.txt +1 -1
  211. package/dist/web/dist/recordings.html +1 -1
  212. package/dist/web/dist/recordings.txt +1 -1
  213. package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -1
  214. package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +1 -1
  215. package/dist/web/dist/talents.html +1 -1
  216. package/dist/web/dist/talents.txt +1 -1
  217. package/package.json +2 -2
  218. package/dist/web/dist/_next/static/chunks/app/logs/page-9f41d44fa1add723.js +0 -1
  219. /package/dist/web/dist/_next/static/{hqJ-7vr4X71DNfzZugHJ- → WdJr3NhHjlu5xN0qOap1k}/_buildManifest.js +0 -0
  220. /package/dist/web/dist/_next/static/{hqJ-7vr4X71DNfzZugHJ- → WdJr3NhHjlu5xN0qOap1k}/_ssgManifest.js +0 -0
@@ -991,8 +991,27 @@ var control_runtime_source_default = '"use strict";\n(() => {\n // src/shared/c
991
991
  // src/background/cdp.ts
992
992
  var attached = /* @__PURE__ */ new Set();
993
993
  var CDP_COMMAND_TIMEOUT_MS = 15e3;
994
- function send(target, method, params) {
995
- return chrome.debugger.sendCommand(target, method, params ?? {});
994
+ function send(target, method, params, timeoutMs = CDP_COMMAND_TIMEOUT_MS) {
995
+ return new Promise((resolve, reject) => {
996
+ const timer = setTimeout(() => {
997
+ if (target.tabId !== void 0) {
998
+ chrome.debugger.detach({ tabId: target.tabId }).catch(() => void 0).then(() => {
999
+ attached.delete(target.tabId);
1000
+ });
1001
+ }
1002
+ reject(new Error(`cdp_timeout: ${method} after ${timeoutMs}ms (tab frozen or unresponsive)`));
1003
+ }, timeoutMs);
1004
+ chrome.debugger.sendCommand(target, method, params ?? {}).then(
1005
+ (v) => {
1006
+ clearTimeout(timer);
1007
+ resolve(v);
1008
+ },
1009
+ (err3) => {
1010
+ clearTimeout(timer);
1011
+ reject(err3);
1012
+ }
1013
+ );
1014
+ });
996
1015
  }
997
1016
  function isAttachableUrl(url) {
998
1017
  if (!url) return false;
@@ -1326,6 +1345,15 @@ chrome.debugger.onDetach.addListener((source) => {
1326
1345
  networkEnabledTabs.delete(source.tabId);
1327
1346
  }
1328
1347
  });
1348
+ async function detachIfAttached(tabId) {
1349
+ if (!attached.has(tabId)) return;
1350
+ try {
1351
+ await chrome.debugger.detach({ tabId });
1352
+ } catch {
1353
+ }
1354
+ attached.delete(tabId);
1355
+ networkEnabledTabs.delete(tabId);
1356
+ }
1329
1357
  var CDP_ALLOWLIST = /* @__PURE__ */ new Set([
1330
1358
  "Accessibility.enable",
1331
1359
  "Accessibility.getFullAXTree",
@@ -1362,10 +1390,11 @@ async function insertText(tabId, text) {
1362
1390
  }
1363
1391
  async function cdpEvaluate(tabId, expression, timeoutMs = CDP_COMMAND_TIMEOUT_MS) {
1364
1392
  await ensureAttached(tabId);
1365
- const result = await chrome.debugger.sendCommand(
1393
+ const result = await send(
1366
1394
  { tabId },
1367
1395
  "Runtime.evaluate",
1368
- { expression, returnByValue: true, awaitPromise: true }
1396
+ { expression, returnByValue: true, awaitPromise: true },
1397
+ timeoutMs
1369
1398
  );
1370
1399
  if (result.exceptionDetails) {
1371
1400
  const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
@@ -1897,6 +1926,19 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
1897
1926
  return err2(e instanceof Error ? e.message : String(e), code);
1898
1927
  }
1899
1928
  }
1929
+ if (command === "hm.liepin.search") {
1930
+ try {
1931
+ const result = await runLiepinSearch(controlArgs, log, {
1932
+ sessionId,
1933
+ groupTitle,
1934
+ explicitTab
1935
+ });
1936
+ return ok(result);
1937
+ } catch (e) {
1938
+ const code = e instanceof ControlBlockedError ? e.code : void 0;
1939
+ return err2(e instanceof Error ? e.message : String(e), code);
1940
+ }
1941
+ }
1900
1942
  let tab = await getControlTab(tabOverride);
1901
1943
  if (!tab?.id) {
1902
1944
  if (command === "cp.goto" && typeof controlArgs.url === "string" && controlArgs.url) {
@@ -2029,6 +2071,17 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
2029
2071
  return err2(e instanceof Error ? e.message : String(e));
2030
2072
  }
2031
2073
  }
2074
+ if (command === "hm.liepin.extract") {
2075
+ if (!isAttachableUrl(tab.url)) {
2076
+ return err2("liepin_extract_requires_http_page");
2077
+ }
2078
+ try {
2079
+ const result = await cdpEvaluate(tab.id, LIEPIN_EXTRACT_SOURCE);
2080
+ return ok(result);
2081
+ } catch (e) {
2082
+ return err2(e instanceof Error ? e.message : String(e));
2083
+ }
2084
+ }
2032
2085
  if (command.startsWith("hm.")) {
2033
2086
  return { status: "error", code: "not_implemented", message: `${command} is not implemented yet` };
2034
2087
  }
@@ -2316,6 +2369,8 @@ async function runLinkedInSearch(args, log, ctx) {
2316
2369
  const filters = args.filters && typeof args.filters === "object" ? args.filters : {};
2317
2370
  const maxPages = Math.min(Math.max(Number(args.maxPages) || 3, 1), 10);
2318
2371
  const keepTab = args.keepTab === true;
2372
+ const activateTab = args.activateTab !== false;
2373
+ const loadTimeoutMs = Math.min(Math.max(Number(args.loadTimeoutMs) || 25e3, 5e3), 3e5);
2319
2374
  const delayBase = typeof args.delayMs === "number" ? Number(args.delayMs) : 2500;
2320
2375
  let effectiveKeywords = keywords;
2321
2376
  if (!filters.geoUrn && typeof filters.location === "string" && filters.location.trim()) {
@@ -2330,7 +2385,12 @@ async function runLinkedInSearch(args, log, ctx) {
2330
2385
  if (typeof filters.company === "string" && filters.company) u.searchParams.set("company", filters.company);
2331
2386
  if (typeof filters.school === "string" && filters.school) u.searchParams.set("school", filters.school);
2332
2387
  const tab = await chrome.tabs.create({ url: u.toString(), active: false });
2333
- log("info", "hm.linkedin.search tab created", { tabId: tab.id, url: u.toString(), maxPages });
2388
+ log("info", "hm.linkedin.search tab created", { tabId: tab.id, url: u.toString(), maxPages, activateTab });
2389
+ if (activateTab && tab.id != null) {
2390
+ await chrome.tabs.update(tab.id, { active: true }).catch(
2391
+ (err3) => log("warn", "hm.linkedin.search activate tab failed", { tabId: tab.id, error: err3.message })
2392
+ );
2393
+ }
2334
2394
  if (ctx.sessionId && !ctx.explicitTab && tab.id) {
2335
2395
  addTabToSessionGroup(ctx.sessionId, ctx.groupTitle, tab).catch(
2336
2396
  (err3) => log("warn", "hm.linkedin.search addTabToSessionGroup failed", { error: err3.message })
@@ -2342,7 +2402,7 @@ async function runLinkedInSearch(args, log, ctx) {
2342
2402
  try {
2343
2403
  for (let page = 1; page <= maxPages; page++) {
2344
2404
  if (tab.id == null) break;
2345
- await waitTabLoaded(tab.id, 25e3);
2405
+ await waitTabLoaded(tab.id, loadTimeoutMs);
2346
2406
  const blocked = await cdpEvaluate(tab.id, LINKEDIN_WALL_CHECK_SOURCE).catch(() => null);
2347
2407
  if (blocked === "CAPTCHA" || blocked === "LOGIN_WALL") {
2348
2408
  log("warn", "hm.linkedin.search blocked by linkedin risk control", { blocked, page, url: lastUrl });
@@ -2374,8 +2434,7 @@ async function runLinkedInSearch(args, log, ctx) {
2374
2434
  }
2375
2435
  } finally {
2376
2436
  if (!keepTab && tab.id != null) {
2377
- await chrome.tabs.remove(tab.id).catch(() => {
2378
- });
2437
+ await closeTabGracefully(tab.id);
2379
2438
  }
2380
2439
  }
2381
2440
  return {
@@ -2388,6 +2447,529 @@ async function runLinkedInSearch(args, log, ctx) {
2388
2447
  tabId: keepTab ? tab.id ?? null : null
2389
2448
  };
2390
2449
  }
2450
+ var LIEPIN_WALL_CHECK_SOURCE = String.raw`(() => {
2451
+ try {
2452
+ if (location.href === 'about:blank' || !location.hostname) return 'BLANK_KILL';
2453
+ const url = location.href;
2454
+ if (/liepin\.com\/(login|passport|signin)/i.test(url)) return 'LOGIN_WALL';
2455
+ if (/liepin\.com\/(safe|verify|check|captcha|slide)/i.test(url)) return 'CAPTCHA';
2456
+ const bodyText = (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 4000);
2457
+ if (/滑块|拖动验证|安全验证|验证码|verify you are human|请完成安全验证/.test(bodyText)) return 'CAPTCHA';
2458
+ if (document.querySelector('[class*="captcha"], [id*="captcha"], iframe[src*="captcha"], iframe[src*="slide"]')) return 'CAPTCHA';
2459
+ if (/请先登录|登录后查看|立即登录/.test(bodyText) && !document.querySelector('#resume-detail-basic-info')) return 'LOGIN_WALL';
2460
+ } catch (e) { /* 检测失败不阻断 */ }
2461
+ return null;
2462
+ })()`;
2463
+ var LIEPIN_EXTRACT_SOURCE = String.raw`(async () => {
2464
+ const waitFor = (fn, timeout, interval) => new Promise((resolve) => {
2465
+ const t0 = Date.now();
2466
+ const tick = () => {
2467
+ let v = null;
2468
+ try { v = fn(); } catch (e) {}
2469
+ if (v) return resolve(v);
2470
+ if (Date.now() - t0 > (timeout || 8000)) return resolve(null);
2471
+ setTimeout(tick, interval || 250);
2472
+ };
2473
+ tick();
2474
+ });
2475
+ if (document.readyState !== 'complete') {
2476
+ await waitFor(() => document.readyState === 'complete', 12000, 200);
2477
+ }
2478
+ const clean = (s) => {
2479
+ if (s == null) return null;
2480
+ const t = String(s).replace(/\s+/g, ' ').trim();
2481
+ return t || null;
2482
+ };
2483
+ const txt = (el) => clean(el ? el.textContent : null);
2484
+ const first = (sel, root) => (root || document).querySelector(sel);
2485
+ const attr = (el, name) => (el ? (el.getAttribute(name) || null) : null);
2486
+ const abs = (href) => {
2487
+ if (!href) return null;
2488
+ try { return new URL(href, location.href).toString(); } catch (e) { return href; }
2489
+ };
2490
+ /* sep-info 用 <i></i> 作分隔符:转 | 后按段切分 */
2491
+ const splitByI = (el) => {
2492
+ if (!el) return [];
2493
+ const html = el.innerHTML || '';
2494
+ const holder = document.createElement('div');
2495
+ return html.replace(/<i[^>]*>\s*<\/i>/g, '|').split('|').map((seg) => {
2496
+ holder.innerHTML = seg;
2497
+ return clean(holder.textContent);
2498
+ }).filter(Boolean);
2499
+ };
2500
+ /* (2012.07 - 至今, 14年2个月) → { start, end, duration } */
2501
+ const parsePeriod = (text) => {
2502
+ const m = (text || '').match(/[((]\s*([^))]*?)\s*[))]/);
2503
+ if (!m) return { start: null, end: null, duration: null };
2504
+ const parts = m[1].split(',').map((s) => clean(s)).filter(Boolean);
2505
+ const range = (parts[0] || '').split('-').map((s) => clean(s));
2506
+ return { start: range[0] || null, end: range[1] || null, duration: parts[1] || null };
2507
+ };
2508
+ /* rd-info-col 映射:每个 col 是 title+cont 对(一行可能两个 col) */
2509
+ const readInfoCols = (root) => {
2510
+ const out = {};
2511
+ (root || document).querySelectorAll('.rd-info-col').forEach((col) => {
2512
+ const title = txt(first('.rd-info-col-title', col));
2513
+ const cont = txt(first('.rd-info-col-cont', col));
2514
+ if (title && cont) out[title.replace(/[::]\s*$/, '')] = cont;
2515
+ });
2516
+ return out;
2517
+ };
2518
+ const resIdFrom = (href, scm) => {
2519
+ if (scm) {
2520
+ const m = scm.match(/cid=([^&]+)/);
2521
+ if (m) return m[1];
2522
+ }
2523
+ if (href) {
2524
+ const m = href.match(/res_id_encode=([^&]+)/);
2525
+ if (m) return m[1];
2526
+ }
2527
+ return null;
2528
+ };
2529
+
2530
+ const url = location.href;
2531
+ const canonical = (document.querySelector('link[rel="canonical"]') || {}).href || url.split('?')[0];
2532
+ const entities = [];
2533
+ const links = { talent: [], company: [], job: [] };
2534
+
2535
+ if (document.querySelector('#resume-detail-basic-info')) {
2536
+ const basic = first('#resume-detail-basic-info');
2537
+ const sepInfos = basic.querySelectorAll('.basic-cont .sep-info');
2538
+ const parts0 = sepInfos.length > 0 ? splitByI(sepInfos[0]) : [];
2539
+ const parts1 = sepInfos.length > 1 ? splitByI(sepInfos[1]) : [];
2540
+ const joined0 = parts0.join(' ');
2541
+ const age = joined0.match(/(\d+)\s*岁/);
2542
+ const workYears = joined0.match(/工作\s*(\d+)\s*年/);
2543
+ const name = txt(first('.name-box .name', basic));
2544
+ const statusBox = first('.res-status-box', basic);
2545
+ const contactTimeRaw = txt(statusBox);
2546
+ const scm = attr(document.querySelector('[data-tlg-scm]'), 'data-tlg-scm');
2547
+
2548
+ const intentRoot = first('#resume-detail-job-exp-info');
2549
+ const workExperiences = [];
2550
+ document.querySelectorAll('#resume-detail-work-info .rd-work-item-cont').forEach((item) => {
2551
+ const cols = readInfoCols(item);
2552
+ workExperiences.push({
2553
+ company: attr(first('.rd-work-comp h5', item), 'title') || txt(first('.rd-work-comp h5', item)),
2554
+ ...parsePeriod(txt(first('.rd-work-time', item))),
2555
+ position: txt(first('.job-name', item)),
2556
+ department: cols['所在部门'] || null,
2557
+ location: cols['工作地点'] || null,
2558
+ subordinates: cols['下属人数'] != null ? Number(cols['下属人数']) : null,
2559
+ reportsTo: cols['汇报对象'] || null,
2560
+ responsibilities: cols['职责业绩'] || null,
2561
+ });
2562
+ });
2563
+ const projects = [];
2564
+ document.querySelectorAll('#resume-detail-project-info .rd-project-item-cont').forEach((item) => {
2565
+ const cols = readInfoCols(item);
2566
+ projects.push({
2567
+ name: attr(first('.rd-project-name h5', item), 'title') || txt(first('.rd-project-name h5', item)),
2568
+ ...parsePeriod(txt(first('.rd-project-time', item))),
2569
+ role: cols['项目职务'] || null,
2570
+ company: cols['所在公司'] || null,
2571
+ description: cols['项目描述'] || null,
2572
+ duties: cols['项目职责'] || null,
2573
+ });
2574
+ });
2575
+ const education = [];
2576
+ document.querySelectorAll('#resume-detail-edu-info .rd-edu-info-item').forEach((item) => {
2577
+ education.push({
2578
+ school: attr(first('.school-name', item), 'title') || txt(first('.school-name', item)),
2579
+ major: attr(first('.school-special', item), 'title') || txt(first('.school-special', item)),
2580
+ degree: attr(first('.school-degree', item), 'title') || txt(first('.school-degree', item)),
2581
+ period: txt(first('.school-time', item)),
2582
+ tags: Array.from(item.querySelectorAll('.edu-school-tags .tag-item')).map((t) => txt(t)).filter(Boolean),
2583
+ });
2584
+ });
2585
+
2586
+ entities.push({
2587
+ entityType: 'talent',
2588
+ platform: 'liepin',
2589
+ resId: resIdFrom(url, scm),
2590
+ name,
2591
+ nameMasked: name ? /\*/.test(name) : undefined,
2592
+ statusTag: txt(first('.user-status-tag', basic)),
2593
+ gender: parts0.find((p) => /^(男|女)$/.test(p)) || null,
2594
+ age: age ? Number(age[1]) : null,
2595
+ city: parts0.find((p) => /^(北京|上海|广州|深圳|天津|重庆|杭州|南京|苏州|成都|武汉|西安|[一-龥]{2,6})$/.test(p) && !/^(男|女|保密)$/.test(p) && !/岁|年/.test(p)) || null,
2596
+ degree: parts0.find((p) => /(博士|硕士|MBA|本科|大专|专科|高中|中专|初中)/.test(p)) || null,
2597
+ workYears: workYears ? Number(workYears[1]) : null,
2598
+ currentCompany: parts0.length ? parts0[parts0.length - 1] : null,
2599
+ currentCompanyMasked: parts0.includes('保密') ? true : undefined,
2600
+ currentPosition: parts1.length ? parts1[0] : null,
2601
+ contactTime: contactTimeRaw ? contactTimeRaw.replace(/^方便联系时间[::]\s*/, '') : null,
2602
+ photoUrl: (() => { const img = first('.logo-box img', basic); return img ? abs(img.getAttribute('src')) : null; })(),
2603
+ intent: intentRoot ? {
2604
+ position: attr(first('.tabs .title', intentRoot), 'title') || txt(first('.tabs .title', intentRoot)),
2605
+ salary: txt(first('.tabs .salary', intentRoot)),
2606
+ city: txt(first('.dqname-wrap', intentRoot)),
2607
+ industries: Array.from(intentRoot.querySelectorAll('.lebels-wrap span')).map((s) => txt(s)).filter(Boolean),
2608
+ } : null,
2609
+ workExperiences,
2610
+ projects,
2611
+ education,
2612
+ languages: Array.from(document.querySelectorAll('#resume-detail-lang-info .rd-lang-item .lang-name')).map((el) => txt(el)).filter(Boolean),
2613
+ selfEvaluation: txt(first('#resume-detail-self-eva-info .resume-detail-template-cont')),
2614
+ statement: txt(first('.c-detail-statement')),
2615
+ hasAntiScrapeWatermark: document.querySelectorAll('.cozue').length > 0,
2616
+ url: canonical,
2617
+ source: 'liepin',
2618
+ });
2619
+ } else if (document.querySelector('.talent-resume-card')) {
2620
+ /* 人才管理页/搜索结果列表:.talent-resume-card 卡片(事实源 §7) */
2621
+ document.querySelectorAll('.talent-resume-card').forEach((card) => {
2622
+ const a = first('a.resume-title', card) || first('a[href*="showresumedetail"]', card);
2623
+ const href = a ? abs(attr(a, 'href')) : null;
2624
+ const scm = attr(card, 'data-tlg-scm') || '';
2625
+ const titleLine = attr(a, 'title') || txt(a);
2626
+ const eduItem = Array.from(card.querySelectorAll('.info-item')).find((el) => /最高学历/.test(el.textContent || ''));
2627
+ const workLines = [];
2628
+ card.querySelectorAll('.work-info .info-cont p, .info-cont p').forEach((p) => {
2629
+ const comp = first('.comp', p);
2630
+ const pos = first('.rw-title', p);
2631
+ if (comp || pos) {
2632
+ workLines.push({
2633
+ period: txt(p.querySelector('i')),
2634
+ company: attr(comp, 'title') || txt(comp),
2635
+ position: attr(pos, 'title') || txt(pos),
2636
+ });
2637
+ }
2638
+ });
2639
+ const acquired = txt(first('.right-extension .temp-side', card));
2640
+ const lead = {
2641
+ url: href,
2642
+ name: titleLine ? (titleLine.split(/\s*[||]\s*/)[0] || titleLine) : null,
2643
+ titleLine,
2644
+ resId: resIdFrom(href, scm),
2645
+ education: eduItem ? clean((eduItem.textContent || '').replace(/^最高学历[::]?/, '')) : null,
2646
+ recentWork: workLines,
2647
+ acquiredAt: acquired ? acquired.replace(/^获取时间[::]?/, '') : null,
2648
+ source: 'liepin',
2649
+ };
2650
+ if (lead.url && !links.talent.some((l) => l.url === lead.url)) links.talent.push(lead);
2651
+ });
2652
+ } else {
2653
+ /* 兜底:任何含简历详情链接的页面,提取链接名单 */
2654
+ document.querySelectorAll('a[href*="showresumedetail"]').forEach((a) => {
2655
+ const href = abs(attr(a, 'href'));
2656
+ if (href && !links.talent.some((l) => l.url === href)) {
2657
+ links.talent.push({ url: href, name: txt(a), resId: resIdFrom(href, ''), source: 'liepin' });
2658
+ }
2659
+ });
2660
+ }
2661
+
2662
+ const pageClass = document.querySelector('#resume-detail-basic-info')
2663
+ ? 'talent'
2664
+ : (document.querySelector('.talent-resume-card') ? 'resume-list' : (links.talent.length ? 'list' : 'unknown'));
2665
+ return {
2666
+ url,
2667
+ canonical,
2668
+ pageClass,
2669
+ title: clean(document.title),
2670
+ extractedAt: new Date().toISOString(),
2671
+ entities,
2672
+ links,
2673
+ };
2674
+ })()`;
2675
+ var LIEPIN_FILL_AND_SEARCH_SOURCE = String.raw`(async (P) => {
2676
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2677
+ const clean = (s) => (s == null ? null : String(s).replace(/\s+/g, ' ').trim() || null);
2678
+ const delay = P.fieldDelayMs && P.fieldDelayMs >= 0 ? P.fieldDelayMs : 1500;
2679
+ const applied = [];
2680
+ const errors = [];
2681
+
2682
+ /* React 受控输入兼容:native setter + input/change 事件 */
2683
+ const setNativeValue = (el, value) => {
2684
+ const setter = Object.getOwnPropertyDescriptor(
2685
+ el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
2686
+ 'value',
2687
+ ).set;
2688
+ setter.call(el, value);
2689
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2690
+ el.dispatchEvent(new Event('change', { bubbles: true }));
2691
+ };
2692
+ const setInput = (sel, value) => {
2693
+ const el = document.querySelector(sel);
2694
+ if (!el) { errors.push('missing_input:' + sel); return false; }
2695
+ if (el.disabled) { errors.push('disabled_input:' + sel); return false; }
2696
+ el.focus();
2697
+ setNativeValue(el, String(value));
2698
+ el.blur();
2699
+ return true;
2700
+ };
2701
+ /* 在某个容器内点击包含指定文本的按钮(如"确 定"/"搜 索") */
2702
+ const clickButtonIn = (rootSel, text) => {
2703
+ const root = rootSel ? document.querySelector(rootSel) : document;
2704
+ if (!root) { errors.push('missing_root:' + rootSel); return false; }
2705
+ const btn = Array.from(root.querySelectorAll('button, span, a')).find((b) => {
2706
+ const t = clean(b.textContent);
2707
+ return t && t.replace(/\s+/g, '') === text;
2708
+ });
2709
+ if (!btn) { errors.push('missing_button:' + text + '@' + rootSel); return false; }
2710
+ btn.click();
2711
+ return true;
2712
+ };
2713
+ /* 点击条件 tag(容器可带 #N 出现序号:如 '.sfilter-city#2' = 第 2 个 sfilter-city,即期望城市) */
2714
+ const clickTag = (containerSel, text) => {
2715
+ let sel = containerSel;
2716
+ let idx = 0;
2717
+ const m = String(containerSel).match(/^(.*)#(\d+)$/);
2718
+ if (m) { sel = m[1]; idx = parseInt(m[2], 10) - 1; }
2719
+ const containers = document.querySelectorAll(sel);
2720
+ const container = containers.length > idx ? containers[idx] : null;
2721
+ if (!container) { errors.push('missing_container:' + containerSel); return false; }
2722
+ const tag = Array.from(container.querySelectorAll('label.tag-item, .tag-item, span')).find((el) => {
2723
+ const t = clean(el.textContent);
2724
+ return t && (t === text || t.replace(/\s+/g, '') === String(text).replace(/\s+/g, ''));
2725
+ });
2726
+ if (!tag) { errors.push('missing_tag:' + text + '@' + containerSel); return false; }
2727
+ tag.click();
2728
+ return true;
2729
+ };
2730
+
2731
+ try {
2732
+ /* 1) 关键词:顶部自动补全输入框(与 .search-btn 同容器,placeholder 搜职位/公司/行业等) */
2733
+ if (P.keyword) {
2734
+ const okInput = (() => {
2735
+ const box = document.querySelector('.auto-input-wrap-v3') || document.querySelector('.ant-select-auto-complete');
2736
+ const el = box ? box.querySelector('input.ant-select-selection-search-input') : null;
2737
+ if (!el) { errors.push('missing_input:keyword'); return false; }
2738
+ el.focus();
2739
+ setNativeValue(el, String(P.keyword));
2740
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2741
+ el.dispatchEvent(new Event('change', { bubbles: true }));
2742
+ el.blur();
2743
+ return true;
2744
+ })();
2745
+ if (okInput) applied.push('keyword');
2746
+ await sleep(delay);
2747
+ }
2748
+
2749
+ /* 2) tag 类条件(每次点击后 human-paced) */
2750
+ const tagSteps = [];
2751
+ if (P.city) tagSteps.push(['.sfilter-city', P.city, 'city']);
2752
+ if (P.expectCity) tagSteps.push(['.sfilter-city#2', P.expectCity, 'expectCity']);
2753
+ if (P.workYear) tagSteps.push(['.sfilter-work-year', P.workYear, 'workYear']);
2754
+ if (P.education) tagSteps.push(['.sfilter-edu', P.education, 'education']);
2755
+ if (P.schoolTag) tagSteps.push(['.sfilter-additional', P.schoolTag, 'schoolTag']);
2756
+ if (P.language) tagSteps.push(['.sfilter-lang', P.language, 'language']);
2757
+ for (const [sel, text, key] of tagSteps) {
2758
+ if (clickTag(sel, text)) applied.push(key);
2759
+ await sleep(delay);
2760
+ }
2761
+
2762
+ /* 3) 区间输入类条件:填输入 + 点容器内"确定" */
2763
+ const rangeSteps = [];
2764
+ if (P.ageMin != null || P.ageMax != null) {
2765
+ rangeSteps.push(['#ageLow', P.ageMin, '#ageHigh', P.ageMax, '.age-shadow-box', 'age']);
2766
+ }
2767
+ if (P.wantSalaryMin != null || P.wantSalaryMax != null) {
2768
+ rangeSteps.push(['#wantSalaryLow', P.wantSalaryMin, '#wantSalaryHigh', P.wantSalaryMax, '.sfilter-salary .search-resume-shadow-box', 'wantSalary']);
2769
+ }
2770
+ if (P.nowSalaryMin != null || P.nowSalaryMax != null) {
2771
+ rangeSteps.push(['#nowSalaryLow', P.nowSalaryMin, '#nowSalaryHigh', P.nowSalaryMax, null, 'nowSalary']);
2772
+ }
2773
+ for (const [loSel, lo, hiSel, hi, boxSel, key] of rangeSteps) {
2774
+ let ok = true;
2775
+ if (lo != null) ok = setInput(loSel, lo) && ok;
2776
+ await sleep(Math.max(600, Math.floor(delay / 2)));
2777
+ if (hi != null) ok = setInput(hiSel, hi) && ok;
2778
+ await sleep(Math.max(600, Math.floor(delay / 2)));
2779
+ /* 确定按钮:优先容器内,其次全局匹配(目前年薪容器无专属 class) */
2780
+ if (ok && !clickButtonIn(boxSel, '确定')) {
2781
+ ok = clickButtonIn(null, '确定');
2782
+ }
2783
+ if (ok) applied.push(key);
2784
+ await sleep(delay);
2785
+ }
2786
+
2787
+ /* 4) 工作年限自定义:先点"自定义"tag 解锁输入,再填 + 确定 */
2788
+ if (P.workYearsMin != null || P.workYearsMax != null) {
2789
+ if (clickTag('.sfilter-work-year', '自定义')) {
2790
+ applied.push('workYearCustom');
2791
+ await sleep(delay);
2792
+ let ok = true;
2793
+ if (P.workYearsMin != null) ok = setInput('#workYearsLow', P.workYearsMin) && ok;
2794
+ await sleep(Math.max(600, Math.floor(delay / 2)));
2795
+ if (P.workYearsMax != null) ok = setInput('#workYearsHigh', P.workYearsMax) && ok;
2796
+ await sleep(Math.max(600, Math.floor(delay / 2)));
2797
+ if (ok && clickButtonIn('.year-custom-box', '确定')) applied.push('workYears');
2798
+ }
2799
+ await sleep(delay);
2800
+ }
2801
+
2802
+ /* 5) 复选框:海外工作经验 / 管理经验 */
2803
+ for (const [name, key] of [['workAbroad', 'workAbroad'], ['managerial', 'managerial']]) {
2804
+ if (P[key]) {
2805
+ const input = document.querySelector('input[name="' + name + '"]');
2806
+ if (input && !input.checked) {
2807
+ const wrapper = input.closest('label') || input;
2808
+ wrapper.click();
2809
+ applied.push(key);
2810
+ await sleep(delay);
2811
+ } else if (!input) {
2812
+ errors.push('missing_checkbox:' + name);
2813
+ }
2814
+ }
2815
+ }
2816
+
2817
+ /* 6) 提交搜索前停顿(类人工审阅),点击顶部"搜 索" */
2818
+ await sleep(Math.max(1500, delay));
2819
+ const searchBtn = document.querySelector('button.search-btn');
2820
+ if (!searchBtn) { errors.push('missing_button:search'); return { applied, errors, submitted: false }; }
2821
+ searchBtn.click();
2822
+ applied.push('submit');
2823
+ return { applied, errors, submitted: true };
2824
+ } catch (e) {
2825
+ errors.push('fill_exception:' + String((e && e.message) || e));
2826
+ return { applied, errors, submitted: false };
2827
+ }
2828
+ })`;
2829
+ var LIEPIN_RESULT_WAIT_SOURCE = String.raw`(() => {
2830
+ return new Promise((resolve) => {
2831
+ const t0 = Date.now();
2832
+ const tick = () => {
2833
+ let hit = false;
2834
+ try {
2835
+ hit = !!(document.querySelector('.talent-resume-card') || document.querySelector('a[href*="showresumedetail"]'));
2836
+ if (!hit && /没有找到|暂无相关|0\s*条结果|共\s*0\s*条/.test(document.body ? document.body.innerText : '')) return resolve('empty');
2837
+ } catch (e) {}
2838
+ if (hit) return resolve(true);
2839
+ if (Date.now() - t0 > 20000) return resolve(false);
2840
+ setTimeout(tick, 400);
2841
+ };
2842
+ tick();
2843
+ });
2844
+ })()`;
2845
+ var LIEPIN_NEXT_CLICK_SOURCE = String.raw`(async () => {
2846
+ const next = document.querySelector('.ant-pagination-next button')
2847
+ || document.querySelector('.ant-pagination-next')
2848
+ || document.querySelector('button[aria-label="下一页"]');
2849
+ if (!next || next.getAttribute('aria-disabled') === 'true' || next.disabled) return false;
2850
+ next.click();
2851
+ return true;
2852
+ })()`;
2853
+ function liepinBlockedCode(blocked) {
2854
+ if (blocked === "LOGIN_WALL") return "LIE_PIN_LOGIN_WALL";
2855
+ if (blocked === "BLANK_KILL") return "LIE_PIN_BLANK_KILL";
2856
+ return "LIE_PIN_CAPTCHA";
2857
+ }
2858
+ async function runLiepinSearch(args, log, ctx) {
2859
+ const keyword = typeof args.keyword === "string" ? args.keyword.trim() : "";
2860
+ if (!keyword) throw new Error("liepin_search_requires_keyword");
2861
+ const maxPages = Math.min(Math.max(Number(args.maxPages) || 1, 1), 5);
2862
+ const keepTab = args.keepTab === true;
2863
+ const fieldDelayMs = typeof args.delayMs === "number" ? Number(args.delayMs) : 1500;
2864
+ const fillParams = { keyword, fieldDelayMs };
2865
+ for (const k of [
2866
+ "city",
2867
+ "expectCity",
2868
+ "workYear",
2869
+ "education",
2870
+ "schoolTag",
2871
+ "language",
2872
+ "ageMin",
2873
+ "ageMax",
2874
+ "wantSalaryMin",
2875
+ "wantSalaryMax",
2876
+ "nowSalaryMin",
2877
+ "nowSalaryMax",
2878
+ "workYearsMin",
2879
+ "workYearsMax",
2880
+ "workAbroad",
2881
+ "managerial"
2882
+ ]) {
2883
+ if (args[k] !== void 0) fillParams[k] = args[k];
2884
+ }
2885
+ const searchUrl = "https://h.liepin.com/search/getConditionItem";
2886
+ const tab = await chrome.tabs.create({ url: searchUrl, active: false });
2887
+ log("info", "hm.liepin.search tab created", { tabId: tab.id, url: searchUrl, maxPages, keyword });
2888
+ if (ctx.sessionId && !ctx.explicitTab && tab.id) {
2889
+ addTabToSessionGroup(ctx.sessionId, ctx.groupTitle, tab).catch(
2890
+ (err3) => log("warn", "hm.liepin.search addTabToSessionGroup failed", { error: err3.message })
2891
+ );
2892
+ }
2893
+ const leads = /* @__PURE__ */ new Map();
2894
+ const applied = [];
2895
+ const fillErrors = [];
2896
+ let pagesScanned = 0;
2897
+ const assertNotBlocked = async (phase) => {
2898
+ if (tab.id == null) return;
2899
+ const t = await chrome.tabs.get(tab.id).catch(() => null);
2900
+ if (!t) throw new Error(`tab closed while ${phase}`);
2901
+ if (!t.url || t.url === "about:blank") {
2902
+ throw new ControlBlockedError("LIE_PIN_BLANK_KILL", `liepin search aborted at ${phase}: page killed to about:blank (secscan)`);
2903
+ }
2904
+ const blocked = await cdpEvaluate(tab.id, LIEPIN_WALL_CHECK_SOURCE).catch(() => null);
2905
+ if (blocked) {
2906
+ const code = liepinBlockedCode(blocked);
2907
+ log("warn", "hm.liepin.search blocked by liepin risk control", { blocked, code, phase });
2908
+ throw new ControlBlockedError(code, `liepin search aborted at ${phase}: ${blocked}`);
2909
+ }
2910
+ };
2911
+ try {
2912
+ if (tab.id == null) throw new Error("failed_to_create_tab");
2913
+ await waitTabLoaded(tab.id, 3e4);
2914
+ await sleep(1500);
2915
+ await assertNotBlocked("after-load");
2916
+ const fillExpr = "(() => { const P = " + JSON.stringify(fillParams) + "; return (" + LIEPIN_FILL_AND_SEARCH_SOURCE + ")(P); })()";
2917
+ const fillResult = await cdpEvaluate(tab.id, fillExpr, 6e4);
2918
+ applied.push(...fillResult?.applied ?? []);
2919
+ fillErrors.push(...fillResult?.errors ?? []);
2920
+ if (!fillResult?.submitted) {
2921
+ log("warn", "hm.liepin.search fill did not submit", { fillErrors });
2922
+ throw new Error("liepin_search_fill_failed: " + fillErrors.join(";"));
2923
+ }
2924
+ log("info", "hm.liepin.search form submitted", { applied, fillErrors });
2925
+ for (let page = 1; page <= maxPages; page++) {
2926
+ if (tab.id == null) break;
2927
+ const waited = await cdpEvaluate(tab.id, LIEPIN_RESULT_WAIT_SOURCE, 3e4).catch(() => false);
2928
+ await sleep(1200);
2929
+ await assertNotBlocked(`page-${page}`);
2930
+ if (waited === "empty") {
2931
+ log("info", "hm.liepin.search empty result", { page });
2932
+ break;
2933
+ }
2934
+ const extracted = await cdpEvaluate(tab.id, LIEPIN_EXTRACT_SOURCE, 3e4);
2935
+ const pageLeads = extracted?.links?.talent || [];
2936
+ for (const l of pageLeads) {
2937
+ if (l.url && !leads.has(l.url)) {
2938
+ leads.set(l.url, {
2939
+ url: l.url,
2940
+ name: l.name ?? null,
2941
+ titleLine: l.titleLine ?? null,
2942
+ resId: l.resId ?? null,
2943
+ page,
2944
+ source: "liepin"
2945
+ });
2946
+ }
2947
+ }
2948
+ pagesScanned = page;
2949
+ log("info", "hm.liepin.search page extracted", { page, pageLeads: pageLeads.length, total: leads.size });
2950
+ if (page >= maxPages) break;
2951
+ const clicked = await cdpEvaluate(tab.id, LIEPIN_NEXT_CLICK_SOURCE).catch(() => false);
2952
+ if (!clicked) break;
2953
+ await sleep(3e3 + Math.random() * 2e3);
2954
+ await assertNotBlocked(`page-${page}-next`);
2955
+ }
2956
+ } finally {
2957
+ if (!keepTab && tab.id != null) {
2958
+ await closeTabGracefully(tab.id);
2959
+ }
2960
+ }
2961
+ return {
2962
+ keyword,
2963
+ searchUrl,
2964
+ pagesScanned,
2965
+ leadCount: leads.size,
2966
+ leads: Array.from(leads.values()),
2967
+ applied,
2968
+ fillErrors,
2969
+ keepTab,
2970
+ tabId: keepTab ? tab.id ?? null : null
2971
+ };
2972
+ }
2391
2973
  var HIREDCHINA_EXTRACT_SOURCE = String.raw`(async () => {
2392
2974
  const waitFor = (fn, timeout, interval) => new Promise((resolve) => {
2393
2975
  const t0 = Date.now();
@@ -2611,6 +3193,14 @@ async function handleCookiesCommand(args) {
2611
3193
  }
2612
3194
  throw new Error(`cookies_unknown_action: ${action}`);
2613
3195
  }
3196
+ async function closeTabGracefully(tabId) {
3197
+ await detachIfAttached(tabId);
3198
+ await Promise.race([
3199
+ chrome.tabs.remove(tabId),
3200
+ new Promise((_, reject) => setTimeout(() => reject(new Error("tab_remove_timeout")), 5e3))
3201
+ ]).catch(() => {
3202
+ });
3203
+ }
2614
3204
  async function handleTabCommand(command, args, log) {
2615
3205
  const tabArg = args.tab ?? args.targetId ?? args.page;
2616
3206
  switch (command) {
@@ -2642,7 +3232,7 @@ async function handleTabCommand(command, args, log) {
2642
3232
  }
2643
3233
  case "cp.tab.close": {
2644
3234
  if (!tabArg) throw new Error("tab_close_requires_tab_id");
2645
- await chrome.tabs.remove(Number(tabArg));
3235
+ await closeTabGracefully(Number(tabArg));
2646
3236
  return { closed: true, page: String(tabArg) };
2647
3237
  }
2648
3238
  case "cp.tab.bind": {
@@ -2825,7 +3415,7 @@ function maybeDiscover(url, title) {
2825
3415
  });
2826
3416
  }
2827
3417
 
2828
- // ../../node_modules/.pnpm/@chromepilot+logger-kit@0.1.3_@nestjs+common@10.4.22_class-transformer@0.5.1_class-validator@_v44hq6ikmqzoani6tawke4nway/node_modules/@chromepilot/logger-kit/dist/client/logger.js
3418
+ // ../../node_modules/.pnpm/@chromepilot+logger-kit@0.1.4_@nestjs+common@10.4.22_class-transformer@0.5.1_class-validator@_legcruqwso5xb42h6lrzoll4b4/node_modules/@chromepilot/logger-kit/dist/client/logger.js
2829
3419
  function stringifyArg(arg) {
2830
3420
  if (typeof arg === "string")
2831
3421
  return arg;
@@ -2977,7 +3567,7 @@ var Logger = class {
2977
3567
  }
2978
3568
  };
2979
3569
 
2980
- // ../../node_modules/.pnpm/@chromepilot+logger-kit@0.1.3_@nestjs+common@10.4.22_class-transformer@0.5.1_class-validator@_v44hq6ikmqzoani6tawke4nway/node_modules/@chromepilot/logger-kit/dist/client/transports/http-transport.js
3570
+ // ../../node_modules/.pnpm/@chromepilot+logger-kit@0.1.4_@nestjs+common@10.4.22_class-transformer@0.5.1_class-validator@_legcruqwso5xb42h6lrzoll4b4/node_modules/@chromepilot/logger-kit/dist/client/transports/http-transport.js
2981
3571
  async function createHttpTransport(opts) {
2982
3572
  const serverUrl = opts.serverUrl.replace(/\/$/, "");
2983
3573
  return async (logs) => {
@@ -3164,36 +3754,58 @@ createBackgroundLogForwarder({
3164
3754
  }).then((f) => {
3165
3755
  logForwarder = f;
3166
3756
  });
3167
- var socket = null;
3168
- var SEND_QUEUE_LIMIT = 500;
3169
- var sendQueue = [];
3170
- var queuedDropped = 0;
3171
- function isFullSnapshot(payload) {
3172
- return payload?.type === "rrweb" && payload?.payload?.type === 2;
3173
- }
3174
- function enqueueRecordingEvent(payload) {
3175
- sendQueue.push({ raw: JSON.stringify(payload), isSnapshot: isFullSnapshot(payload) });
3176
- while (sendQueue.length > SEND_QUEUE_LIMIT) {
3177
- const idx = sendQueue.findIndex((item) => !item.isSnapshot);
3178
- if (idx === -1) break;
3179
- sendQueue.splice(idx, 1);
3180
- queuedDropped++;
3181
- }
3182
- }
3183
- function flushSendQueue() {
3184
- if (!socket || socket.readyState !== WebSocket.OPEN || !sendQueue.length) return;
3185
- while (sendQueue.length) {
3186
- const item = sendQueue.shift();
3757
+ var OFFSCREEN_URL = "offscreen/offscreen.html";
3758
+ function sendToOffscreen(raw) {
3759
+ chrome.runtime.sendMessage({ type: "offscreen:send", raw }).catch(() => {
3760
+ });
3761
+ }
3762
+ function sendInitToOffscreen() {
3763
+ const manifest = chrome.runtime.getManifest();
3764
+ chrome.runtime.sendMessage({ type: "offscreen:init", extensionId: chrome.runtime.id, version: manifest.version }).catch(() => {
3765
+ });
3766
+ }
3767
+ async function ensureOffscreen() {
3768
+ try {
3769
+ const contexts = await chrome.runtime.getContexts({
3770
+ contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT]
3771
+ });
3772
+ if (contexts.length > 0) {
3773
+ sendInitToOffscreen();
3774
+ return;
3775
+ }
3187
3776
  try {
3188
- socket.send(item.raw);
3189
- } catch (e) {
3190
- console.error("[HunterMate] flush send failed", e);
3191
- break;
3777
+ await chrome.offscreen.createDocument({
3778
+ url: OFFSCREEN_URL,
3779
+ // D1: WEBSOCKET 枚举从未在正式 Chrome 落地(152 实测抛枚举非法),必须用 WORKERS;
3780
+ // @types/chrome 0.0.268 的 Reason 枚举同样没有 WEBSOCKET 成员,断言方向一致
3781
+ reasons: ["WORKERS"],
3782
+ justification: "hunter-mate \u9700\u4E0E\u672C\u5730 server \u4FDD\u6301\u5E38\u9A7B WebSocket \u8FDE\u63A5(MV3 SW \u4F1A\u88AB\u7A7A\u95F2\u56DE\u6536)"
3783
+ });
3784
+ sendInitToOffscreen();
3785
+ } catch (createErr) {
3786
+ const msg = createErr instanceof Error ? createErr.message : String(createErr);
3787
+ sendLog("warn", "ensureOffscreen createDocument failed, retry after closeDocument", { error: msg });
3788
+ try {
3789
+ await chrome.offscreen.closeDocument();
3790
+ } catch {
3791
+ }
3792
+ await new Promise((r) => setTimeout(r, 1e3));
3793
+ await chrome.offscreen.createDocument({
3794
+ url: OFFSCREEN_URL,
3795
+ reasons: ["WORKERS"],
3796
+ justification: "hunter-mate \u9700\u4E0E\u672C\u5730 server \u4FDD\u6301\u5E38\u9A7B WebSocket \u8FDE\u63A5(MV3 SW \u4F1A\u88AB\u7A7A\u95F2\u56DE\u6536)"
3797
+ });
3798
+ sendInitToOffscreen();
3192
3799
  }
3800
+ } catch (err3) {
3801
+ console.error("[HunterMate] ensureOffscreen failed", err3);
3802
+ sendLog("error", "ensureOffscreen failed", { error: err3 instanceof Error ? err3.message : String(err3) });
3193
3803
  }
3194
- if (queuedDropped > 0) {
3195
- console.warn(`[HunterMate] send queue overflow, dropped ${queuedDropped} incremental events (snapshots kept)`);
3196
- queuedDropped = 0;
3804
+ try {
3805
+ const st = await chrome.runtime.sendMessage({ type: "offscreen:get-state" });
3806
+ state.wsConnected = !!st?.wsConnected;
3807
+ broadcastState();
3808
+ } catch {
3197
3809
  }
3198
3810
  }
3199
3811
  function sendLog(level, message, meta) {
@@ -3201,14 +3813,7 @@ function sendLog(level, message, meta) {
3201
3813
  backgroundLogger[level === "debug" ? "debug" : level]("extension:sendLog", { message, meta });
3202
3814
  } catch {
3203
3815
  }
3204
- const ws = socket;
3205
- if (ws?.readyState === WebSocket.OPEN) {
3206
- try {
3207
- ws.send(JSON.stringify({ type: "log", level, message, meta }));
3208
- } catch (e) {
3209
- console.error("[HunterMate] sendLog failed", e);
3210
- }
3211
- }
3816
+ sendToOffscreen(JSON.stringify({ type: "log", level, message, meta }));
3212
3817
  const prefix = `[HunterMate] ${message}`;
3213
3818
  if (level === "debug") {
3214
3819
  console.debug(prefix, meta ?? "");
@@ -3281,101 +3886,72 @@ function broadcastState() {
3281
3886
  });
3282
3887
  });
3283
3888
  }
3284
- function connect() {
3285
- if (socket) return;
3286
- socket = new WebSocket(WS_URL);
3287
- socket.onopen = () => {
3288
- state.wsConnected = true;
3289
- console.log("[HunterMate] connected to local server");
3290
- socket?.send(
3291
- JSON.stringify({
3292
- type: "hello",
3293
- extensionId: chrome.runtime.id,
3294
- version: chrome.runtime.getManifest().version,
3295
- capabilities: ["cp.*", "hm.*"]
3296
- })
3297
- );
3298
- flushSendQueue();
3299
- broadcastState();
3300
- backgroundLogger.info("ws:connected", { extensionId: chrome.runtime.id });
3301
- };
3302
- socket.onclose = () => {
3303
- socket = null;
3304
- state.wsConnected = false;
3305
- broadcastState();
3306
- backgroundLogger.warn("ws:closed", { reconnectDelayMs: 5e3 });
3307
- setTimeout(connect, 5e3);
3308
- };
3309
- socket.onerror = (err3) => {
3310
- backgroundLogger.error("ws:error", { message: String(err3) });
3311
- };
3312
- socket.onmessage = async (event) => {
3313
- let data;
3314
- try {
3315
- data = JSON.parse(event.data);
3316
- } catch {
3317
- return;
3318
- }
3319
- if (data.type === "pong" || data.type === "status" || data.type === "ack") return;
3320
- if (data.type === "reload") {
3321
- console.log("[HunterMate] received reload broadcast, reloading extension in 200ms...");
3322
- setTimeout(() => chrome.runtime.reload(), 200);
3323
- return;
3324
- }
3325
- if (data.type === "cmd" && data.requestId) {
3326
- const startedAt = Date.now();
3327
- sendLog("info", "extension handling control command", { command: data.command, requestId: data.requestId, params: data.params });
3328
- let outcome;
3329
- if (data.command === "hm.status") {
3330
- const manifest = chrome.runtime.getManifest();
3331
- outcome = {
3332
- status: "ok",
3333
- result: {
3334
- ...state,
3335
- tabStatesCount: tabStates.size,
3336
- extBasePath: chrome.runtime.getURL(""),
3337
- manifestPermissions: manifest.permissions,
3338
- hasSidePanelInManifest: manifest.permissions?.includes("sidePanel"),
3339
- chromeSidePanelType: typeof chrome.sidePanel
3340
- }
3341
- };
3342
- } else {
3343
- try {
3344
- outcome = await executeControlCommand(String(data.command ?? ""), data.params ?? {}, {
3345
- onLog: (level, message, meta) => sendLog(level, message, meta),
3346
- sessionId: typeof data.sessionId === "string" ? data.sessionId : void 0
3347
- });
3348
- } catch (e) {
3349
- outcome = { status: "error", message: e instanceof Error ? e.message : String(e) };
3350
- }
3351
- }
3352
- const resp = outcome.status === "ok" ? {
3353
- type: "cmd_response",
3354
- requestId: data.requestId,
3889
+ async function handleServerMessage(data) {
3890
+ if (!data || typeof data !== "object") return;
3891
+ if (data.type === "pong" || data.type === "status" || data.type === "ack") return;
3892
+ if (data.type === "reload") {
3893
+ console.log("[HunterMate] received reload broadcast, reloading extension in 200ms...");
3894
+ setTimeout(() => chrome.runtime.reload(), 200);
3895
+ return;
3896
+ }
3897
+ if (data.type === "cmd" && data.requestId) {
3898
+ const startedAt = Date.now();
3899
+ sendLog("info", "extension handling control command", { command: data.command, requestId: data.requestId, params: data.params });
3900
+ let outcome;
3901
+ if (data.command === "hm.status") {
3902
+ const manifest = chrome.runtime.getManifest();
3903
+ outcome = {
3355
3904
  status: "ok",
3356
- // undefined 显式归一为 null:JSON.stringify 会丢 undefined 键,
3357
- // 调用方无法区分"无 result 键"与"result 为 undefined"
3358
- result: outcome.result === void 0 ? null : outcome.result,
3359
- durationMs: Date.now() - startedAt
3360
- } : {
3361
- type: "cmd_response",
3362
- requestId: data.requestId,
3363
- status: "error",
3364
- error: { code: outcome.code, message: outcome.message },
3365
- durationMs: Date.now() - startedAt
3905
+ result: {
3906
+ ...state,
3907
+ tabStatesCount: tabStates.size,
3908
+ extBasePath: chrome.runtime.getURL(""),
3909
+ manifestPermissions: manifest.permissions,
3910
+ hasSidePanelInManifest: manifest.permissions?.includes("sidePanel"),
3911
+ chromeSidePanelType: typeof chrome.sidePanel,
3912
+ wsHeldBy: "offscreen"
3913
+ }
3366
3914
  };
3915
+ } else {
3367
3916
  try {
3368
- socket?.send(JSON.stringify(resp));
3369
- sendLog("debug", "cmd_response sent", { requestId: data.requestId, status: resp.status, durationMs: resp.durationMs });
3917
+ outcome = await executeControlCommand(String(data.command ?? ""), data.params ?? {}, {
3918
+ onLog: (level, message, meta) => sendLog(level, message, meta),
3919
+ sessionId: typeof data.sessionId === "string" ? data.sessionId : void 0
3920
+ });
3370
3921
  } catch (e) {
3371
- sendLog("error", "cmd_response send failed", { requestId: data.requestId, error: e instanceof Error ? e.message : String(e) });
3372
- }
3373
- return;
3374
- }
3375
- console.log("[HunterMate] server message", data);
3376
- };
3922
+ outcome = { status: "error", message: e instanceof Error ? e.message : String(e) };
3923
+ }
3924
+ }
3925
+ const resp = outcome.status === "ok" ? {
3926
+ type: "cmd_response",
3927
+ requestId: data.requestId,
3928
+ status: "ok",
3929
+ // undefined 显式归一为 null:JSON.stringify 会丢 undefined 键,
3930
+ // 调用方无法区分"无 result 键"与"result 为 undefined"
3931
+ result: outcome.result === void 0 ? null : outcome.result,
3932
+ durationMs: Date.now() - startedAt
3933
+ } : {
3934
+ type: "cmd_response",
3935
+ requestId: data.requestId,
3936
+ status: "error",
3937
+ error: { code: outcome.code, message: outcome.message },
3938
+ durationMs: Date.now() - startedAt
3939
+ };
3940
+ sendToOffscreen(JSON.stringify(resp));
3941
+ sendLog("debug", "cmd_response sent", { requestId: data.requestId, status: resp.status, durationMs: resp.durationMs });
3942
+ }
3377
3943
  }
3378
- connect();
3944
+ chrome.runtime.onMessage.addListener((message) => {
3945
+ if (message?.type === "offscreen:ws-state") {
3946
+ state.wsConnected = !!message.wsConnected;
3947
+ broadcastState();
3948
+ return;
3949
+ }
3950
+ if (message?.type === "offscreen:ws-message") {
3951
+ void handleServerMessage(message.payload);
3952
+ return;
3953
+ }
3954
+ });
3379
3955
  var RECONNECT_ALARM_NAME = "hm:reconnect";
3380
3956
  function ensureReconnectAlarm() {
3381
3957
  chrome.alarms.create(RECONNECT_ALARM_NAME, { periodInMinutes: 0.5 }).catch((err3) => console.error("[HunterMate] create reconnect alarm failed", err3));
@@ -3383,21 +3959,24 @@ function ensureReconnectAlarm() {
3383
3959
  chrome.alarms.onAlarm.addListener((alarm) => {
3384
3960
  if (alarm.name !== RECONNECT_ALARM_NAME) return;
3385
3961
  checkHttpHealth();
3386
- if (socket && socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) {
3387
- socket = null;
3388
- }
3389
- if (!socket) {
3390
- connect();
3391
- }
3962
+ sendLog("debug", "sw:alarm-tick", { wsConnected: state.wsConnected });
3963
+ void ensureOffscreen();
3392
3964
  });
3393
3965
  ensureReconnectAlarm();
3966
+ void ensureOffscreen();
3967
+ chrome.runtime.onInstalled.addListener(() => {
3968
+ console.log("[HunterMate] onInstalled: cold-start offscreen");
3969
+ ensureReconnectAlarm();
3970
+ void ensureOffscreen();
3971
+ });
3972
+ chrome.runtime.onStartup.addListener(() => {
3973
+ console.log("[HunterMate] onStartup: cold-start offscreen");
3974
+ ensureReconnectAlarm();
3975
+ void ensureOffscreen();
3976
+ });
3394
3977
  setInterval(() => {
3395
3978
  checkHttpHealth();
3396
- if (socket?.readyState === WebSocket.OPEN) {
3397
- socket.send(JSON.stringify({ type: "ping" }));
3398
- } else {
3399
- connect();
3400
- }
3979
+ void ensureOffscreen();
3401
3980
  broadcastState();
3402
3981
  }, 2e4);
3403
3982
  setInterval(() => {
@@ -3482,11 +4061,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
3482
4061
  if (message.type === "recording-event") {
3483
4062
  const tabId = sender.tab?.id ?? -1;
3484
4063
  const payload = { ...message.payload, tabId };
3485
- if (socket?.readyState === WebSocket.OPEN) {
3486
- socket.send(JSON.stringify(payload));
3487
- } else {
3488
- enqueueRecordingEvent(payload);
3489
- }
4064
+ sendToOffscreen(JSON.stringify(payload));
3490
4065
  const ts = tabStates.get(tabId);
3491
4066
  if (ts && message.payload?.sessionId === ts.recordingSessionId) {
3492
4067
  ts.eventCount++;