hunter-mate 0.1.6 → 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 (261) hide show
  1. package/dist/assets/extension/background/service-worker.js +911 -159
  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 +12 -4
  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 +12 -1
  12. package/dist/assets/server/app.module.js.map +1 -1
  13. package/dist/assets/server/auth/auth.controller.d.ts +7 -0
  14. package/dist/assets/server/auth/auth.controller.js +20 -0
  15. package/dist/assets/server/auth/auth.controller.js.map +1 -1
  16. package/dist/assets/server/auth/auth.middleware.js +9 -1
  17. package/dist/assets/server/auth/auth.middleware.js.map +1 -1
  18. package/dist/assets/server/auth/auth.service.d.ts +11 -0
  19. package/dist/assets/server/auth/auth.service.js +58 -1
  20. package/dist/assets/server/auth/auth.service.js.map +1 -1
  21. package/dist/assets/server/build-info.json +1 -1
  22. package/dist/assets/server/customer/customer.controller.d.ts +18 -0
  23. package/dist/assets/server/customer/customer.controller.js +81 -0
  24. package/dist/assets/server/customer/customer.controller.js.map +1 -0
  25. package/dist/assets/server/customer/customer.entity.d.ts +17 -0
  26. package/dist/assets/server/customer/customer.entity.js +82 -0
  27. package/dist/assets/server/customer/customer.entity.js.map +1 -0
  28. package/dist/assets/server/customer/customer.module.d.ts +2 -0
  29. package/dist/assets/server/customer/customer.module.js +28 -0
  30. package/dist/assets/server/customer/customer.module.js.map +1 -0
  31. package/dist/assets/server/customer/customer.service.d.ts +36 -0
  32. package/dist/assets/server/customer/customer.service.js +200 -0
  33. package/dist/assets/server/customer/customer.service.js.map +1 -0
  34. package/dist/assets/server/db/schema-validator.service.d.ts +2 -0
  35. package/dist/assets/server/db/schema-validator.service.js +32 -1
  36. package/dist/assets/server/db/schema-validator.service.js.map +1 -1
  37. package/dist/assets/server/db/sqlite.module.d.ts +1 -0
  38. package/dist/assets/server/db/sqlite.module.js +12 -2
  39. package/dist/assets/server/db/sqlite.module.js.map +1 -1
  40. package/dist/assets/server/devops/devops-sink-init.d.ts +1 -14
  41. package/dist/assets/server/devops/devops-sink-init.js +3 -8
  42. package/dist/assets/server/devops/devops-sink-init.js.map +1 -1
  43. package/dist/assets/server/extension/extension-control.controller.d.ts +15 -0
  44. package/dist/assets/server/extension/extension-control.controller.js +42 -2
  45. package/dist/assets/server/extension/extension-control.controller.js.map +1 -1
  46. package/dist/assets/server/extension/extension-control.service.d.ts +14 -1
  47. package/dist/assets/server/extension/extension-control.service.js +65 -2
  48. package/dist/assets/server/extension/extension-control.service.js.map +1 -1
  49. package/dist/assets/server/extraction/adapter.service.d.ts +3 -0
  50. package/dist/assets/server/extraction/adapter.service.js +127 -6
  51. package/dist/assets/server/extraction/adapter.service.js.map +1 -1
  52. package/dist/assets/server/extraction/anomaly-report.service.js +1 -1
  53. package/dist/assets/server/extraction/anomaly-report.service.js.map +1 -1
  54. package/dist/assets/server/extraction/entities/adapter-rule.entity.d.ts +2 -0
  55. package/dist/assets/server/extraction/entities/adapter-rule.entity.js +10 -0
  56. package/dist/assets/server/extraction/entities/adapter-rule.entity.js.map +1 -1
  57. package/dist/assets/server/extraction/extraction-entity.controller.d.ts +21 -3
  58. package/dist/assets/server/extraction/extraction-entity.controller.js +44 -9
  59. package/dist/assets/server/extraction/extraction-entity.controller.js.map +1 -1
  60. package/dist/assets/server/extraction/extraction.module.js +13 -3
  61. package/dist/assets/server/extraction/extraction.module.js.map +1 -1
  62. package/dist/assets/server/extraction/extraction.service.d.ts +4 -1
  63. package/dist/assets/server/extraction/extraction.service.js +59 -4
  64. package/dist/assets/server/extraction/extraction.service.js.map +1 -1
  65. package/dist/assets/server/extraction/harness-overlay.service.d.ts +41 -0
  66. package/dist/assets/server/extraction/harness-overlay.service.js +319 -0
  67. package/dist/assets/server/extraction/harness-overlay.service.js.map +1 -0
  68. package/dist/assets/server/extraction/liepin-extractor.d.ts +1 -0
  69. package/dist/assets/server/extraction/liepin-extractor.js +172 -0
  70. package/dist/assets/server/extraction/liepin-extractor.js.map +1 -0
  71. package/dist/assets/server/extraction/script-rule.js +4 -0
  72. package/dist/assets/server/extraction/script-rule.js.map +1 -1
  73. package/dist/assets/server/governance/action-audit.entity.d.ts +12 -0
  74. package/dist/assets/server/governance/action-audit.entity.js +66 -0
  75. package/dist/assets/server/governance/action-audit.entity.js.map +1 -0
  76. package/dist/assets/server/governance/governance.controller.d.ts +14 -0
  77. package/dist/assets/server/governance/governance.controller.js +77 -0
  78. package/dist/assets/server/governance/governance.controller.js.map +1 -0
  79. package/dist/assets/server/governance/governance.ingester.d.ts +20 -0
  80. package/dist/assets/server/governance/governance.ingester.js +114 -0
  81. package/dist/assets/server/governance/governance.ingester.js.map +1 -0
  82. package/dist/assets/server/governance/governance.module.d.ts +2 -0
  83. package/dist/assets/server/governance/governance.module.js +26 -0
  84. package/dist/assets/server/governance/governance.module.js.map +1 -0
  85. package/dist/assets/server/governance/governance.service.d.ts +114 -0
  86. package/dist/assets/server/governance/governance.service.js +356 -0
  87. package/dist/assets/server/governance/governance.service.js.map +1 -0
  88. package/dist/assets/server/jobcase/jobcase.controller.d.ts +1 -0
  89. package/dist/assets/server/jobcase/jobcase.entity.d.ts +1 -0
  90. package/dist/assets/server/jobcase/jobcase.entity.js +6 -0
  91. package/dist/assets/server/jobcase/jobcase.entity.js.map +1 -1
  92. package/dist/assets/server/jobcase/jobcase.service.d.ts +2 -0
  93. package/dist/assets/server/jobcase/jobcase.service.js +2 -1
  94. package/dist/assets/server/jobcase/jobcase.service.js.map +1 -1
  95. package/dist/assets/server/main.js +1 -0
  96. package/dist/assets/server/main.js.map +1 -1
  97. package/dist/assets/server/package.json +5 -4
  98. package/dist/assets/server/preference/preference.controller.d.ts +14 -0
  99. package/dist/assets/server/preference/preference.controller.js +66 -0
  100. package/dist/assets/server/preference/preference.controller.js.map +1 -0
  101. package/dist/assets/server/preference/preference.entity.d.ts +13 -0
  102. package/dist/assets/server/preference/preference.entity.js +77 -0
  103. package/dist/assets/server/preference/preference.entity.js.map +1 -0
  104. package/dist/assets/server/preference/preference.module.d.ts +2 -0
  105. package/dist/assets/server/preference/preference.module.js +26 -0
  106. package/dist/assets/server/preference/preference.module.js.map +1 -0
  107. package/dist/assets/server/preference/preference.service.d.ts +33 -0
  108. package/dist/assets/server/preference/preference.service.js +122 -0
  109. package/dist/assets/server/preference/preference.service.js.map +1 -0
  110. package/dist/assets/server/recording/recording-event-queue.service.d.ts +26 -0
  111. package/dist/assets/server/recording/recording-event-queue.service.js +115 -0
  112. package/dist/assets/server/recording/recording-event-queue.service.js.map +1 -0
  113. package/dist/assets/server/recording/recording-retention.service.d.ts +38 -0
  114. package/dist/assets/server/recording/recording-retention.service.js +174 -0
  115. package/dist/assets/server/recording/recording-retention.service.js.map +1 -0
  116. package/dist/assets/server/recording/recording-snapshot.service.d.ts +1 -0
  117. package/dist/assets/server/recording/recording-snapshot.service.js +4 -0
  118. package/dist/assets/server/recording/recording-snapshot.service.js.map +1 -1
  119. package/dist/assets/server/recording/recording.controller.d.ts +21 -1
  120. package/dist/assets/server/recording/recording.controller.js +37 -2
  121. package/dist/assets/server/recording/recording.controller.js.map +1 -1
  122. package/dist/assets/server/recording/recording.gateway.js +11 -0
  123. package/dist/assets/server/recording/recording.gateway.js.map +1 -1
  124. package/dist/assets/server/recording/recording.module.js +16 -2
  125. package/dist/assets/server/recording/recording.module.js.map +1 -1
  126. package/dist/assets/server/recording/recording.service.d.ts +10 -1
  127. package/dist/assets/server/recording/recording.service.js +31 -6
  128. package/dist/assets/server/recording/recording.service.js.map +1 -1
  129. package/dist/assets/server/remark/remark.controller.d.ts +1 -1
  130. package/dist/assets/server/remark/remark.controller.js +1 -10
  131. package/dist/assets/server/remark/remark.controller.js.map +1 -1
  132. package/dist/assets/server/remark/remark.entity.d.ts +1 -1
  133. package/dist/assets/server/remark/remark.entity.js.map +1 -1
  134. package/dist/assets/server/site/entities/site-registry.entity.d.ts +1 -1
  135. package/dist/assets/server/site/entities/site-registry.entity.js +1 -1
  136. package/dist/assets/server/site/entities/site-registry.entity.js.map +1 -1
  137. package/dist/assets/server/site/site.controller.d.ts +1 -1
  138. package/dist/assets/server/site/site.service.d.ts +5 -1
  139. package/dist/assets/server/site/site.service.js +13 -0
  140. package/dist/assets/server/site/site.service.js.map +1 -1
  141. package/dist/assets/server/system/system.controller.d.ts +1 -0
  142. package/dist/assets/server/system/system.controller.js +1 -1
  143. package/dist/assets/server/system/system.controller.js.map +1 -1
  144. package/dist/assets/server/system/system.service.d.ts +1 -0
  145. package/dist/assets/server/system/system.service.js +3 -2
  146. package/dist/assets/server/system/system.service.js.map +1 -1
  147. package/dist/cache/db.js +6 -6
  148. package/dist/commands/addons.js +2 -1
  149. package/dist/commands/cache.js +3 -4
  150. package/dist/commands/case.d.ts +67 -0
  151. package/dist/commands/case.js +101 -0
  152. package/dist/commands/config.d.ts +4 -0
  153. package/dist/commands/config.js +8 -1
  154. package/dist/commands/customer.d.ts +25 -0
  155. package/dist/commands/customer.js +137 -0
  156. package/dist/commands/doctor.js +119 -13
  157. package/dist/commands/extension.js +26 -32
  158. package/dist/commands/extras.d.ts +22 -3
  159. package/dist/commands/extras.js +116 -7
  160. package/dist/commands/init.d.ts +1 -1
  161. package/dist/commands/init.js +3 -4
  162. package/dist/commands/issue-env.d.ts +23 -0
  163. package/dist/commands/issue-env.js +101 -0
  164. package/dist/commands/job.d.ts +6 -2
  165. package/dist/commands/job.js +75 -20
  166. package/dist/commands/login.d.ts +19 -0
  167. package/dist/commands/login.js +85 -13
  168. package/dist/commands/migrate.d.ts +32 -0
  169. package/dist/commands/migrate.js +116 -0
  170. package/dist/commands/note.d.ts +3 -0
  171. package/dist/commands/note.js +75 -0
  172. package/dist/commands/ops.js +82 -15
  173. package/dist/commands/preference.d.ts +6 -0
  174. package/dist/commands/preference.js +80 -0
  175. package/dist/commands/record.js +2 -4
  176. package/dist/commands/report.js +2 -4
  177. package/dist/commands/search.js +9 -2
  178. package/dist/commands/server.d.ts +9 -2
  179. package/dist/commands/server.js +118 -19
  180. package/dist/commands/status.d.ts +4 -0
  181. package/dist/commands/status.js +16 -6
  182. package/dist/commands/update.js +8 -9
  183. package/dist/config.d.ts +21 -0
  184. package/dist/config.js +27 -0
  185. package/dist/i18n/locales/en-US.json +19 -1
  186. package/dist/i18n/locales/zh-CN.json +19 -1
  187. package/dist/index.js +52 -2
  188. package/dist/keychain.d.ts +16 -1
  189. package/dist/keychain.js +43 -16
  190. package/dist/logger.js +2 -2
  191. package/dist/updater.d.ts +1 -1
  192. package/dist/updater.js +24 -7
  193. package/dist/utils/command-error.d.ts +11 -0
  194. package/dist/utils/command-error.js +47 -0
  195. package/dist/utils/link.d.ts +18 -0
  196. package/dist/utils/link.js +83 -0
  197. package/dist/utils/proxy-detect.d.ts +20 -0
  198. package/dist/utils/proxy-detect.js +96 -0
  199. package/dist/utils/script-path.d.ts +5 -0
  200. package/dist/utils/script-path.js +27 -0
  201. package/dist/version.js +34 -9
  202. package/dist/watchdog.d.ts +23 -1
  203. package/dist/watchdog.js +97 -6
  204. package/dist/web/dist/404.html +1 -0
  205. package/dist/web/dist/_next/static/WdJr3NhHjlu5xN0qOap1k/_buildManifest.js +1 -0
  206. package/dist/web/dist/_next/static/WdJr3NhHjlu5xN0qOap1k/_ssgManifest.js +1 -0
  207. package/dist/web/dist/_next/static/chunks/166.f30be223f694f8ee.js +1 -0
  208. package/dist/web/dist/_next/static/chunks/1e6d5a0c.0488932e17ecdf24.js +15 -0
  209. package/dist/web/dist/_next/static/chunks/539-5d0e8c5c238c56f8.js +1 -0
  210. package/dist/web/dist/_next/static/chunks/589-d632233641f35681.js +1 -0
  211. package/dist/web/dist/_next/static/chunks/671.3b8e9cc96b25b6ea.js +1 -0
  212. package/dist/web/dist/_next/static/chunks/935-99f57b7d4151d9e7.js +5 -0
  213. package/dist/web/dist/_next/static/chunks/a7067448-96dd4b8457c447e8.js +1 -0
  214. package/dist/web/dist/_next/static/chunks/app/_not-found/page-fdf1a13a7656886a.js +1 -0
  215. package/dist/web/dist/_next/static/chunks/app/auth/verify/page-8e5cff53d6f7d8e1.js +1 -0
  216. package/dist/web/dist/_next/static/chunks/app/companies/page-e38a69e1929d0953.js +1 -0
  217. package/dist/web/dist/_next/static/chunks/app/entity/page-250f8a9b6d91ab19.js +1 -0
  218. package/dist/web/dist/_next/static/chunks/app/jobs/page-6a281776fec065f6.js +1 -0
  219. package/dist/web/dist/_next/static/chunks/app/layout-83e623e973c1693f.js +1 -0
  220. package/dist/web/dist/_next/static/chunks/app/logs/page-d478031d49f7ba00.js +1 -0
  221. package/dist/web/dist/_next/static/chunks/app/matching/jobs/[id]/talents/page-f57f4b7529823601.js +1 -0
  222. package/dist/web/dist/_next/static/chunks/app/matching/portraits/[id]/jobs/page-614d1e62d7b9d215.js +1 -0
  223. package/dist/web/dist/_next/static/chunks/app/page-e3392de08ae81fba.js +1 -0
  224. package/dist/web/dist/_next/static/chunks/app/parselog/page-58d3acece95b4089.js +1 -0
  225. package/dist/web/dist/_next/static/chunks/app/recordings/page-e2e2e3c3a64083fb.js +1 -0
  226. package/dist/web/dist/_next/static/chunks/app/snapshots/[id]/agent-prompt/page-fd3b7d7373b8f8b7.js +1 -0
  227. package/dist/web/dist/_next/static/chunks/app/talents/page-30748338da20d7e0.js +1 -0
  228. package/dist/web/dist/_next/static/chunks/framework-b3a558b17a070965.js +1 -0
  229. package/dist/web/dist/_next/static/chunks/main-5a4e0cd75dd7f04b.js +1 -0
  230. package/dist/web/dist/_next/static/chunks/main-app-80b038a312917d90.js +1 -0
  231. package/dist/web/dist/_next/static/chunks/pages/_app-da7d628f49271ece.js +4 -0
  232. package/dist/web/dist/_next/static/chunks/pages/_error-17756f67e0a11350.js +1 -0
  233. package/dist/web/dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  234. package/dist/web/dist/_next/static/chunks/webpack-1784baecc6fdcf0b.js +1 -0
  235. package/dist/web/dist/_next/static/css/db881c5b3072b88a.css +3 -0
  236. package/dist/web/dist/_next/static/css/f60d210841fab76b.css +1 -0
  237. package/dist/web/dist/auth/verify.html +1 -0
  238. package/dist/web/dist/auth/verify.txt +8 -0
  239. package/dist/web/dist/companies.html +1 -0
  240. package/dist/web/dist/companies.txt +7 -0
  241. package/dist/web/dist/entity.html +1 -0
  242. package/dist/web/dist/entity.txt +8 -0
  243. package/dist/web/dist/index.html +1 -0
  244. package/dist/web/dist/index.txt +8 -0
  245. package/dist/web/dist/jobs.html +1 -0
  246. package/dist/web/dist/jobs.txt +7 -0
  247. package/dist/web/dist/logs.html +1 -0
  248. package/dist/web/dist/logs.txt +8 -0
  249. package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -0
  250. package/dist/web/dist/matching/jobs/placeholder/talents.txt +8 -0
  251. package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -0
  252. package/dist/web/dist/matching/portraits/placeholder/jobs.txt +8 -0
  253. package/dist/web/dist/parselog.html +1 -0
  254. package/dist/web/dist/parselog.txt +8 -0
  255. package/dist/web/dist/recordings.html +1 -0
  256. package/dist/web/dist/recordings.txt +8 -0
  257. package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -0
  258. package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +8 -0
  259. package/dist/web/dist/talents.html +1 -0
  260. package/dist/web/dist/talents.txt +7 -0
  261. package/package.json +26 -6
@@ -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";
@@ -1534,6 +1563,21 @@ async function captureActiveTabScreenshot2(tab, options = {}) {
1534
1563
  throw new Error("capture_failed_unreachable");
1535
1564
  }
1536
1565
 
1566
+ // src/shared/eval-wrap.ts
1567
+ function buildEvalSyntaxCheckExpression(script) {
1568
+ return "(() => { try { new Function(" + JSON.stringify("return (async () => (" + script + "\n))();") + "); return true; } catch (e) { return false; } })()";
1569
+ }
1570
+ function buildEvalExpressionWrapper(script) {
1571
+ return `(async () => { return (
1572
+ ${script}
1573
+ ); })()`;
1574
+ }
1575
+ function buildEvalStatementWrapper(script) {
1576
+ return `(async () => {
1577
+ ${script}
1578
+ })()`;
1579
+ }
1580
+
1537
1581
  // src/background/session-groups.ts
1538
1582
  var STORAGE_KEY = "hmSessionGroups";
1539
1583
  var cache = /* @__PURE__ */ new Map();
@@ -1869,8 +1913,48 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
1869
1913
  await chrome.windows.remove(windowId);
1870
1914
  return ok({ closed: true, windowId });
1871
1915
  }
1872
- const tab = await getControlTab(tabOverride);
1873
- if (!tab?.id) return err2("no_active_tab");
1916
+ if (command === "hm.linkedin.search") {
1917
+ try {
1918
+ const result = await runLinkedInSearch(controlArgs, log, {
1919
+ sessionId,
1920
+ groupTitle,
1921
+ explicitTab
1922
+ });
1923
+ return ok(result);
1924
+ } catch (e) {
1925
+ const code = e instanceof ControlBlockedError ? e.code : void 0;
1926
+ return err2(e instanceof Error ? e.message : String(e), code);
1927
+ }
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
+ }
1942
+ let tab = await getControlTab(tabOverride);
1943
+ if (!tab?.id) {
1944
+ if (command === "cp.goto" && typeof controlArgs.url === "string" && controlArgs.url) {
1945
+ const created = await chrome.tabs.create({ url: controlArgs.url, active: false });
1946
+ log("info", "auto-created tab for cp.goto (no active tab)", { tabId: created.id, url: controlArgs.url });
1947
+ if (sessionId && !explicitTab) {
1948
+ addTabToSessionGroup(sessionId, groupTitle, created).catch(
1949
+ (err3) => log("warn", "cp.goto auto-tab addTabToSessionGroup failed", { sessionId, error: err3.message })
1950
+ );
1951
+ }
1952
+ tab = created;
1953
+ } else {
1954
+ return err2("no_active_tab");
1955
+ }
1956
+ }
1957
+ if (!tab.id) return err2("no_active_tab");
1874
1958
  if (sessionId && !explicitTab) {
1875
1959
  ensureSessionGroup(sessionId, groupTitle, tab).catch(
1876
1960
  (err3) => log("warn", "ensureSessionGroup failed", { sessionId, tabId: tab.id, error: err3.message })
@@ -1945,20 +2029,24 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
1945
2029
  if (command === "cp.eval" && typeof controlArgs.script === "string") {
1946
2030
  const script = controlArgs.script;
1947
2031
  const frame = controlArgs.frame;
1948
- if (frame !== void 0 && isAttachableUrl(tab.url)) {
1949
- let frameId = typeof frame === "string" ? frame : "";
1950
- let frameUrl;
1951
- if (typeof frame === "number" || /^\d+$/.test(String(frame))) {
1952
- const list = await frameListCdp(tab.id);
1953
- const target = list.frames?.find((f) => f.index === Number(frame));
1954
- if (!target) return err2(`frame_not_found: ${frame}`);
1955
- frameId = target.frame_id;
1956
- frameUrl = target.url;
2032
+ const tabId = tab.id;
2033
+ const evalWith = async (expr) => {
2034
+ if (frame !== void 0 && isAttachableUrl(tab.url)) {
2035
+ let frameId = typeof frame === "string" ? frame : "";
2036
+ let frameUrl;
2037
+ if (typeof frame === "number" || /^\d+$/.test(String(frame))) {
2038
+ const list = await frameListCdp(tabId);
2039
+ const target = list.frames?.find((f) => f.index === Number(frame));
2040
+ if (!target) throw new Error(`frame_not_found: ${frame}`);
2041
+ frameId = target.frame_id;
2042
+ frameUrl = target.url;
2043
+ }
2044
+ return evaluateInFrame(tabId, expr, frameId, frameUrl);
1957
2045
  }
1958
- const result2 = await evaluateInFrame(tab.id, `(async () => { ${script} })()`, frameId, frameUrl);
1959
- return ok(result2);
1960
- }
1961
- const result = await cdpEvaluate(tab.id, `(async () => { ${script} })()`);
2046
+ return cdpEvaluate(tabId, expr);
2047
+ };
2048
+ const useExpression = await evalWith(buildEvalSyntaxCheckExpression(script)).catch(() => false) === true;
2049
+ const result = await evalWith(useExpression ? buildEvalExpressionWrapper(script) : buildEvalStatementWrapper(script));
1962
2050
  return ok(result);
1963
2051
  }
1964
2052
  if (command === "hm.linkedin.extract") {
@@ -1983,6 +2071,17 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
1983
2071
  return err2(e instanceof Error ? e.message : String(e));
1984
2072
  }
1985
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
+ }
1986
2085
  if (command.startsWith("hm.")) {
1987
2086
  return { status: "error", code: "not_implemented", message: `${command} is not implemented yet` };
1988
2087
  }
@@ -2021,6 +2120,13 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
2021
2120
  return err2(e instanceof Error ? e.message : String(e));
2022
2121
  }
2023
2122
  }
2123
+ var ControlBlockedError = class extends Error {
2124
+ constructor(code, message) {
2125
+ super(message);
2126
+ this.code = code;
2127
+ this.name = "ControlBlockedError";
2128
+ }
2129
+ };
2024
2130
  function buildInjectionExpression(command, controlArgs) {
2025
2131
  return '(() => { try { if (typeof globalThis.__cpRunControl !== "function" || globalThis.__cpRuntimeVersion !== ' + JSON.stringify(RUNTIME_VERSION) + ") {\n" + control_runtime_source_default + "\n} const r = globalThis.__cpRunControl(" + JSON.stringify(command) + ", " + JSON.stringify(controlArgs) + '); if (r && typeof r.then === "function") { return r; } return r; } catch (e) { return { __cpError: String((e && e.message) || e) }; } })()';
2026
2132
  }
@@ -2213,6 +2319,657 @@ var LINKEDIN_EXTRACT_SOURCE = String.raw`(async () => {
2213
2319
  extractedAt: new Date().toISOString(),
2214
2320
  };
2215
2321
  })()`;
2322
+ var LINKEDIN_WALL_CHECK_SOURCE = String.raw`(() => {
2323
+ try {
2324
+ if (document.querySelector('iframe[src*="captcha"], iframe[src*="challenge-platform"]')) return 'CAPTCHA';
2325
+ const bodyText = (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 3000);
2326
+ if (/verify you('| a)re human|captcha|security (check|verification)/i.test(bodyText)) return 'CAPTCHA';
2327
+ const url = location.href;
2328
+ if (/linkedin\.com\/(authwall|uas\/login|login)/i.test(url)) return 'LOGIN_WALL';
2329
+ if (/linkedin\.com\/checkpoint/i.test(url)) return 'CAPTCHA';
2330
+ } catch (e) { /* 检测失败不阻断搜索 */ }
2331
+ return null;
2332
+ })()`;
2333
+ var LINKEDIN_NEXT_CLICK_SOURCE = String.raw`(async () => {
2334
+ const btn = document.querySelector('button.artdeco-pagination__button--next')
2335
+ || document.querySelector('button[aria-label="Next"]')
2336
+ || document.querySelector('[aria-label="Next"] button');
2337
+ if (!btn || btn.disabled) return false;
2338
+ btn.click();
2339
+ return true;
2340
+ })()`;
2341
+ function sleep(ms) {
2342
+ return new Promise((r) => setTimeout(r, ms));
2343
+ }
2344
+ async function waitTabLoaded(tabId, timeoutMs = 25e3) {
2345
+ const t0 = Date.now();
2346
+ let lastUrl = "";
2347
+ let stableHits = 0;
2348
+ while (Date.now() - t0 < timeoutMs) {
2349
+ const t = await chrome.tabs.get(tabId).catch(() => null);
2350
+ if (!t) throw new Error(`tab ${tabId} closed while loading`);
2351
+ if (t.status === "complete") {
2352
+ if (t.url === lastUrl) {
2353
+ stableHits++;
2354
+ if (stableHits >= 2) return;
2355
+ } else {
2356
+ stableHits = 0;
2357
+ lastUrl = t.url || "";
2358
+ }
2359
+ } else {
2360
+ stableHits = 0;
2361
+ lastUrl = "";
2362
+ }
2363
+ await sleep(400);
2364
+ }
2365
+ throw new Error(`tab ${tabId} load timeout after ${timeoutMs}ms`);
2366
+ }
2367
+ async function runLinkedInSearch(args, log, ctx) {
2368
+ const keywords = typeof args.keywords === "string" ? args.keywords.trim() : "";
2369
+ const filters = args.filters && typeof args.filters === "object" ? args.filters : {};
2370
+ const maxPages = Math.min(Math.max(Number(args.maxPages) || 3, 1), 10);
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);
2374
+ const delayBase = typeof args.delayMs === "number" ? Number(args.delayMs) : 2500;
2375
+ let effectiveKeywords = keywords;
2376
+ if (!filters.geoUrn && typeof filters.location === "string" && filters.location.trim()) {
2377
+ effectiveKeywords = (keywords + " " + filters.location.trim()).trim();
2378
+ }
2379
+ if (!effectiveKeywords && !filters.company && !filters.school) {
2380
+ throw new Error("linkedin_search_requires_keywords_or_filters");
2381
+ }
2382
+ const u = new URL("https://www.linkedin.com/search/results/people/");
2383
+ if (effectiveKeywords) u.searchParams.set("keywords", effectiveKeywords);
2384
+ if (typeof filters.geoUrn === "string" && filters.geoUrn) u.searchParams.set("geoUrn", filters.geoUrn);
2385
+ if (typeof filters.company === "string" && filters.company) u.searchParams.set("company", filters.company);
2386
+ if (typeof filters.school === "string" && filters.school) u.searchParams.set("school", filters.school);
2387
+ const tab = await chrome.tabs.create({ url: u.toString(), active: false });
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
+ }
2394
+ if (ctx.sessionId && !ctx.explicitTab && tab.id) {
2395
+ addTabToSessionGroup(ctx.sessionId, ctx.groupTitle, tab).catch(
2396
+ (err3) => log("warn", "hm.linkedin.search addTabToSessionGroup failed", { error: err3.message })
2397
+ );
2398
+ }
2399
+ const leads = /* @__PURE__ */ new Map();
2400
+ let pagesScanned = 0;
2401
+ let lastUrl = "";
2402
+ try {
2403
+ for (let page = 1; page <= maxPages; page++) {
2404
+ if (tab.id == null) break;
2405
+ await waitTabLoaded(tab.id, loadTimeoutMs);
2406
+ const blocked = await cdpEvaluate(tab.id, LINKEDIN_WALL_CHECK_SOURCE).catch(() => null);
2407
+ if (blocked === "CAPTCHA" || blocked === "LOGIN_WALL") {
2408
+ log("warn", "hm.linkedin.search blocked by linkedin risk control", { blocked, page, url: lastUrl });
2409
+ throw new ControlBlockedError(blocked, `linkedin search aborted on page ${page}: ${blocked}`);
2410
+ }
2411
+ const extracted = await cdpEvaluate(tab.id, LINKEDIN_EXTRACT_SOURCE);
2412
+ lastUrl = extracted?.url || lastUrl;
2413
+ const pageLeads = extracted?.links?.talent || [];
2414
+ for (const l of pageLeads) {
2415
+ if (l.url && !leads.has(l.url)) {
2416
+ leads.set(l.url, { url: l.url, name: l.name ?? null, page, source: "linkedin" });
2417
+ }
2418
+ }
2419
+ pagesScanned = page;
2420
+ log("info", "hm.linkedin.search page extracted", { page, pageLeads: pageLeads.length, total: leads.size });
2421
+ if (page >= maxPages) break;
2422
+ const clicked = await cdpEvaluate(tab.id, LINKEDIN_NEXT_CLICK_SOURCE).catch(() => false);
2423
+ const urlBefore = (await chrome.tabs.get(tab.id).catch(() => null))?.url || "";
2424
+ await sleep(clicked ? 2500 : 0);
2425
+ const urlAfterClick = (await chrome.tabs.get(tab.id).catch(() => null))?.url || "";
2426
+ if (!clicked || urlAfterClick === urlBefore) {
2427
+ const next = new URL(u.toString());
2428
+ next.searchParams.set("page", String(page + 1));
2429
+ if (next.toString() === urlBefore) break;
2430
+ await chrome.tabs.update(tab.id, { url: next.toString() });
2431
+ log("debug", "hm.linkedin.search fallback to page param", { page: page + 1 });
2432
+ }
2433
+ await sleep(delayBase + Math.random() * 2500);
2434
+ }
2435
+ } finally {
2436
+ if (!keepTab && tab.id != null) {
2437
+ await closeTabGracefully(tab.id);
2438
+ }
2439
+ }
2440
+ return {
2441
+ keywords: effectiveKeywords,
2442
+ searchUrl: u.toString(),
2443
+ pagesScanned,
2444
+ leadCount: leads.size,
2445
+ leads: Array.from(leads.values()),
2446
+ keepTab,
2447
+ tabId: keepTab ? tab.id ?? null : null
2448
+ };
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
+ }
2216
2973
  var HIREDCHINA_EXTRACT_SOURCE = String.raw`(async () => {
2217
2974
  const waitFor = (fn, timeout, interval) => new Promise((resolve) => {
2218
2975
  const t0 = Date.now();
@@ -2436,6 +3193,14 @@ async function handleCookiesCommand(args) {
2436
3193
  }
2437
3194
  throw new Error(`cookies_unknown_action: ${action}`);
2438
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
+ }
2439
3204
  async function handleTabCommand(command, args, log) {
2440
3205
  const tabArg = args.tab ?? args.targetId ?? args.page;
2441
3206
  switch (command) {
@@ -2467,7 +3232,7 @@ async function handleTabCommand(command, args, log) {
2467
3232
  }
2468
3233
  case "cp.tab.close": {
2469
3234
  if (!tabArg) throw new Error("tab_close_requires_tab_id");
2470
- await chrome.tabs.remove(Number(tabArg));
3235
+ await closeTabGracefully(Number(tabArg));
2471
3236
  return { closed: true, page: String(tabArg) };
2472
3237
  }
2473
3238
  case "cp.tab.bind": {
@@ -2650,7 +3415,7 @@ function maybeDiscover(url, title) {
2650
3415
  });
2651
3416
  }
2652
3417
 
2653
- // ../../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
2654
3419
  function stringifyArg(arg) {
2655
3420
  if (typeof arg === "string")
2656
3421
  return arg;
@@ -2802,7 +3567,7 @@ var Logger = class {
2802
3567
  }
2803
3568
  };
2804
3569
 
2805
- // ../../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
2806
3571
  async function createHttpTransport(opts) {
2807
3572
  const serverUrl = opts.serverUrl.replace(/\/$/, "");
2808
3573
  return async (logs) => {
@@ -2989,36 +3754,58 @@ createBackgroundLogForwarder({
2989
3754
  }).then((f) => {
2990
3755
  logForwarder = f;
2991
3756
  });
2992
- var socket = null;
2993
- var SEND_QUEUE_LIMIT = 500;
2994
- var sendQueue = [];
2995
- var queuedDropped = 0;
2996
- function isFullSnapshot(payload) {
2997
- return payload?.type === "rrweb" && payload?.payload?.type === 2;
2998
- }
2999
- function enqueueRecordingEvent(payload) {
3000
- sendQueue.push({ raw: JSON.stringify(payload), isSnapshot: isFullSnapshot(payload) });
3001
- while (sendQueue.length > SEND_QUEUE_LIMIT) {
3002
- const idx = sendQueue.findIndex((item) => !item.isSnapshot);
3003
- if (idx === -1) break;
3004
- sendQueue.splice(idx, 1);
3005
- queuedDropped++;
3006
- }
3007
- }
3008
- function flushSendQueue() {
3009
- if (!socket || socket.readyState !== WebSocket.OPEN || !sendQueue.length) return;
3010
- while (sendQueue.length) {
3011
- 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
+ }
3012
3776
  try {
3013
- socket.send(item.raw);
3014
- } catch (e) {
3015
- console.error("[HunterMate] flush send failed", e);
3016
- 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();
3017
3799
  }
3800
+ } catch (err3) {
3801
+ console.error("[HunterMate] ensureOffscreen failed", err3);
3802
+ sendLog("error", "ensureOffscreen failed", { error: err3 instanceof Error ? err3.message : String(err3) });
3018
3803
  }
3019
- if (queuedDropped > 0) {
3020
- console.warn(`[HunterMate] send queue overflow, dropped ${queuedDropped} incremental events (snapshots kept)`);
3021
- queuedDropped = 0;
3804
+ try {
3805
+ const st = await chrome.runtime.sendMessage({ type: "offscreen:get-state" });
3806
+ state.wsConnected = !!st?.wsConnected;
3807
+ broadcastState();
3808
+ } catch {
3022
3809
  }
3023
3810
  }
3024
3811
  function sendLog(level, message, meta) {
@@ -3026,14 +3813,7 @@ function sendLog(level, message, meta) {
3026
3813
  backgroundLogger[level === "debug" ? "debug" : level]("extension:sendLog", { message, meta });
3027
3814
  } catch {
3028
3815
  }
3029
- const ws = socket;
3030
- if (ws?.readyState === WebSocket.OPEN) {
3031
- try {
3032
- ws.send(JSON.stringify({ type: "log", level, message, meta }));
3033
- } catch (e) {
3034
- console.error("[HunterMate] sendLog failed", e);
3035
- }
3036
- }
3816
+ sendToOffscreen(JSON.stringify({ type: "log", level, message, meta }));
3037
3817
  const prefix = `[HunterMate] ${message}`;
3038
3818
  if (level === "debug") {
3039
3819
  console.debug(prefix, meta ?? "");
@@ -3106,99 +3886,72 @@ function broadcastState() {
3106
3886
  });
3107
3887
  });
3108
3888
  }
3109
- function connect() {
3110
- if (socket) return;
3111
- socket = new WebSocket(WS_URL);
3112
- socket.onopen = () => {
3113
- state.wsConnected = true;
3114
- console.log("[HunterMate] connected to local server");
3115
- socket?.send(
3116
- JSON.stringify({
3117
- type: "hello",
3118
- extensionId: chrome.runtime.id,
3119
- version: chrome.runtime.getManifest().version,
3120
- capabilities: ["cp.*", "hm.*"]
3121
- })
3122
- );
3123
- flushSendQueue();
3124
- broadcastState();
3125
- backgroundLogger.info("ws:connected", { extensionId: chrome.runtime.id });
3126
- };
3127
- socket.onclose = () => {
3128
- socket = null;
3129
- state.wsConnected = false;
3130
- broadcastState();
3131
- backgroundLogger.warn("ws:closed", { reconnectDelayMs: 5e3 });
3132
- setTimeout(connect, 5e3);
3133
- };
3134
- socket.onerror = (err3) => {
3135
- backgroundLogger.error("ws:error", { message: String(err3) });
3136
- };
3137
- socket.onmessage = async (event) => {
3138
- let data;
3139
- try {
3140
- data = JSON.parse(event.data);
3141
- } catch {
3142
- return;
3143
- }
3144
- if (data.type === "pong" || data.type === "status" || data.type === "ack") return;
3145
- if (data.type === "reload") {
3146
- console.log("[HunterMate] received reload broadcast, reloading extension in 200ms...");
3147
- setTimeout(() => chrome.runtime.reload(), 200);
3148
- return;
3149
- }
3150
- if (data.type === "cmd" && data.requestId) {
3151
- const startedAt = Date.now();
3152
- sendLog("info", "extension handling control command", { command: data.command, requestId: data.requestId, params: data.params });
3153
- let outcome;
3154
- if (data.command === "hm.status") {
3155
- const manifest = chrome.runtime.getManifest();
3156
- outcome = {
3157
- status: "ok",
3158
- result: {
3159
- ...state,
3160
- tabStatesCount: tabStates.size,
3161
- extBasePath: chrome.runtime.getURL(""),
3162
- manifestPermissions: manifest.permissions,
3163
- hasSidePanelInManifest: manifest.permissions?.includes("sidePanel"),
3164
- chromeSidePanelType: typeof chrome.sidePanel
3165
- }
3166
- };
3167
- } else {
3168
- try {
3169
- outcome = await executeControlCommand(String(data.command ?? ""), data.params ?? {}, {
3170
- onLog: (level, message, meta) => sendLog(level, message, meta),
3171
- sessionId: typeof data.sessionId === "string" ? data.sessionId : void 0
3172
- });
3173
- } catch (e) {
3174
- outcome = { status: "error", message: e instanceof Error ? e.message : String(e) };
3175
- }
3176
- }
3177
- const resp = outcome.status === "ok" ? {
3178
- type: "cmd_response",
3179
- 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 = {
3180
3904
  status: "ok",
3181
- result: outcome.result,
3182
- durationMs: Date.now() - startedAt
3183
- } : {
3184
- type: "cmd_response",
3185
- requestId: data.requestId,
3186
- status: "error",
3187
- error: { code: outcome.code, message: outcome.message },
3188
- 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
+ }
3189
3914
  };
3915
+ } else {
3190
3916
  try {
3191
- socket?.send(JSON.stringify(resp));
3192
- 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
+ });
3193
3921
  } catch (e) {
3194
- sendLog("error", "cmd_response send failed", { requestId: data.requestId, error: e instanceof Error ? e.message : String(e) });
3922
+ outcome = { status: "error", message: e instanceof Error ? e.message : String(e) };
3195
3923
  }
3196
- return;
3197
3924
  }
3198
- console.log("[HunterMate] server message", data);
3199
- };
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
+ }
3200
3943
  }
3201
- 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
+ });
3202
3955
  var RECONNECT_ALARM_NAME = "hm:reconnect";
3203
3956
  function ensureReconnectAlarm() {
3204
3957
  chrome.alarms.create(RECONNECT_ALARM_NAME, { periodInMinutes: 0.5 }).catch((err3) => console.error("[HunterMate] create reconnect alarm failed", err3));
@@ -3206,21 +3959,24 @@ function ensureReconnectAlarm() {
3206
3959
  chrome.alarms.onAlarm.addListener((alarm) => {
3207
3960
  if (alarm.name !== RECONNECT_ALARM_NAME) return;
3208
3961
  checkHttpHealth();
3209
- if (socket && socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) {
3210
- socket = null;
3211
- }
3212
- if (!socket) {
3213
- connect();
3214
- }
3962
+ sendLog("debug", "sw:alarm-tick", { wsConnected: state.wsConnected });
3963
+ void ensureOffscreen();
3215
3964
  });
3216
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
+ });
3217
3977
  setInterval(() => {
3218
3978
  checkHttpHealth();
3219
- if (socket?.readyState === WebSocket.OPEN) {
3220
- socket.send(JSON.stringify({ type: "ping" }));
3221
- } else {
3222
- connect();
3223
- }
3979
+ void ensureOffscreen();
3224
3980
  broadcastState();
3225
3981
  }, 2e4);
3226
3982
  setInterval(() => {
@@ -3305,11 +4061,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
3305
4061
  if (message.type === "recording-event") {
3306
4062
  const tabId = sender.tab?.id ?? -1;
3307
4063
  const payload = { ...message.payload, tabId };
3308
- if (socket?.readyState === WebSocket.OPEN) {
3309
- socket.send(JSON.stringify(payload));
3310
- } else {
3311
- enqueueRecordingEvent(payload);
3312
- }
4064
+ sendToOffscreen(JSON.stringify(payload));
3313
4065
  const ts = tabStates.get(tabId);
3314
4066
  if (ts && message.payload?.sessionId === ts.recordingSessionId) {
3315
4067
  ts.eventCount++;