hunter-mate 0.1.7 → 0.1.11

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 (239) hide show
  1. package/dist/assets/extension/background/service-worker.js +808 -153
  2. package/dist/assets/extension/build-info.json +1 -0
  3. package/dist/assets/extension/content/index.js +23 -5
  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 +17 -1
  38. package/dist/assets/server/extension/extension-control.controller.js +68 -12
  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/extension/extension.module.js +2 -1
  44. package/dist/assets/server/extension/extension.module.js.map +1 -1
  45. package/dist/assets/server/extraction/adapter.service.d.ts +3 -0
  46. package/dist/assets/server/extraction/adapter.service.js +127 -6
  47. package/dist/assets/server/extraction/adapter.service.js.map +1 -1
  48. package/dist/assets/server/extraction/anomaly-report.service.js +1 -1
  49. package/dist/assets/server/extraction/anomaly-report.service.js.map +1 -1
  50. package/dist/assets/server/extraction/entities/adapter-rule.entity.d.ts +2 -0
  51. package/dist/assets/server/extraction/entities/adapter-rule.entity.js +10 -0
  52. package/dist/assets/server/extraction/entities/adapter-rule.entity.js.map +1 -1
  53. package/dist/assets/server/extraction/extraction-entity.controller.d.ts +21 -3
  54. package/dist/assets/server/extraction/extraction-entity.controller.js +44 -9
  55. package/dist/assets/server/extraction/extraction-entity.controller.js.map +1 -1
  56. package/dist/assets/server/extraction/extraction.module.js +13 -3
  57. package/dist/assets/server/extraction/extraction.module.js.map +1 -1
  58. package/dist/assets/server/extraction/extraction.service.d.ts +4 -1
  59. package/dist/assets/server/extraction/extraction.service.js +71 -13
  60. package/dist/assets/server/extraction/extraction.service.js.map +1 -1
  61. package/dist/assets/server/extraction/harness-overlay.service.d.ts +41 -0
  62. package/dist/assets/server/extraction/harness-overlay.service.js +319 -0
  63. package/dist/assets/server/extraction/harness-overlay.service.js.map +1 -0
  64. package/dist/assets/server/extraction/liepin-extractor.d.ts +1 -0
  65. package/dist/assets/server/extraction/liepin-extractor.js +172 -0
  66. package/dist/assets/server/extraction/liepin-extractor.js.map +1 -0
  67. package/dist/assets/server/extraction/script-rule.js +14 -2
  68. package/dist/assets/server/extraction/script-rule.js.map +1 -1
  69. package/dist/assets/server/governance/action-audit.entity.d.ts +12 -0
  70. package/dist/assets/server/governance/action-audit.entity.js +66 -0
  71. package/dist/assets/server/governance/action-audit.entity.js.map +1 -0
  72. package/dist/assets/server/governance/governance.command-recorder.d.ts +17 -0
  73. package/dist/assets/server/governance/governance.command-recorder.js +98 -0
  74. package/dist/assets/server/governance/governance.command-recorder.js.map +1 -0
  75. package/dist/assets/server/governance/governance.controller.d.ts +14 -0
  76. package/dist/assets/server/governance/governance.controller.js +77 -0
  77. package/dist/assets/server/governance/governance.controller.js.map +1 -0
  78. package/dist/assets/server/governance/governance.ingester.d.ts +21 -0
  79. package/dist/assets/server/governance/governance.ingester.js +117 -0
  80. package/dist/assets/server/governance/governance.ingester.js.map +1 -0
  81. package/dist/assets/server/governance/governance.module.d.ts +2 -0
  82. package/dist/assets/server/governance/governance.module.js +27 -0
  83. package/dist/assets/server/governance/governance.module.js.map +1 -0
  84. package/dist/assets/server/governance/governance.service.d.ts +122 -0
  85. package/dist/assets/server/governance/governance.service.js +398 -0
  86. package/dist/assets/server/governance/governance.service.js.map +1 -0
  87. package/dist/assets/server/jobcase/jobcase.controller.d.ts +1 -0
  88. package/dist/assets/server/jobcase/jobcase.entity.d.ts +1 -0
  89. package/dist/assets/server/jobcase/jobcase.entity.js +6 -0
  90. package/dist/assets/server/jobcase/jobcase.entity.js.map +1 -1
  91. package/dist/assets/server/jobcase/jobcase.service.d.ts +2 -0
  92. package/dist/assets/server/jobcase/jobcase.service.js +2 -1
  93. package/dist/assets/server/jobcase/jobcase.service.js.map +1 -1
  94. package/dist/assets/server/main.js +1 -0
  95. package/dist/assets/server/main.js.map +1 -1
  96. package/dist/assets/server/package.json +1 -1
  97. package/dist/assets/server/preference/preference.controller.d.ts +14 -0
  98. package/dist/assets/server/preference/preference.controller.js +66 -0
  99. package/dist/assets/server/preference/preference.controller.js.map +1 -0
  100. package/dist/assets/server/preference/preference.entity.d.ts +13 -0
  101. package/dist/assets/server/preference/preference.entity.js +77 -0
  102. package/dist/assets/server/preference/preference.entity.js.map +1 -0
  103. package/dist/assets/server/preference/preference.module.d.ts +2 -0
  104. package/dist/assets/server/preference/preference.module.js +26 -0
  105. package/dist/assets/server/preference/preference.module.js.map +1 -0
  106. package/dist/assets/server/preference/preference.service.d.ts +33 -0
  107. package/dist/assets/server/preference/preference.service.js +122 -0
  108. package/dist/assets/server/preference/preference.service.js.map +1 -0
  109. package/dist/assets/server/recording/recording-event-queue.service.js +1 -1
  110. package/dist/assets/server/recording/recording-event-queue.service.js.map +1 -1
  111. package/dist/assets/server/recording/recording-retention.service.d.ts +19 -2
  112. package/dist/assets/server/recording/recording-retention.service.js +95 -6
  113. package/dist/assets/server/recording/recording-retention.service.js.map +1 -1
  114. package/dist/assets/server/recording/recording-snapshot.service.d.ts +1 -0
  115. package/dist/assets/server/recording/recording-snapshot.service.js +4 -0
  116. package/dist/assets/server/recording/recording-snapshot.service.js.map +1 -1
  117. package/dist/assets/server/recording/recording.controller.d.ts +10 -0
  118. package/dist/assets/server/recording/recording.controller.js +19 -0
  119. package/dist/assets/server/recording/recording.controller.js.map +1 -1
  120. package/dist/assets/server/recording/recording.gateway.js +15 -0
  121. package/dist/assets/server/recording/recording.gateway.js.map +1 -1
  122. package/dist/assets/server/recording/recording.module.js +6 -1
  123. package/dist/assets/server/recording/recording.module.js.map +1 -1
  124. package/dist/assets/server/recording/recording.service.d.ts +5 -0
  125. package/dist/assets/server/recording/recording.service.js +10 -0
  126. package/dist/assets/server/recording/recording.service.js.map +1 -1
  127. package/dist/assets/server/remark/remark.controller.d.ts +1 -1
  128. package/dist/assets/server/remark/remark.controller.js +1 -10
  129. package/dist/assets/server/remark/remark.controller.js.map +1 -1
  130. package/dist/assets/server/remark/remark.entity.d.ts +1 -1
  131. package/dist/assets/server/remark/remark.entity.js.map +1 -1
  132. package/dist/assets/server/site/entities/site-registry.entity.d.ts +1 -1
  133. package/dist/assets/server/site/entities/site-registry.entity.js +1 -1
  134. package/dist/assets/server/site/entities/site-registry.entity.js.map +1 -1
  135. package/dist/assets/server/site/site.controller.d.ts +1 -1
  136. package/dist/assets/server/site/site.service.d.ts +5 -1
  137. package/dist/assets/server/site/site.service.js +13 -0
  138. package/dist/assets/server/site/site.service.js.map +1 -1
  139. package/dist/assets/server/system/system.controller.d.ts +1 -0
  140. package/dist/assets/server/system/system.controller.js +1 -1
  141. package/dist/assets/server/system/system.controller.js.map +1 -1
  142. package/dist/assets/server/system/system.service.d.ts +1 -0
  143. package/dist/assets/server/system/system.service.js +3 -2
  144. package/dist/assets/server/system/system.service.js.map +1 -1
  145. package/dist/cache/db.js +6 -6
  146. package/dist/commands/addons.js +2 -1
  147. package/dist/commands/cache.js +1 -1
  148. package/dist/commands/case.d.ts +67 -0
  149. package/dist/commands/case.js +101 -0
  150. package/dist/commands/config.d.ts +4 -0
  151. package/dist/commands/config.js +8 -1
  152. package/dist/commands/customer.d.ts +25 -0
  153. package/dist/commands/customer.js +137 -0
  154. package/dist/commands/doctor.js +33 -9
  155. package/dist/commands/extension.js +21 -16
  156. package/dist/commands/extras.d.ts +18 -3
  157. package/dist/commands/extras.js +162 -5
  158. package/dist/commands/init.d.ts +1 -1
  159. package/dist/commands/init.js +3 -4
  160. package/dist/commands/install.d.ts +5 -0
  161. package/dist/commands/install.js +22 -42
  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 +2 -1
  165. package/dist/commands/job.js +25 -5
  166. package/dist/commands/login.d.ts +4 -0
  167. package/dist/commands/login.js +57 -4
  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 +64 -13
  173. package/dist/commands/preference.d.ts +6 -0
  174. package/dist/commands/preference.js +80 -0
  175. package/dist/commands/record.js +1 -1
  176. package/dist/commands/report.js +1 -1
  177. package/dist/commands/search.js +1 -1
  178. package/dist/commands/server.d.ts +12 -0
  179. package/dist/commands/server.js +182 -18
  180. package/dist/commands/status.d.ts +4 -0
  181. package/dist/commands/status.js +16 -6
  182. package/dist/commands/update.js +7 -8
  183. package/dist/config.d.ts +21 -0
  184. package/dist/config.js +27 -0
  185. package/dist/extension-sync.d.ts +35 -0
  186. package/dist/extension-sync.js +159 -0
  187. package/dist/i18n/locales/en-US.json +17 -1
  188. package/dist/i18n/locales/zh-CN.json +17 -1
  189. package/dist/index.js +17 -0
  190. package/dist/keychain.d.ts +16 -1
  191. package/dist/keychain.js +43 -16
  192. package/dist/logger.js +2 -2
  193. package/dist/postinstall.d.ts +1 -0
  194. package/dist/postinstall.js +31 -0
  195. package/dist/updater.js +2 -2
  196. package/dist/utils/artifact-checksum.d.ts +4 -0
  197. package/dist/utils/artifact-checksum.js +40 -0
  198. package/dist/utils/command-error.d.ts +4 -2
  199. package/dist/utils/command-error.js +19 -3
  200. package/dist/utils/link.d.ts +18 -0
  201. package/dist/utils/link.js +83 -0
  202. package/dist/utils/local-http.d.ts +7 -0
  203. package/dist/utils/local-http.js +30 -0
  204. package/dist/version.js +86 -49
  205. package/dist/watchdog.d.ts +8 -0
  206. package/dist/watchdog.js +49 -9
  207. package/dist/web/dist/404.html +1 -1
  208. package/dist/web/dist/_next/static/chunks/app/logs/page-d478031d49f7ba00.js +1 -0
  209. package/dist/web/dist/_next/static/chunks/{main-app-80b038a312917d90.js → main-app-0af848156a5b13fc.js} +1 -1
  210. package/dist/web/dist/_next/static/chunks/pages/{_app-da7d628f49271ece.js → _app-3d844792ec91e4d1.js} +2 -2
  211. package/dist/web/dist/auth/verify.html +1 -1
  212. package/dist/web/dist/auth/verify.txt +1 -1
  213. package/dist/web/dist/companies.html +1 -1
  214. package/dist/web/dist/companies.txt +1 -1
  215. package/dist/web/dist/entity.html +1 -1
  216. package/dist/web/dist/entity.txt +1 -1
  217. package/dist/web/dist/index.html +1 -1
  218. package/dist/web/dist/index.txt +1 -1
  219. package/dist/web/dist/jobs.html +1 -1
  220. package/dist/web/dist/jobs.txt +1 -1
  221. package/dist/web/dist/logs.html +1 -1
  222. package/dist/web/dist/logs.txt +2 -2
  223. package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -1
  224. package/dist/web/dist/matching/jobs/placeholder/talents.txt +1 -1
  225. package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -1
  226. package/dist/web/dist/matching/portraits/placeholder/jobs.txt +1 -1
  227. package/dist/web/dist/parselog.html +1 -1
  228. package/dist/web/dist/parselog.txt +1 -1
  229. package/dist/web/dist/recordings.html +1 -1
  230. package/dist/web/dist/recordings.txt +1 -1
  231. package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -1
  232. package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +1 -1
  233. package/dist/web/dist/talents.html +1 -1
  234. package/dist/web/dist/talents.txt +1 -1
  235. package/package.json +6 -4
  236. package/scripts/postinstall-run.mjs +14 -0
  237. package/dist/web/dist/_next/static/chunks/app/logs/page-9f41d44fa1add723.js +0 -1
  238. /package/dist/web/dist/_next/static/{hqJ-7vr4X71DNfzZugHJ- → 3bVkK4pAgbbmp5r0ThyBL}/_buildManifest.js +0 -0
  239. /package/dist/web/dist/_next/static/{hqJ-7vr4X71DNfzZugHJ- → 3bVkK4pAgbbmp5r0ThyBL}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
1
  // src/shared/messaging.ts
2
- var SERVER_PORT = 34570;
2
+ var SERVER_PORT = 34571;
3
3
  var WS_URL = `ws://localhost:${SERVER_PORT}/ws`;
4
4
  var SERVER_HTTP_URL = `http://localhost:${SERVER_PORT}`;
5
5
 
@@ -505,6 +505,19 @@ function runControl(command, args) {
505
505
  }
506
506
  function goto(gotoArgs) {
507
507
  if (!gotoArgs.url) return { error: "url_required" };
508
+ let sameUrl = false;
509
+ try {
510
+ sameUrl = new URL(gotoArgs.url, window.location.href).href === window.location.href;
511
+ } catch {
512
+ sameUrl = false;
513
+ }
514
+ if (sameUrl) {
515
+ if (gotoArgs.reload === true) {
516
+ window.location.reload();
517
+ return { navigated: true, reloaded: true, url: gotoArgs.url };
518
+ }
519
+ return { navigated: false, reused: true, url: gotoArgs.url };
520
+ }
508
521
  window.location.href = gotoArgs.url;
509
522
  return { navigated: true, url: gotoArgs.url };
510
523
  }
@@ -986,13 +999,32 @@ globalThis.__cpRunControl = runControl;
986
999
  globalThis.__cpRuntimeVersion = RUNTIME_VERSION;
987
1000
 
988
1001
  // src/generated/control-runtime-source.ts
989
- var control_runtime_source_default = '"use strict";\n(() => {\n // src/shared/control-runtime.ts\n function getRefStore() {\n const g = globalThis;\n if (!g.__cpRefs) {\n g.__cpRefs = { counter: 1, refs: /* @__PURE__ */ new Map(), cssPaths: /* @__PURE__ */ new Map() };\n }\n return g.__cpRefs;\n }\n function allocRef(el) {\n const store = getRefStore();\n for (const [ref2, existing] of store.refs) {\n if (existing === el) return ref2;\n }\n const ref = store.counter++;\n store.refs.set(ref, el);\n store.cssPaths.set(ref, uniqueCssPath(el));\n return ref;\n }\n function uniqueCssPath(el) {\n if (el.id) return `#${CSS.escape(el.id)}`;\n const parts = [];\n let cur = el;\n while (cur && cur !== document.body && cur !== document.documentElement) {\n let sel = cur.tagName.toLowerCase();\n if (cur.id) {\n parts.unshift(`#${CSS.escape(cur.id)}`);\n break;\n }\n const parent = cur.parentElement;\n if (parent) {\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);\n if (sameTag.length > 1) {\n const idx = sameTag.indexOf(cur) + 1;\n sel += `:nth-of-type(${idx})`;\n }\n }\n parts.unshift(sel);\n cur = parent;\n }\n return parts.join(" > ");\n }\n function err(code, message, extra) {\n return { error: { code, message, ...extra || {} } };\n }\n var ERROR_CODES = {\n NOT_FOUND: "not_found",\n STALE_REF: "stale_ref",\n INVALID_SELECTOR: "invalid_selector",\n SELECTOR_NOT_FOUND: "selector_not_found",\n SELECTOR_AMBIGUOUS: "selector_ambiguous",\n SELECTOR_NTH_OUT_OF_RANGE: "selector_nth_out_of_range",\n OPTION_NOT_FOUND: "option_not_found",\n NOT_A_SELECT: "not_a_select"\n };\n var IMPLICIT_ROLE = {\n a: "link",\n button: "button",\n select: "combobox",\n textarea: "textbox",\n h1: "heading",\n h2: "heading",\n h3: "heading",\n h4: "heading",\n h5: "heading",\n h6: "heading",\n img: "img",\n nav: "navigation",\n ul: "list",\n ol: "list",\n li: "listitem",\n main: "main",\n article: "article",\n aside: "complementary",\n header: "banner",\n footer: "contentinfo",\n form: "form",\n table: "table",\n summary: "button",\n option: "option",\n output: "status"\n };\n function roleOf(el) {\n const explicit = el.getAttribute("role");\n if (explicit) return explicit.split(/\\s+/)[0];\n const tag = el.tagName.toLowerCase();\n if (tag === "input") {\n const type = el.type || "text";\n if (type === "hidden") return null;\n if (type === "checkbox") return "checkbox";\n if (type === "radio") return "radio";\n if (type === "button" || type === "submit" || type === "reset") return "button";\n if (type === "range") return "slider";\n return "textbox";\n }\n if (tag === "progress") return "progressbar";\n if (tag === "meter") return "meter";\n return IMPLICIT_ROLE[tag] ?? null;\n }\n function accessibleName(el) {\n const aria = el.getAttribute("aria-label");\n if (aria) return aria.trim();\n const labelledby = el.getAttribute("aria-labelledby");\n if (labelledby) {\n const parts = labelledby.split(/\\s+/).map((id2) => document.getElementById(id2)?.textContent?.trim() || "");\n const joined = parts.filter(Boolean).join(" ");\n if (joined) return joined.slice(0, 120);\n }\n const id = el.getAttribute("id");\n if (id) {\n const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);\n if (label?.textContent?.trim()) return label.textContent.trim().slice(0, 120);\n }\n if (el.tagName.toLowerCase() === "input") {\n const type = el.type;\n if (type === "button" || type === "submit" || type === "reset") {\n const v = el.value;\n if (v) return v;\n }\n }\n const alt = el.getAttribute("alt");\n if (alt) return alt;\n const title = el.getAttribute("title");\n if (title) return title;\n const placeholder = el.getAttribute("placeholder");\n if (placeholder) return placeholder;\n return (el.textContent || "").replace(/\\s+/g, " ").trim().slice(0, 120);\n }\n function isVisible(el) {\n if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) return false;\n const html = el;\n if (html.hasAttribute("hidden")) return false;\n if (html.closest("[hidden]")) return false;\n const style = window.getComputedStyle(html);\n if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;\n if (el instanceof HTMLElement && el.offsetParent === null) {\n const pos = style.position;\n if (pos !== "fixed" && pos !== "sticky") return false;\n }\n return true;\n }\n var INTERACTIVE_SELECTOR = "a[href], button, input, textarea, select, summary, [role], [contenteditable=\'\'], [contenteditable=\'true\'], [onclick], [tabindex]";\n function isInteractive(el) {\n return el.matches(INTERACTIVE_SELECTOR);\n }\n function testidSelector(testid) {\n return `[data-testid="${CSS.escape(testid)}"], [data-test-id="${CSS.escape(testid)}"], [data-test="${CSS.escape(testid)}"], [data-cy="${CSS.escape(testid)}"]`;\n }\n function resolveTargetRich(sel) {\n const targetDesc = sel.ref != null ? String(sel.ref) : sel.css ?? (sel.role || sel.name || sel.label || sel.testid || sel.text ? `${sel.role ? `role=${sel.role} ` : ""}${sel.name ? `name=${sel.name} ` : ""}${sel.label ? `label=${sel.label} ` : ""}${sel.testid ? `testid=${sel.testid} ` : ""}${sel.text ? `text=${sel.text}` : ""}`.trim() : "");\n if (sel.ref != null) {\n const store = getRefStore();\n const el = store.refs.get(sel.ref);\n if (el && document.contains(el) && isVisible(el)) {\n return { el, matches_n: 1, match_level: "exact", target: targetDesc };\n }\n if (el && !document.contains(el)) {\n const cssPath = store.cssPaths.get(sel.ref);\n if (cssPath) {\n const re = document.querySelector(cssPath);\n if (re && isVisible(re)) {\n store.refs.set(sel.ref, re);\n return { el: re, matches_n: 1, match_level: "reidentified", target: targetDesc };\n }\n }\n return { errorEnvelope: err(ERROR_CODES.STALE_REF, "Numeric ref is no longer in the DOM. Re-run state.") };\n }\n return { errorEnvelope: err(ERROR_CODES.NOT_FOUND, "Numeric ref is no longer in the DOM. Re-run state.") };\n }\n let candidates = [];\n if (sel.css) {\n try {\n candidates = Array.from(document.querySelectorAll(sel.css));\n } catch {\n return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: sel.css }) };\n }\n if (candidates.length === 0) {\n return { errorEnvelope: err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: sel.css, candidates: suggestCandidates() }) };\n }\n } else if (sel.testid) {\n try {\n candidates = Array.from(document.querySelectorAll(testidSelector(sel.testid)));\n } catch {\n return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.", { testid: sel.testid }) };\n }\n } else if (sel.role || sel.name || sel.label || sel.text) {\n const wantRole = sel.role ? sel.role.toLowerCase() : null;\n const wantName = sel.name ? sel.name.toLowerCase() : null;\n const wantText = sel.text ? sel.text.toLowerCase() : null;\n let labelTargets = [];\n if (sel.label) {\n const lower = sel.label.toLowerCase();\n labelTargets = Array.from(document.querySelectorAll("label")).filter((l) => (l.textContent || "").trim().toLowerCase().includes(lower)).map((l) => {\n const forId = l.getAttribute("for");\n if (forId) return document.getElementById(forId);\n return l.querySelector("input, select, textarea");\n }).filter((x) => !!x);\n }\n const all = Array.from(document.querySelectorAll("*"));\n candidates = all.filter((el) => {\n if (!isVisible(el)) return false;\n const role = roleOf(el);\n if (wantRole && (!role || role.toLowerCase() !== wantRole)) return false;\n if (wantName) {\n const name = accessibleName(el).toLowerCase();\n if (!name.includes(wantName)) return false;\n }\n if (wantText) {\n const t = (el.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();\n if (!t.includes(wantText)) return false;\n }\n return true;\n });\n if (labelTargets.length) {\n const set = new Set(labelTargets);\n candidates = labelTargets.concat(candidates.filter((c) => !set.has(c)));\n }\n if (candidates.length === 0) {\n return {\n errorEnvelope: err(\n ERROR_CODES.SELECTOR_NOT_FOUND,\n "Semantic locator matches 0 elements.",\n { role: sel.role, name: sel.name, label: sel.label, text: sel.text, candidates: suggestCandidates() }\n )\n };\n }\n } else {\n return { errorEnvelope: err("no_target", "Provide a numeric ref, CSS selector, or semantic locator (--role/--name/--label/--text/--testid).") };\n }\n const visible = candidates.filter(isVisible);\n const pool = visible.length ? visible : candidates;\n if (sel.nth != null) {\n if (sel.nth < 0 || sel.nth >= pool.length) {\n return { errorEnvelope: err(ERROR_CODES.SELECTOR_NTH_OUT_OF_RANGE, `--nth ${sel.nth} beyond match count ${pool.length}.`) };\n }\n const el = pool[sel.nth];\n return { el, matches_n: pool.length, match_level: "reidentified", target: targetDesc };\n }\n if (pool.length > 1) {\n return {\n errorEnvelope: err(ERROR_CODES.SELECTOR_AMBIGUOUS, `CSS matches ${pool.length} elements and no --nth given.`, {\n matches_n: pool.length,\n candidates: pool.slice(0, 5).map((el) => uniqueCssPath(el))\n })\n };\n }\n return { el: pool[0], matches_n: 1, match_level: "reidentified", target: targetDesc };\n }\n function suggestCandidates() {\n const store = getRefStore();\n const recent = Array.from(store.refs.entries()).slice(-5).map(([, el]) => uniqueCssPath(el));\n return recent;\n }\n function compoundOf(el) {\n const tag = el.tagName.toLowerCase();\n if (tag === "input") {\n const input = el;\n const type = input.type;\n if (["date", "time", "datetime-local", "month", "week"].includes(type)) {\n const formatMap = {\n date: "YYYY-MM-DD",\n time: "HH:mm",\n "datetime-local": "YYYY-MM-DDTHH:mm",\n month: "YYYY-MM",\n week: "YYYY-Www"\n };\n return {\n control: type,\n format: formatMap[type] ?? type,\n current: input.value || "",\n min: input.min || null,\n max: input.max || null\n };\n }\n if (type === "file") {\n const current = Array.from(input.files ?? []).map((f) => f.name);\n return { control: "file", multiple: input.multiple, current, accept: input.accept || null };\n }\n return null;\n }\n if (tag === "select") {\n const select = el;\n const options = Array.from(select.options).slice(0, 50).map((o) => ({\n label: (o.textContent || "").trim(),\n value: o.value,\n selected: o.selected\n }));\n return {\n control: "select",\n multiple: select.multiple,\n current: select.selectedOptions[0] ? (select.selectedOptions[0].textContent || "").trim() : "",\n options,\n options_total: select.options.length\n };\n }\n return null;\n }\n var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "meta", "link", "head", "base", "template", "path", "circle", "rect", "g", "defs", "polygon", "line"]);\n function describeElement(el, ref) {\n const tag = el.tagName.toLowerCase();\n const role = el.getAttribute("role") ?? null;\n const name = accessibleName(el);\n const parts = [];\n if (ref != null) parts.push(`[${ref}]`);\n parts.push(tag);\n if (role && role !== IMPLICIT_ROLE[tag]) parts.push(`role=${role}`);\n if (tag === "input") {\n const input = el;\n if (input.type && input.type !== "text") parts.push(`[${input.type}]`);\n if (input.disabled) parts.push("(disabled)");\n }\n if (name) parts.push(JSON.stringify(name.length > 60 ? name.slice(0, 57) + "\\u2026" : name));\n if (tag === "a") {\n const href = el.getAttribute("href");\n if (href) parts.push(`\\u2192 ${href.slice(0, 50)}`);\n }\n if (el.disabled) parts.push("(disabled)");\n return parts.join(" ");\n }\n function buildStateTree() {\n const lines = [];\n const compounds = {};\n const budget = { nodes: 0, chars: 0, truncated: false };\n const MAX_NODES = 400;\n const MAX_CHARS = 9e3;\n let hiddenInteractive = 0;\n function emit(depth, text) {\n const line = " ".repeat(Math.min(depth, 12)) + text;\n lines.push(line);\n budget.chars += line.length;\n }\n function walk(el, depth) {\n if (budget.nodes >= MAX_NODES || budget.chars >= MAX_CHARS) {\n budget.truncated = true;\n return;\n }\n const tag = el.tagName.toLowerCase();\n if (SKIP_TAGS.has(tag)) return;\n budget.nodes++;\n const visible = isVisible(el);\n const interactive = isInteractive(el);\n if (interactive) {\n if (visible) {\n const ref = allocRef(el);\n emit(depth, describeElement(el, ref));\n const compound = compoundOf(el);\n if (compound) compounds[String(ref)] = compound;\n } else {\n hiddenInteractive++;\n return;\n }\n } else if (visible) {\n if (/^h[1-6]$/.test(tag)) {\n const t = accessibleName(el);\n if (t) emit(depth, `${"#".repeat(Number(tag[1]))} ${t}`);\n } else if (tag === "p" || tag === "li" || tag === "td" || tag === "th" || tag === "figcaption" || tag === "blockquote") {\n const ownText = ownTextOf(el);\n if (ownText) emit(depth, ownText.slice(0, 100));\n } else if (tag === "img") {\n const alt = el.getAttribute("alt") || "";\n if (alt) emit(depth, `img ${JSON.stringify(alt.slice(0, 60))}`);\n }\n }\n if (depth < 14) {\n for (const child of Array.from(el.children)) {\n walk(child, depth + 1);\n if (budget.truncated) return;\n }\n }\n }\n function ownTextOf(el) {\n let text = "";\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) text += node.textContent || "";\n }\n return text.replace(/\\s+/g, " ").trim();\n }\n const root = document.querySelector("main") || document.querySelector("#root") || document.body;\n emit(0, `# ${document.title}`);\n walk(root, 0);\n const scrollable = document.documentElement.scrollHeight > window.innerHeight + 200;\n if (scrollable) {\n emit(0, "\\u2195 page is scrollable \\u2014 run cp.scroll down to reveal more");\n }\n if (hiddenInteractive > 0) {\n emit(0, `\\u2139 ${hiddenInteractive} hidden interactive element(s) not tagged`);\n }\n if (budget.truncated) {\n emit(0, "\\u2026 (snapshot truncated by budget \\u2014 narrow with a selector or scroll)");\n }\n return {\n url: location.href,\n title: document.title,\n text: lines.join("\\n"),\n compounds,\n hints: { scroll: scrollable, hidden_interactive: hiddenInteractive },\n truncated: budget.truncated\n };\n }\n function runControl(command, args) {\n const a = args || {};\n function getVisibleText(el) {\n return (el.textContent || "").trim().replace(/\\s+/g, " ").slice(0, 120);\n }\n function getAttrs(el) {\n const attrs = {};\n for (const name of ["id", "class", "name", "type", "href", "src", "placeholder", "value", "role", "aria-label", "data-testid"]) {\n attrs[name] = el.getAttribute(name);\n }\n return attrs;\n }\n function sel() {\n return {\n ref: a.ref,\n css: a.css,\n nth: a.nth,\n role: a.role,\n name: a.name,\n label: a.label,\n text: a.text,\n testid: a.testid\n };\n }\n function findElements(findArgs) {\n const limit = findArgs.limit ?? 20;\n const textMax = findArgs["text-max"] ?? 120;\n let elements = [];\n if (findArgs.css) {\n try {\n elements = Array.from(document.querySelectorAll(findArgs.css));\n } catch {\n return err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: findArgs.css });\n }\n } else if (findArgs.testid) {\n try {\n elements = Array.from(document.querySelectorAll(testidSelector(findArgs.testid)));\n } catch {\n return err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.");\n }\n } else if (findArgs.role || findArgs.name || findArgs.label) {\n const r = resolveTargetRich({ role: findArgs.role, name: findArgs.name, label: findArgs.label, text: findArgs.text });\n if ("errorEnvelope" in r) return r.errorEnvelope;\n elements = document.querySelectorAll("*").length ? semanticCandidates(findArgs) : [];\n } else if (findArgs.text) {\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);\n const lower = findArgs.text.toLowerCase();\n const seen = /* @__PURE__ */ new Set();\n let node;\n while (node = walker.nextNode()) {\n if (node.textContent?.toLowerCase().includes(lower)) {\n const el = node.parentElement;\n if (el && !seen.has(el)) {\n seen.add(el);\n elements.push(el);\n if (elements.length >= limit) break;\n }\n }\n }\n } else {\n elements = Array.from(document.querySelectorAll(INTERACTIVE_SELECTOR));\n }\n const out = elements.slice(0, limit).map((el, i) => {\n const ref = allocRef(el);\n return {\n nth: i,\n ref,\n tag: el.tagName.toLowerCase(),\n role: roleOf(el),\n text: getVisibleText(el).slice(0, textMax),\n attrs: getAttrs(el),\n visible: isVisible(el),\n compound: compoundOf(el) ?? void 0\n };\n });\n return out;\n }\n function semanticCandidates(findArgs) {\n const wantRole = findArgs.role ? findArgs.role.toLowerCase() : null;\n const wantName = findArgs.name ? findArgs.name.toLowerCase() : null;\n const wantText = findArgs.text ? findArgs.text.toLowerCase() : null;\n const all = Array.from(document.querySelectorAll("*"));\n return all.filter((el) => {\n if (!isVisible(el)) return false;\n if (wantRole) {\n const role = roleOf(el);\n if (!role || role.toLowerCase() !== wantRole) return false;\n }\n if (wantName && !accessibleName(el).toLowerCase().includes(wantName)) return false;\n if (wantText && !(el.textContent || "").toLowerCase().includes(wantText)) return false;\n return true;\n });\n }\n function pageState() {\n return {\n url: location.href,\n title: document.title,\n width: window.innerWidth,\n height: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio\n };\n }\n function goto(gotoArgs) {\n if (!gotoArgs.url) return { error: "url_required" };\n window.location.href = gotoArgs.url;\n return { navigated: true, url: gotoArgs.url };\n }\n function evalScript(evalArgs) {\n if (!evalArgs.script) return { error: "script_required" };\n try {\n const fn = new Function(evalArgs.script);\n const result = fn();\n return { result };\n } catch (e) {\n return { error: String(e) };\n }\n }\n function fillElement(fArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable) {\n const isContentEditable = el.isContentEditable;\n if (isContentEditable) {\n el.innerText = fArgs.text ?? "";\n } else {\n input.value = fArgs.text ?? "";\n }\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n const actual = isContentEditable ? el.innerText : input.value;\n return actual === (fArgs.text ?? "") ? { filled: true, verified: true, text: fArgs.text, actual, target, matches_n, match_level } : err("verify_failed", "Input value diverged after fill (controlled component?).", { actual });\n }\n return err("invalid_type", "fill only supports input, textarea, and contenteditable.");\n }\n function typeElement(tArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n el.focus({ preventScroll: true });\n input.value = tArgs.text ?? "";\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n let autocomplete = false;\n try {\n autocomplete = !!document.querySelector("[role=\'listbox\'], [role=\'combobox\'] [role=\'listbox\'], datalist, .ant-select-dropdown:not([style*=\'hidden\']), [id^=\'listbox\']");\n } catch {\n }\n return { typed: true, text: tArgs.text, target, matches_n, match_level, autocomplete };\n }\n function clickElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.scrollIntoView({ block: "center" });\n el.click();\n return { clicked: true, target, matches_n, match_level, tag: el.tagName.toLowerCase(), text: getVisibleText(el) };\n }\n function selectElement(sArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n if (el.tagName !== "SELECT") {\n return err(ERROR_CODES.NOT_A_SELECT, "select was called on a non-<select> element.", { tag: el.tagName.toLowerCase() });\n }\n const select = el;\n const want = sArgs.label ?? sArgs.value ?? sArgs.option;\n if (!want) return err("option_required", "Provide label or value.");\n const byLabel = Array.from(select.options).find((o) => (o.textContent || "").trim() === want);\n const byValue = Array.from(select.options).find((o) => o.value === want);\n const opt = byLabel ?? byValue;\n if (!opt) {\n const available = Array.from(select.options).slice(0, 50).map((o) => (o.textContent || "").trim());\n return err(ERROR_CODES.OPTION_NOT_FOUND, "Option not found.", { available });\n }\n select.value = opt.value;\n select.dispatchEvent(new Event("input", { bubbles: true }));\n select.dispatchEvent(new Event("change", { bubbles: true }));\n return { selected: true, value: select.value, target, matches_n, match_level };\n }\n function pressKeys(kArgs) {\n const keyCombo = kArgs.keys ?? kArgs.key ?? "";\n if (!keyCombo) return { error: "keys_required" };\n const comboParts = keyCombo.split("+").map((s) => s.trim());\n const main = comboParts[comboParts.length - 1];\n const ctrl = comboParts.includes("Control") || comboParts.includes("Ctrl") || comboParts.includes("Meta") || comboParts.includes("Cmd");\n const alt = comboParts.includes("Alt");\n const shift = comboParts.includes("Shift");\n const target = document.activeElement ?? document.body;\n const opts = { key: main, bubbles: true, cancelable: true, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: comboParts.includes("Meta") || comboParts.includes("Cmd") };\n target.dispatchEvent(new KeyboardEvent("keydown", opts));\n target.dispatchEvent(new KeyboardEvent("keypress", opts));\n target.dispatchEvent(new KeyboardEvent("keyup", opts));\n return { pressed: true, key: keyCombo };\n }\n function hoverElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const rect = el.getBoundingClientRect();\n const opts = { bubbles: true, cancelable: true, clientX: rect.x + rect.width / 2, clientY: rect.y + rect.height / 2 };\n el.dispatchEvent(new MouseEvent("mouseover", opts));\n el.dispatchEvent(new MouseEvent("mousemove", opts));\n return { hovered: true, target, matches_n, match_level };\n }\n function focusElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.focus({ preventScroll: true });\n return { focused: true, target, matches_n, match_level };\n }\n function dblclickElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));\n return { dblclicked: true, target, matches_n, match_level };\n }\n function setChecked(want) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n const isAria = input.type !== "checkbox" && input.type !== "radio" && el.getAttribute("aria-checked") != null;\n if (!isAria && input.type !== "checkbox" && input.type !== "radio") {\n return err("invalid_type", "Target is not a checkbox/radio/aria-checked control.", { tag: el.tagName.toLowerCase() });\n }\n if (isAria) {\n el.setAttribute("aria-checked", String(want));\n el.dispatchEvent(new Event("change", { bubbles: true }));\n return { [want ? "checked" : "unchecked"]: true, changed: true, target, matches_n, match_level, kind: "aria" };\n }\n if (input.type === "radio" && !want) {\n return err("invalid_type", "Radio buttons cannot be unchecked directly; select another radio in the group.");\n }\n const changed = input.checked !== want;\n input.checked = want;\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n return { [want ? "checked" : "unchecked"]: true, changed, target, matches_n, match_level, kind: input.type };\n }\n function scrollElement(sArgs) {\n const dir = sArgs.direction ?? "down";\n const amount = sArgs.amount ?? 500;\n if (dir !== "up" && dir !== "down") return { error: "invalid_direction" };\n const dy = dir === "down" ? amount : -amount;\n window.scrollBy({ top: dy, behavior: "instant" });\n return {\n scrolled: true,\n direction: dir,\n amount,\n scrollY: Math.round(window.scrollY),\n scrollHeight: document.documentElement.scrollHeight\n };\n }\n function backNav() {\n history.back();\n return { back: true };\n }\n function htmlJsonTree(el, depth, maxDepth, childrenMax, textMax, truncated) {\n const children = Array.from(el.children);\n const ownText = (el.textContent || "").replace(/\\s+/g, " ").trim();\n const node = {\n tag: el.tagName.toLowerCase(),\n attrs: getAttrs(el),\n text: ownText.length > textMax ? (ownText.slice(0, textMax), truncated.text_truncated = (truncated.text_truncated || 0) + 1, ownText.slice(0, textMax)) : ownText,\n children: [],\n compound: compoundOf(el) ?? void 0\n };\n if (depth < maxDepth) {\n let kids = children;\n if (children.length > childrenMax) {\n truncated.children_dropped = (truncated.children_dropped || 0) + (children.length - childrenMax);\n kids = children.slice(0, childrenMax);\n }\n node.children = kids.map((c) => htmlJsonTree(c, depth + 1, maxDepth, childrenMax, textMax, truncated));\n } else if (children.length) {\n truncated.depth = (truncated.depth || 0) + 1;\n }\n return node;\n }\n function getElement(gArgs) {\n const what = gArgs.what ?? "text";\n if (what === "title") return { value: document.title, matches_n: 1, match_level: "exact" };\n if (what === "url") return { value: location.href, matches_n: 1, match_level: "exact" };\n if (what === "html") {\n if (gArgs["as"] === "json") {\n const root2 = gArgs.css ? document.querySelector(gArgs.css) : document.body;\n if (!root2) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });\n const truncated = {};\n const tree = htmlJsonTree(root2, 0, gArgs.depth ?? 6, gArgs["children-max"] ?? 20, gArgs["text-max"] ?? 200, truncated);\n return {\n value: tree,\n truncated: Object.keys(truncated).length ? truncated : void 0\n };\n }\n const root = gArgs.css ? document.querySelector(gArgs.css) : document.body;\n if (!root) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });\n let html = root.outerHTML;\n if (gArgs.max && html.length > gArgs.max) html = html.slice(0, gArgs.max) + "\\u2026";\n return { value: html };\n }\n const r = resolveTargetRich(gArgs);\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level } = r;\n switch (what) {\n case "text": {\n const t = getVisibleText(el);\n return { value: t.length > 2e3 ? t.slice(0, 2e3) : t, matches_n, match_level };\n }\n case "value":\n return { value: el.value ?? el.innerText ?? null, matches_n, match_level };\n case "attributes":\n return { value: getAttrs(el), matches_n, match_level };\n default:\n return { error: `unknown_what: ${what}` };\n }\n }\n function extractMarkdown(eArgs) {\n const chunkSize = eArgs["chunk-size"] ?? 0;\n const start = eArgs.start ?? 0;\n const scope = eArgs.selector ? document.querySelector(eArgs.selector) : document.querySelector("main") || document.querySelector("article") || document.body;\n if (!scope) return { error: "scope_not_found" };\n const parts = [];\n const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);\n let node;\n while (node = walker.nextNode()) {\n const el = node;\n const tag = el.tagName.toLowerCase();\n const text = getVisibleText(el);\n if (!text) continue;\n if (tag === "h1") parts.push(`# ${text}`);\n else if (tag === "h2") parts.push(`## ${text}`);\n else if (tag === "h3") parts.push(`### ${text}`);\n else if (tag === "p" || tag === "li") parts.push(text);\n else if (tag === "a") parts.push(`[${text}](${el.getAttribute("href") ?? ""})`);\n }\n const full = parts.join("\\n\\n");\n const sliced = chunkSize > 0 ? full.slice(start, start + chunkSize) : full.slice(start);\n return {\n url: location.href,\n title: document.title,\n selector: eArgs.selector ?? "auto",\n total_chars: full.length,\n chunk_size: chunkSize,\n start,\n end: start + sliced.length,\n next_start_char: start + sliced.length < full.length ? start + sliced.length : null,\n content: sliced\n };\n }\n function listFrames() {\n const frames = Array.from(document.querySelectorAll("iframe")).map((f, index) => {\n const src = f.getAttribute("src") ?? "";\n let crossOrigin = false;\n try {\n if (src && /^https?:/.test(src)) crossOrigin = new URL(src, location.href).origin !== location.origin;\n } catch {\n }\n return { index, url: src, name: f.getAttribute("name") ?? "", cross_origin: crossOrigin };\n });\n return { frames };\n }\n function screenshotMeta() {\n return {\n width: window.innerWidth,\n height: window.innerHeight,\n url: location.href,\n title: document.title\n };\n }\n function startTitleMarquee(tArgs) {\n const g = globalThis;\n if (g.__cpMarquee) {\n clearInterval(g.__cpMarquee.interval);\n } else {\n g.__cpMarquee = { original: document.title, interval: 0 };\n }\n const prefix = tArgs.text ?? "\\u{1F916}";\n const base = g.__cpMarquee.original;\n const full = `${prefix} ${base}`;\n const speed = Math.max(100, Math.min(2e3, tArgs.speed ?? 300));\n let pos = 0;\n const tick = () => {\n const display = full.slice(pos) + " " + full.slice(0, pos);\n document.title = display.slice(0, 60);\n pos = (pos + 1) % (full.length + 1 || 1);\n };\n tick();\n g.__cpMarquee.interval = window.setInterval(tick, speed);\n return { started: true, original: base, prefix, speed };\n }\n function stopTitleMarquee() {\n const g = globalThis;\n const state = g.__cpMarquee;\n if (!state) return { restored: false, reason: "no_active_marquee" };\n clearInterval(state.interval);\n document.title = state.original;\n delete g.__cpMarquee;\n return { restored: true, original: state.original };\n }\n function networkEntriesLegacy(nArgs) {\n const all = performance.getEntriesByType("resource").map((e) => ({\n url: e.name,\n type: e.initiatorType ?? "unknown",\n duration: Math.round(e.duration)\n }));\n let entries = all;\n if (nArgs.filter) {\n const keywords = nArgs.filter.split(",").map((s) => s.trim().toLowerCase());\n entries = all.filter((e) => keywords.some((kw) => e.url.toLowerCase().includes(kw)));\n }\n return { entries, count: entries.length, source: "performance-api" };\n }\n async function waitForCondition(wArgs) {\n const timeout = wArgs.timeout ?? 1e4;\n if (wArgs.time != null) {\n await new Promise((res) => setTimeout(res, Math.min(wArgs.time * 1e3, 3e4)));\n return { waited: true, kind: "time", seconds: wArgs.time };\n }\n const deadline = Date.now() + timeout;\n const pollInterval = 200;\n if (wArgs.selector) {\n while (Date.now() < deadline) {\n if (document.querySelector(wArgs.selector)) return { waited: true, kind: "selector", selector: wArgs.selector, elapsedMs: timeout - (deadline - Date.now()) };\n await new Promise((res) => setTimeout(res, pollInterval));\n }\n return err("wait_timeout", `Selector not found within ${timeout}ms.`, { selector: wArgs.selector });\n }\n if (wArgs.text) {\n const lower = wArgs.text.toLowerCase();\n while (Date.now() < deadline) {\n if ((document.body.textContent || "").toLowerCase().includes(lower)) {\n return { waited: true, kind: "text", text: wArgs.text, elapsedMs: timeout - (deadline - Date.now()) };\n }\n await new Promise((res) => setTimeout(res, pollInterval));\n }\n return err("wait_timeout", `Text not found within ${timeout}ms.`, { text: wArgs.text });\n }\n return { error: "no_condition" };\n }\n function uploadFiles(uArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n if (input.type !== "file") return err("invalid_type", "Target is not an input[type=file].", { tag: el.tagName.toLowerCase() });\n try {\n const dt = new DataTransfer();\n for (const name of uArgs.files ?? []) {\n dt.items.add(new File([""], name.split("/").pop() || name));\n }\n input.files = dt.files;\n } catch {\n }\n input.dispatchEvent(new Event("change", { bubbles: true }));\n return {\n uploaded: true,\n files: uArgs.files ?? [],\n file_names: (uArgs.files ?? []).map((f) => f.split("/").pop()),\n target,\n matches_n,\n match_level,\n multiple: input.multiple,\n accept: input.accept || null\n };\n }\n function dragElements(dArgs) {\n const r1 = resolveTargetRich(sel());\n if ("errorEnvelope" in r1) return r1.errorEnvelope;\n const r2 = resolveTargetRich({ ref: dArgs.ref2, css: dArgs.css2 });\n if ("errorEnvelope" in r2) return r2.errorEnvelope;\n const src = r1.el;\n const tgt = r2.el;\n const make = (type) => {\n const r1rect = src.getBoundingClientRect();\n return new DragEvent(type, { bubbles: true, cancelable: true, clientX: r1rect.x + r1rect.width / 2, clientY: r1rect.y + r1rect.height / 2 });\n };\n src.dispatchEvent(make("dragstart"));\n tgt.dispatchEvent(new DragEvent("dragenter", { bubbles: true }));\n tgt.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true }));\n tgt.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true }));\n src.dispatchEvent(new DragEvent("dragend", { bubbles: true }));\n return { dragged: true, source: String(sel().ref ?? sel().css ?? ""), target: String(dArgs.ref2 ?? dArgs.css2 ?? ""), source_matches_n: r1.matches_n, target_matches_n: r2.matches_n };\n }\n switch (command) {\n case "cp.state":\n return a.tree === false ? pageState() : buildStateTree();\n case "cp.snapshot":\n return buildStateTree();\n case "cp.find":\n return findElements({\n css: a.css,\n text: a.text,\n limit: a.limit,\n role: a.role,\n name: a.name,\n label: a.label,\n testid: a.testid,\n "text-max": a["text-max"]\n });\n case "cp.click":\n return clickElement();\n case "cp.type":\n return typeElement({ text: a.text });\n case "cp.goto":\n return goto({ url: a.url });\n case "cp.eval":\n return evalScript({ script: a.script });\n case "cp.fill":\n return fillElement({ text: a.text });\n case "cp.select":\n return selectElement({ label: a.label, value: a.value, option: a.option });\n case "cp.keys":\n return pressKeys({ keys: a.keys, key: a.key });\n case "cp.hover":\n return hoverElement();\n case "cp.focus":\n return focusElement();\n case "cp.dblclick":\n return dblclickElement();\n case "cp.check":\n return setChecked(true);\n case "cp.uncheck":\n return setChecked(false);\n case "cp.scroll":\n return scrollElement({ direction: a.direction, amount: a.amount });\n case "cp.back":\n return backNav();\n case "cp.get":\n return getElement({\n what: a.what,\n css: a.css,\n ref: a.ref,\n nth: a.nth,\n "as": a["as"],\n depth: a.depth,\n "children-max": a["children-max"],\n "text-max": a["text-max"],\n max: a.max,\n role: a.role,\n name: a.name,\n label: a.label,\n text: a.text,\n testid: a.testid\n });\n case "cp.extract":\n return extractMarkdown({\n "chunk-size": a["chunk-size"],\n start: a.start,\n selector: a.selector\n });\n case "cp.frames":\n return listFrames();\n case "cp.screenshot":\n return screenshotMeta();\n case "cp.network":\n return networkEntriesLegacy({ filter: a.filter });\n case "cp.wait":\n return waitForCondition({\n selector: a.selector,\n text: a.text,\n time: a.time,\n timeout: a.timeout\n });\n case "cp.upload":\n return uploadFiles({ files: a.files });\n case "cp.drag":\n return dragElements({ ref2: a.ref2, css2: a.css2 });\n case "cp.title.marquee":\n return startTitleMarquee({ text: a.text, speed: a.speed });\n case "cp.title.restore":\n return stopTitleMarquee();\n default:\n return { error: `unknown_command: ${command}` };\n }\n }\n var RUNTIME_VERSION = "0.3.0-opencli-parity";\n globalThis.__cpRunControl = runControl;\n globalThis.__cpRuntimeVersion = RUNTIME_VERSION;\n})();\n';
1002
+ var control_runtime_source_default = '"use strict";\n(() => {\n // src/shared/control-runtime.ts\n function getRefStore() {\n const g = globalThis;\n if (!g.__cpRefs) {\n g.__cpRefs = { counter: 1, refs: /* @__PURE__ */ new Map(), cssPaths: /* @__PURE__ */ new Map() };\n }\n return g.__cpRefs;\n }\n function allocRef(el) {\n const store = getRefStore();\n for (const [ref2, existing] of store.refs) {\n if (existing === el) return ref2;\n }\n const ref = store.counter++;\n store.refs.set(ref, el);\n store.cssPaths.set(ref, uniqueCssPath(el));\n return ref;\n }\n function uniqueCssPath(el) {\n if (el.id) return `#${CSS.escape(el.id)}`;\n const parts = [];\n let cur = el;\n while (cur && cur !== document.body && cur !== document.documentElement) {\n let sel = cur.tagName.toLowerCase();\n if (cur.id) {\n parts.unshift(`#${CSS.escape(cur.id)}`);\n break;\n }\n const parent = cur.parentElement;\n if (parent) {\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);\n if (sameTag.length > 1) {\n const idx = sameTag.indexOf(cur) + 1;\n sel += `:nth-of-type(${idx})`;\n }\n }\n parts.unshift(sel);\n cur = parent;\n }\n return parts.join(" > ");\n }\n function err(code, message, extra) {\n return { error: { code, message, ...extra || {} } };\n }\n var ERROR_CODES = {\n NOT_FOUND: "not_found",\n STALE_REF: "stale_ref",\n INVALID_SELECTOR: "invalid_selector",\n SELECTOR_NOT_FOUND: "selector_not_found",\n SELECTOR_AMBIGUOUS: "selector_ambiguous",\n SELECTOR_NTH_OUT_OF_RANGE: "selector_nth_out_of_range",\n OPTION_NOT_FOUND: "option_not_found",\n NOT_A_SELECT: "not_a_select"\n };\n var IMPLICIT_ROLE = {\n a: "link",\n button: "button",\n select: "combobox",\n textarea: "textbox",\n h1: "heading",\n h2: "heading",\n h3: "heading",\n h4: "heading",\n h5: "heading",\n h6: "heading",\n img: "img",\n nav: "navigation",\n ul: "list",\n ol: "list",\n li: "listitem",\n main: "main",\n article: "article",\n aside: "complementary",\n header: "banner",\n footer: "contentinfo",\n form: "form",\n table: "table",\n summary: "button",\n option: "option",\n output: "status"\n };\n function roleOf(el) {\n const explicit = el.getAttribute("role");\n if (explicit) return explicit.split(/\\s+/)[0];\n const tag = el.tagName.toLowerCase();\n if (tag === "input") {\n const type = el.type || "text";\n if (type === "hidden") return null;\n if (type === "checkbox") return "checkbox";\n if (type === "radio") return "radio";\n if (type === "button" || type === "submit" || type === "reset") return "button";\n if (type === "range") return "slider";\n return "textbox";\n }\n if (tag === "progress") return "progressbar";\n if (tag === "meter") return "meter";\n return IMPLICIT_ROLE[tag] ?? null;\n }\n function accessibleName(el) {\n const aria = el.getAttribute("aria-label");\n if (aria) return aria.trim();\n const labelledby = el.getAttribute("aria-labelledby");\n if (labelledby) {\n const parts = labelledby.split(/\\s+/).map((id2) => document.getElementById(id2)?.textContent?.trim() || "");\n const joined = parts.filter(Boolean).join(" ");\n if (joined) return joined.slice(0, 120);\n }\n const id = el.getAttribute("id");\n if (id) {\n const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);\n if (label?.textContent?.trim()) return label.textContent.trim().slice(0, 120);\n }\n if (el.tagName.toLowerCase() === "input") {\n const type = el.type;\n if (type === "button" || type === "submit" || type === "reset") {\n const v = el.value;\n if (v) return v;\n }\n }\n const alt = el.getAttribute("alt");\n if (alt) return alt;\n const title = el.getAttribute("title");\n if (title) return title;\n const placeholder = el.getAttribute("placeholder");\n if (placeholder) return placeholder;\n return (el.textContent || "").replace(/\\s+/g, " ").trim().slice(0, 120);\n }\n function isVisible(el) {\n if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) return false;\n const html = el;\n if (html.hasAttribute("hidden")) return false;\n if (html.closest("[hidden]")) return false;\n const style = window.getComputedStyle(html);\n if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;\n if (el instanceof HTMLElement && el.offsetParent === null) {\n const pos = style.position;\n if (pos !== "fixed" && pos !== "sticky") return false;\n }\n return true;\n }\n var INTERACTIVE_SELECTOR = "a[href], button, input, textarea, select, summary, [role], [contenteditable=\'\'], [contenteditable=\'true\'], [onclick], [tabindex]";\n function isInteractive(el) {\n return el.matches(INTERACTIVE_SELECTOR);\n }\n function testidSelector(testid) {\n return `[data-testid="${CSS.escape(testid)}"], [data-test-id="${CSS.escape(testid)}"], [data-test="${CSS.escape(testid)}"], [data-cy="${CSS.escape(testid)}"]`;\n }\n function resolveTargetRich(sel) {\n const targetDesc = sel.ref != null ? String(sel.ref) : sel.css ?? (sel.role || sel.name || sel.label || sel.testid || sel.text ? `${sel.role ? `role=${sel.role} ` : ""}${sel.name ? `name=${sel.name} ` : ""}${sel.label ? `label=${sel.label} ` : ""}${sel.testid ? `testid=${sel.testid} ` : ""}${sel.text ? `text=${sel.text}` : ""}`.trim() : "");\n if (sel.ref != null) {\n const store = getRefStore();\n const el = store.refs.get(sel.ref);\n if (el && document.contains(el) && isVisible(el)) {\n return { el, matches_n: 1, match_level: "exact", target: targetDesc };\n }\n if (el && !document.contains(el)) {\n const cssPath = store.cssPaths.get(sel.ref);\n if (cssPath) {\n const re = document.querySelector(cssPath);\n if (re && isVisible(re)) {\n store.refs.set(sel.ref, re);\n return { el: re, matches_n: 1, match_level: "reidentified", target: targetDesc };\n }\n }\n return { errorEnvelope: err(ERROR_CODES.STALE_REF, "Numeric ref is no longer in the DOM. Re-run state.") };\n }\n return { errorEnvelope: err(ERROR_CODES.NOT_FOUND, "Numeric ref is no longer in the DOM. Re-run state.") };\n }\n let candidates = [];\n if (sel.css) {\n try {\n candidates = Array.from(document.querySelectorAll(sel.css));\n } catch {\n return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: sel.css }) };\n }\n if (candidates.length === 0) {\n return { errorEnvelope: err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: sel.css, candidates: suggestCandidates() }) };\n }\n } else if (sel.testid) {\n try {\n candidates = Array.from(document.querySelectorAll(testidSelector(sel.testid)));\n } catch {\n return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.", { testid: sel.testid }) };\n }\n } else if (sel.role || sel.name || sel.label || sel.text) {\n const wantRole = sel.role ? sel.role.toLowerCase() : null;\n const wantName = sel.name ? sel.name.toLowerCase() : null;\n const wantText = sel.text ? sel.text.toLowerCase() : null;\n let labelTargets = [];\n if (sel.label) {\n const lower = sel.label.toLowerCase();\n labelTargets = Array.from(document.querySelectorAll("label")).filter((l) => (l.textContent || "").trim().toLowerCase().includes(lower)).map((l) => {\n const forId = l.getAttribute("for");\n if (forId) return document.getElementById(forId);\n return l.querySelector("input, select, textarea");\n }).filter((x) => !!x);\n }\n const all = Array.from(document.querySelectorAll("*"));\n candidates = all.filter((el) => {\n if (!isVisible(el)) return false;\n const role = roleOf(el);\n if (wantRole && (!role || role.toLowerCase() !== wantRole)) return false;\n if (wantName) {\n const name = accessibleName(el).toLowerCase();\n if (!name.includes(wantName)) return false;\n }\n if (wantText) {\n const t = (el.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();\n if (!t.includes(wantText)) return false;\n }\n return true;\n });\n if (labelTargets.length) {\n const set = new Set(labelTargets);\n candidates = labelTargets.concat(candidates.filter((c) => !set.has(c)));\n }\n if (candidates.length === 0) {\n return {\n errorEnvelope: err(\n ERROR_CODES.SELECTOR_NOT_FOUND,\n "Semantic locator matches 0 elements.",\n { role: sel.role, name: sel.name, label: sel.label, text: sel.text, candidates: suggestCandidates() }\n )\n };\n }\n } else {\n return { errorEnvelope: err("no_target", "Provide a numeric ref, CSS selector, or semantic locator (--role/--name/--label/--text/--testid).") };\n }\n const visible = candidates.filter(isVisible);\n const pool = visible.length ? visible : candidates;\n if (sel.nth != null) {\n if (sel.nth < 0 || sel.nth >= pool.length) {\n return { errorEnvelope: err(ERROR_CODES.SELECTOR_NTH_OUT_OF_RANGE, `--nth ${sel.nth} beyond match count ${pool.length}.`) };\n }\n const el = pool[sel.nth];\n return { el, matches_n: pool.length, match_level: "reidentified", target: targetDesc };\n }\n if (pool.length > 1) {\n return {\n errorEnvelope: err(ERROR_CODES.SELECTOR_AMBIGUOUS, `CSS matches ${pool.length} elements and no --nth given.`, {\n matches_n: pool.length,\n candidates: pool.slice(0, 5).map((el) => uniqueCssPath(el))\n })\n };\n }\n return { el: pool[0], matches_n: 1, match_level: "reidentified", target: targetDesc };\n }\n function suggestCandidates() {\n const store = getRefStore();\n const recent = Array.from(store.refs.entries()).slice(-5).map(([, el]) => uniqueCssPath(el));\n return recent;\n }\n function compoundOf(el) {\n const tag = el.tagName.toLowerCase();\n if (tag === "input") {\n const input = el;\n const type = input.type;\n if (["date", "time", "datetime-local", "month", "week"].includes(type)) {\n const formatMap = {\n date: "YYYY-MM-DD",\n time: "HH:mm",\n "datetime-local": "YYYY-MM-DDTHH:mm",\n month: "YYYY-MM",\n week: "YYYY-Www"\n };\n return {\n control: type,\n format: formatMap[type] ?? type,\n current: input.value || "",\n min: input.min || null,\n max: input.max || null\n };\n }\n if (type === "file") {\n const current = Array.from(input.files ?? []).map((f) => f.name);\n return { control: "file", multiple: input.multiple, current, accept: input.accept || null };\n }\n return null;\n }\n if (tag === "select") {\n const select = el;\n const options = Array.from(select.options).slice(0, 50).map((o) => ({\n label: (o.textContent || "").trim(),\n value: o.value,\n selected: o.selected\n }));\n return {\n control: "select",\n multiple: select.multiple,\n current: select.selectedOptions[0] ? (select.selectedOptions[0].textContent || "").trim() : "",\n options,\n options_total: select.options.length\n };\n }\n return null;\n }\n var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "meta", "link", "head", "base", "template", "path", "circle", "rect", "g", "defs", "polygon", "line"]);\n function describeElement(el, ref) {\n const tag = el.tagName.toLowerCase();\n const role = el.getAttribute("role") ?? null;\n const name = accessibleName(el);\n const parts = [];\n if (ref != null) parts.push(`[${ref}]`);\n parts.push(tag);\n if (role && role !== IMPLICIT_ROLE[tag]) parts.push(`role=${role}`);\n if (tag === "input") {\n const input = el;\n if (input.type && input.type !== "text") parts.push(`[${input.type}]`);\n if (input.disabled) parts.push("(disabled)");\n }\n if (name) parts.push(JSON.stringify(name.length > 60 ? name.slice(0, 57) + "\\u2026" : name));\n if (tag === "a") {\n const href = el.getAttribute("href");\n if (href) parts.push(`\\u2192 ${href.slice(0, 50)}`);\n }\n if (el.disabled) parts.push("(disabled)");\n return parts.join(" ");\n }\n function buildStateTree() {\n const lines = [];\n const compounds = {};\n const budget = { nodes: 0, chars: 0, truncated: false };\n const MAX_NODES = 400;\n const MAX_CHARS = 9e3;\n let hiddenInteractive = 0;\n function emit(depth, text) {\n const line = " ".repeat(Math.min(depth, 12)) + text;\n lines.push(line);\n budget.chars += line.length;\n }\n function walk(el, depth) {\n if (budget.nodes >= MAX_NODES || budget.chars >= MAX_CHARS) {\n budget.truncated = true;\n return;\n }\n const tag = el.tagName.toLowerCase();\n if (SKIP_TAGS.has(tag)) return;\n budget.nodes++;\n const visible = isVisible(el);\n const interactive = isInteractive(el);\n if (interactive) {\n if (visible) {\n const ref = allocRef(el);\n emit(depth, describeElement(el, ref));\n const compound = compoundOf(el);\n if (compound) compounds[String(ref)] = compound;\n } else {\n hiddenInteractive++;\n return;\n }\n } else if (visible) {\n if (/^h[1-6]$/.test(tag)) {\n const t = accessibleName(el);\n if (t) emit(depth, `${"#".repeat(Number(tag[1]))} ${t}`);\n } else if (tag === "p" || tag === "li" || tag === "td" || tag === "th" || tag === "figcaption" || tag === "blockquote") {\n const ownText = ownTextOf(el);\n if (ownText) emit(depth, ownText.slice(0, 100));\n } else if (tag === "img") {\n const alt = el.getAttribute("alt") || "";\n if (alt) emit(depth, `img ${JSON.stringify(alt.slice(0, 60))}`);\n }\n }\n if (depth < 14) {\n for (const child of Array.from(el.children)) {\n walk(child, depth + 1);\n if (budget.truncated) return;\n }\n }\n }\n function ownTextOf(el) {\n let text = "";\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) text += node.textContent || "";\n }\n return text.replace(/\\s+/g, " ").trim();\n }\n const root = document.querySelector("main") || document.querySelector("#root") || document.body;\n emit(0, `# ${document.title}`);\n walk(root, 0);\n const scrollable = document.documentElement.scrollHeight > window.innerHeight + 200;\n if (scrollable) {\n emit(0, "\\u2195 page is scrollable \\u2014 run cp.scroll down to reveal more");\n }\n if (hiddenInteractive > 0) {\n emit(0, `\\u2139 ${hiddenInteractive} hidden interactive element(s) not tagged`);\n }\n if (budget.truncated) {\n emit(0, "\\u2026 (snapshot truncated by budget \\u2014 narrow with a selector or scroll)");\n }\n return {\n url: location.href,\n title: document.title,\n text: lines.join("\\n"),\n compounds,\n hints: { scroll: scrollable, hidden_interactive: hiddenInteractive },\n truncated: budget.truncated\n };\n }\n function runControl(command, args) {\n const a = args || {};\n function getVisibleText(el) {\n return (el.textContent || "").trim().replace(/\\s+/g, " ").slice(0, 120);\n }\n function getAttrs(el) {\n const attrs = {};\n for (const name of ["id", "class", "name", "type", "href", "src", "placeholder", "value", "role", "aria-label", "data-testid"]) {\n attrs[name] = el.getAttribute(name);\n }\n return attrs;\n }\n function sel() {\n return {\n ref: a.ref,\n css: a.css,\n nth: a.nth,\n role: a.role,\n name: a.name,\n label: a.label,\n text: a.text,\n testid: a.testid\n };\n }\n function findElements(findArgs) {\n const limit = findArgs.limit ?? 20;\n const textMax = findArgs["text-max"] ?? 120;\n let elements = [];\n if (findArgs.css) {\n try {\n elements = Array.from(document.querySelectorAll(findArgs.css));\n } catch {\n return err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: findArgs.css });\n }\n } else if (findArgs.testid) {\n try {\n elements = Array.from(document.querySelectorAll(testidSelector(findArgs.testid)));\n } catch {\n return err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.");\n }\n } else if (findArgs.role || findArgs.name || findArgs.label) {\n const r = resolveTargetRich({ role: findArgs.role, name: findArgs.name, label: findArgs.label, text: findArgs.text });\n if ("errorEnvelope" in r) return r.errorEnvelope;\n elements = document.querySelectorAll("*").length ? semanticCandidates(findArgs) : [];\n } else if (findArgs.text) {\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);\n const lower = findArgs.text.toLowerCase();\n const seen = /* @__PURE__ */ new Set();\n let node;\n while (node = walker.nextNode()) {\n if (node.textContent?.toLowerCase().includes(lower)) {\n const el = node.parentElement;\n if (el && !seen.has(el)) {\n seen.add(el);\n elements.push(el);\n if (elements.length >= limit) break;\n }\n }\n }\n } else {\n elements = Array.from(document.querySelectorAll(INTERACTIVE_SELECTOR));\n }\n const out = elements.slice(0, limit).map((el, i) => {\n const ref = allocRef(el);\n return {\n nth: i,\n ref,\n tag: el.tagName.toLowerCase(),\n role: roleOf(el),\n text: getVisibleText(el).slice(0, textMax),\n attrs: getAttrs(el),\n visible: isVisible(el),\n compound: compoundOf(el) ?? void 0\n };\n });\n return out;\n }\n function semanticCandidates(findArgs) {\n const wantRole = findArgs.role ? findArgs.role.toLowerCase() : null;\n const wantName = findArgs.name ? findArgs.name.toLowerCase() : null;\n const wantText = findArgs.text ? findArgs.text.toLowerCase() : null;\n const all = Array.from(document.querySelectorAll("*"));\n return all.filter((el) => {\n if (!isVisible(el)) return false;\n if (wantRole) {\n const role = roleOf(el);\n if (!role || role.toLowerCase() !== wantRole) return false;\n }\n if (wantName && !accessibleName(el).toLowerCase().includes(wantName)) return false;\n if (wantText && !(el.textContent || "").toLowerCase().includes(wantText)) return false;\n return true;\n });\n }\n function pageState() {\n return {\n url: location.href,\n title: document.title,\n width: window.innerWidth,\n height: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio\n };\n }\n function goto(gotoArgs) {\n if (!gotoArgs.url) return { error: "url_required" };\n let sameUrl = false;\n try {\n sameUrl = new URL(gotoArgs.url, window.location.href).href === window.location.href;\n } catch {\n sameUrl = false;\n }\n if (sameUrl) {\n if (gotoArgs.reload === true) {\n window.location.reload();\n return { navigated: true, reloaded: true, url: gotoArgs.url };\n }\n return { navigated: false, reused: true, url: gotoArgs.url };\n }\n window.location.href = gotoArgs.url;\n return { navigated: true, url: gotoArgs.url };\n }\n function evalScript(evalArgs) {\n if (!evalArgs.script) return { error: "script_required" };\n try {\n const fn = new Function(evalArgs.script);\n const result = fn();\n return { result };\n } catch (e) {\n return { error: String(e) };\n }\n }\n function fillElement(fArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable) {\n const isContentEditable = el.isContentEditable;\n if (isContentEditable) {\n el.innerText = fArgs.text ?? "";\n } else {\n input.value = fArgs.text ?? "";\n }\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n const actual = isContentEditable ? el.innerText : input.value;\n return actual === (fArgs.text ?? "") ? { filled: true, verified: true, text: fArgs.text, actual, target, matches_n, match_level } : err("verify_failed", "Input value diverged after fill (controlled component?).", { actual });\n }\n return err("invalid_type", "fill only supports input, textarea, and contenteditable.");\n }\n function typeElement(tArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n el.focus({ preventScroll: true });\n input.value = tArgs.text ?? "";\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n let autocomplete = false;\n try {\n autocomplete = !!document.querySelector("[role=\'listbox\'], [role=\'combobox\'] [role=\'listbox\'], datalist, .ant-select-dropdown:not([style*=\'hidden\']), [id^=\'listbox\']");\n } catch {\n }\n return { typed: true, text: tArgs.text, target, matches_n, match_level, autocomplete };\n }\n function clickElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.scrollIntoView({ block: "center" });\n el.click();\n return { clicked: true, target, matches_n, match_level, tag: el.tagName.toLowerCase(), text: getVisibleText(el) };\n }\n function selectElement(sArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n if (el.tagName !== "SELECT") {\n return err(ERROR_CODES.NOT_A_SELECT, "select was called on a non-<select> element.", { tag: el.tagName.toLowerCase() });\n }\n const select = el;\n const want = sArgs.label ?? sArgs.value ?? sArgs.option;\n if (!want) return err("option_required", "Provide label or value.");\n const byLabel = Array.from(select.options).find((o) => (o.textContent || "").trim() === want);\n const byValue = Array.from(select.options).find((o) => o.value === want);\n const opt = byLabel ?? byValue;\n if (!opt) {\n const available = Array.from(select.options).slice(0, 50).map((o) => (o.textContent || "").trim());\n return err(ERROR_CODES.OPTION_NOT_FOUND, "Option not found.", { available });\n }\n select.value = opt.value;\n select.dispatchEvent(new Event("input", { bubbles: true }));\n select.dispatchEvent(new Event("change", { bubbles: true }));\n return { selected: true, value: select.value, target, matches_n, match_level };\n }\n function pressKeys(kArgs) {\n const keyCombo = kArgs.keys ?? kArgs.key ?? "";\n if (!keyCombo) return { error: "keys_required" };\n const comboParts = keyCombo.split("+").map((s) => s.trim());\n const main = comboParts[comboParts.length - 1];\n const ctrl = comboParts.includes("Control") || comboParts.includes("Ctrl") || comboParts.includes("Meta") || comboParts.includes("Cmd");\n const alt = comboParts.includes("Alt");\n const shift = comboParts.includes("Shift");\n const target = document.activeElement ?? document.body;\n const opts = { key: main, bubbles: true, cancelable: true, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: comboParts.includes("Meta") || comboParts.includes("Cmd") };\n target.dispatchEvent(new KeyboardEvent("keydown", opts));\n target.dispatchEvent(new KeyboardEvent("keypress", opts));\n target.dispatchEvent(new KeyboardEvent("keyup", opts));\n return { pressed: true, key: keyCombo };\n }\n function hoverElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const rect = el.getBoundingClientRect();\n const opts = { bubbles: true, cancelable: true, clientX: rect.x + rect.width / 2, clientY: rect.y + rect.height / 2 };\n el.dispatchEvent(new MouseEvent("mouseover", opts));\n el.dispatchEvent(new MouseEvent("mousemove", opts));\n return { hovered: true, target, matches_n, match_level };\n }\n function focusElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.focus({ preventScroll: true });\n return { focused: true, target, matches_n, match_level };\n }\n function dblclickElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));\n return { dblclicked: true, target, matches_n, match_level };\n }\n function setChecked(want) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n const isAria = input.type !== "checkbox" && input.type !== "radio" && el.getAttribute("aria-checked") != null;\n if (!isAria && input.type !== "checkbox" && input.type !== "radio") {\n return err("invalid_type", "Target is not a checkbox/radio/aria-checked control.", { tag: el.tagName.toLowerCase() });\n }\n if (isAria) {\n el.setAttribute("aria-checked", String(want));\n el.dispatchEvent(new Event("change", { bubbles: true }));\n return { [want ? "checked" : "unchecked"]: true, changed: true, target, matches_n, match_level, kind: "aria" };\n }\n if (input.type === "radio" && !want) {\n return err("invalid_type", "Radio buttons cannot be unchecked directly; select another radio in the group.");\n }\n const changed = input.checked !== want;\n input.checked = want;\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n return { [want ? "checked" : "unchecked"]: true, changed, target, matches_n, match_level, kind: input.type };\n }\n function scrollElement(sArgs) {\n const dir = sArgs.direction ?? "down";\n const amount = sArgs.amount ?? 500;\n if (dir !== "up" && dir !== "down") return { error: "invalid_direction" };\n const dy = dir === "down" ? amount : -amount;\n window.scrollBy({ top: dy, behavior: "instant" });\n return {\n scrolled: true,\n direction: dir,\n amount,\n scrollY: Math.round(window.scrollY),\n scrollHeight: document.documentElement.scrollHeight\n };\n }\n function backNav() {\n history.back();\n return { back: true };\n }\n function htmlJsonTree(el, depth, maxDepth, childrenMax, textMax, truncated) {\n const children = Array.from(el.children);\n const ownText = (el.textContent || "").replace(/\\s+/g, " ").trim();\n const node = {\n tag: el.tagName.toLowerCase(),\n attrs: getAttrs(el),\n text: ownText.length > textMax ? (ownText.slice(0, textMax), truncated.text_truncated = (truncated.text_truncated || 0) + 1, ownText.slice(0, textMax)) : ownText,\n children: [],\n compound: compoundOf(el) ?? void 0\n };\n if (depth < maxDepth) {\n let kids = children;\n if (children.length > childrenMax) {\n truncated.children_dropped = (truncated.children_dropped || 0) + (children.length - childrenMax);\n kids = children.slice(0, childrenMax);\n }\n node.children = kids.map((c) => htmlJsonTree(c, depth + 1, maxDepth, childrenMax, textMax, truncated));\n } else if (children.length) {\n truncated.depth = (truncated.depth || 0) + 1;\n }\n return node;\n }\n function getElement(gArgs) {\n const what = gArgs.what ?? "text";\n if (what === "title") return { value: document.title, matches_n: 1, match_level: "exact" };\n if (what === "url") return { value: location.href, matches_n: 1, match_level: "exact" };\n if (what === "html") {\n if (gArgs["as"] === "json") {\n const root2 = gArgs.css ? document.querySelector(gArgs.css) : document.body;\n if (!root2) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });\n const truncated = {};\n const tree = htmlJsonTree(root2, 0, gArgs.depth ?? 6, gArgs["children-max"] ?? 20, gArgs["text-max"] ?? 200, truncated);\n return {\n value: tree,\n truncated: Object.keys(truncated).length ? truncated : void 0\n };\n }\n const root = gArgs.css ? document.querySelector(gArgs.css) : document.body;\n if (!root) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });\n let html = root.outerHTML;\n if (gArgs.max && html.length > gArgs.max) html = html.slice(0, gArgs.max) + "\\u2026";\n return { value: html };\n }\n const r = resolveTargetRich(gArgs);\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level } = r;\n switch (what) {\n case "text": {\n const t = getVisibleText(el);\n return { value: t.length > 2e3 ? t.slice(0, 2e3) : t, matches_n, match_level };\n }\n case "value":\n return { value: el.value ?? el.innerText ?? null, matches_n, match_level };\n case "attributes":\n return { value: getAttrs(el), matches_n, match_level };\n default:\n return { error: `unknown_what: ${what}` };\n }\n }\n function extractMarkdown(eArgs) {\n const chunkSize = eArgs["chunk-size"] ?? 0;\n const start = eArgs.start ?? 0;\n const scope = eArgs.selector ? document.querySelector(eArgs.selector) : document.querySelector("main") || document.querySelector("article") || document.body;\n if (!scope) return { error: "scope_not_found" };\n const parts = [];\n const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);\n let node;\n while (node = walker.nextNode()) {\n const el = node;\n const tag = el.tagName.toLowerCase();\n const text = getVisibleText(el);\n if (!text) continue;\n if (tag === "h1") parts.push(`# ${text}`);\n else if (tag === "h2") parts.push(`## ${text}`);\n else if (tag === "h3") parts.push(`### ${text}`);\n else if (tag === "p" || tag === "li") parts.push(text);\n else if (tag === "a") parts.push(`[${text}](${el.getAttribute("href") ?? ""})`);\n }\n const full = parts.join("\\n\\n");\n const sliced = chunkSize > 0 ? full.slice(start, start + chunkSize) : full.slice(start);\n return {\n url: location.href,\n title: document.title,\n selector: eArgs.selector ?? "auto",\n total_chars: full.length,\n chunk_size: chunkSize,\n start,\n end: start + sliced.length,\n next_start_char: start + sliced.length < full.length ? start + sliced.length : null,\n content: sliced\n };\n }\n function listFrames() {\n const frames = Array.from(document.querySelectorAll("iframe")).map((f, index) => {\n const src = f.getAttribute("src") ?? "";\n let crossOrigin = false;\n try {\n if (src && /^https?:/.test(src)) crossOrigin = new URL(src, location.href).origin !== location.origin;\n } catch {\n }\n return { index, url: src, name: f.getAttribute("name") ?? "", cross_origin: crossOrigin };\n });\n return { frames };\n }\n function screenshotMeta() {\n return {\n width: window.innerWidth,\n height: window.innerHeight,\n url: location.href,\n title: document.title\n };\n }\n function startTitleMarquee(tArgs) {\n const g = globalThis;\n if (g.__cpMarquee) {\n clearInterval(g.__cpMarquee.interval);\n } else {\n g.__cpMarquee = { original: document.title, interval: 0 };\n }\n const prefix = tArgs.text ?? "\\u{1F916}";\n const base = g.__cpMarquee.original;\n const full = `${prefix} ${base}`;\n const speed = Math.max(100, Math.min(2e3, tArgs.speed ?? 300));\n let pos = 0;\n const tick = () => {\n const display = full.slice(pos) + " " + full.slice(0, pos);\n document.title = display.slice(0, 60);\n pos = (pos + 1) % (full.length + 1 || 1);\n };\n tick();\n g.__cpMarquee.interval = window.setInterval(tick, speed);\n return { started: true, original: base, prefix, speed };\n }\n function stopTitleMarquee() {\n const g = globalThis;\n const state = g.__cpMarquee;\n if (!state) return { restored: false, reason: "no_active_marquee" };\n clearInterval(state.interval);\n document.title = state.original;\n delete g.__cpMarquee;\n return { restored: true, original: state.original };\n }\n function networkEntriesLegacy(nArgs) {\n const all = performance.getEntriesByType("resource").map((e) => ({\n url: e.name,\n type: e.initiatorType ?? "unknown",\n duration: Math.round(e.duration)\n }));\n let entries = all;\n if (nArgs.filter) {\n const keywords = nArgs.filter.split(",").map((s) => s.trim().toLowerCase());\n entries = all.filter((e) => keywords.some((kw) => e.url.toLowerCase().includes(kw)));\n }\n return { entries, count: entries.length, source: "performance-api" };\n }\n async function waitForCondition(wArgs) {\n const timeout = wArgs.timeout ?? 1e4;\n if (wArgs.time != null) {\n await new Promise((res) => setTimeout(res, Math.min(wArgs.time * 1e3, 3e4)));\n return { waited: true, kind: "time", seconds: wArgs.time };\n }\n const deadline = Date.now() + timeout;\n const pollInterval = 200;\n if (wArgs.selector) {\n while (Date.now() < deadline) {\n if (document.querySelector(wArgs.selector)) return { waited: true, kind: "selector", selector: wArgs.selector, elapsedMs: timeout - (deadline - Date.now()) };\n await new Promise((res) => setTimeout(res, pollInterval));\n }\n return err("wait_timeout", `Selector not found within ${timeout}ms.`, { selector: wArgs.selector });\n }\n if (wArgs.text) {\n const lower = wArgs.text.toLowerCase();\n while (Date.now() < deadline) {\n if ((document.body.textContent || "").toLowerCase().includes(lower)) {\n return { waited: true, kind: "text", text: wArgs.text, elapsedMs: timeout - (deadline - Date.now()) };\n }\n await new Promise((res) => setTimeout(res, pollInterval));\n }\n return err("wait_timeout", `Text not found within ${timeout}ms.`, { text: wArgs.text });\n }\n return { error: "no_condition" };\n }\n function uploadFiles(uArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n if (input.type !== "file") return err("invalid_type", "Target is not an input[type=file].", { tag: el.tagName.toLowerCase() });\n try {\n const dt = new DataTransfer();\n for (const name of uArgs.files ?? []) {\n dt.items.add(new File([""], name.split("/").pop() || name));\n }\n input.files = dt.files;\n } catch {\n }\n input.dispatchEvent(new Event("change", { bubbles: true }));\n return {\n uploaded: true,\n files: uArgs.files ?? [],\n file_names: (uArgs.files ?? []).map((f) => f.split("/").pop()),\n target,\n matches_n,\n match_level,\n multiple: input.multiple,\n accept: input.accept || null\n };\n }\n function dragElements(dArgs) {\n const r1 = resolveTargetRich(sel());\n if ("errorEnvelope" in r1) return r1.errorEnvelope;\n const r2 = resolveTargetRich({ ref: dArgs.ref2, css: dArgs.css2 });\n if ("errorEnvelope" in r2) return r2.errorEnvelope;\n const src = r1.el;\n const tgt = r2.el;\n const make = (type) => {\n const r1rect = src.getBoundingClientRect();\n return new DragEvent(type, { bubbles: true, cancelable: true, clientX: r1rect.x + r1rect.width / 2, clientY: r1rect.y + r1rect.height / 2 });\n };\n src.dispatchEvent(make("dragstart"));\n tgt.dispatchEvent(new DragEvent("dragenter", { bubbles: true }));\n tgt.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true }));\n tgt.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true }));\n src.dispatchEvent(new DragEvent("dragend", { bubbles: true }));\n return { dragged: true, source: String(sel().ref ?? sel().css ?? ""), target: String(dArgs.ref2 ?? dArgs.css2 ?? ""), source_matches_n: r1.matches_n, target_matches_n: r2.matches_n };\n }\n switch (command) {\n case "cp.state":\n return a.tree === false ? pageState() : buildStateTree();\n case "cp.snapshot":\n return buildStateTree();\n case "cp.find":\n return findElements({\n css: a.css,\n text: a.text,\n limit: a.limit,\n role: a.role,\n name: a.name,\n label: a.label,\n testid: a.testid,\n "text-max": a["text-max"]\n });\n case "cp.click":\n return clickElement();\n case "cp.type":\n return typeElement({ text: a.text });\n case "cp.goto":\n return goto({ url: a.url });\n case "cp.eval":\n return evalScript({ script: a.script });\n case "cp.fill":\n return fillElement({ text: a.text });\n case "cp.select":\n return selectElement({ label: a.label, value: a.value, option: a.option });\n case "cp.keys":\n return pressKeys({ keys: a.keys, key: a.key });\n case "cp.hover":\n return hoverElement();\n case "cp.focus":\n return focusElement();\n case "cp.dblclick":\n return dblclickElement();\n case "cp.check":\n return setChecked(true);\n case "cp.uncheck":\n return setChecked(false);\n case "cp.scroll":\n return scrollElement({ direction: a.direction, amount: a.amount });\n case "cp.back":\n return backNav();\n case "cp.get":\n return getElement({\n what: a.what,\n css: a.css,\n ref: a.ref,\n nth: a.nth,\n "as": a["as"],\n depth: a.depth,\n "children-max": a["children-max"],\n "text-max": a["text-max"],\n max: a.max,\n role: a.role,\n name: a.name,\n label: a.label,\n text: a.text,\n testid: a.testid\n });\n case "cp.extract":\n return extractMarkdown({\n "chunk-size": a["chunk-size"],\n start: a.start,\n selector: a.selector\n });\n case "cp.frames":\n return listFrames();\n case "cp.screenshot":\n return screenshotMeta();\n case "cp.network":\n return networkEntriesLegacy({ filter: a.filter });\n case "cp.wait":\n return waitForCondition({\n selector: a.selector,\n text: a.text,\n time: a.time,\n timeout: a.timeout\n });\n case "cp.upload":\n return uploadFiles({ files: a.files });\n case "cp.drag":\n return dragElements({ ref2: a.ref2, css2: a.css2 });\n case "cp.title.marquee":\n return startTitleMarquee({ text: a.text, speed: a.speed });\n case "cp.title.restore":\n return stopTitleMarquee();\n default:\n return { error: `unknown_command: ${command}` };\n }\n }\n var RUNTIME_VERSION = "0.3.0-opencli-parity";\n globalThis.__cpRunControl = runControl;\n globalThis.__cpRuntimeVersion = RUNTIME_VERSION;\n})();\n';
990
1003
 
991
1004
  // src/background/cdp.ts
992
1005
  var attached = /* @__PURE__ */ new Set();
993
1006
  var CDP_COMMAND_TIMEOUT_MS = 15e3;
994
- function send(target, method, params) {
995
- return chrome.debugger.sendCommand(target, method, params ?? {});
1007
+ function send(target, method, params, timeoutMs = CDP_COMMAND_TIMEOUT_MS) {
1008
+ return new Promise((resolve, reject) => {
1009
+ const timer = setTimeout(() => {
1010
+ if (target.tabId !== void 0) {
1011
+ chrome.debugger.detach({ tabId: target.tabId }).catch(() => void 0).then(() => {
1012
+ attached.delete(target.tabId);
1013
+ });
1014
+ }
1015
+ reject(new Error(`cdp_timeout: ${method} after ${timeoutMs}ms (tab frozen or unresponsive)`));
1016
+ }, timeoutMs);
1017
+ chrome.debugger.sendCommand(target, method, params ?? {}).then(
1018
+ (v) => {
1019
+ clearTimeout(timer);
1020
+ resolve(v);
1021
+ },
1022
+ (err3) => {
1023
+ clearTimeout(timer);
1024
+ reject(err3);
1025
+ }
1026
+ );
1027
+ });
996
1028
  }
997
1029
  function isAttachableUrl(url) {
998
1030
  if (!url) return false;
@@ -1326,6 +1358,21 @@ chrome.debugger.onDetach.addListener((source) => {
1326
1358
  networkEnabledTabs.delete(source.tabId);
1327
1359
  }
1328
1360
  });
1361
+ async function detachIfAttached(tabId) {
1362
+ if (!attached.has(tabId)) return;
1363
+ try {
1364
+ await chrome.debugger.detach({ tabId });
1365
+ } catch {
1366
+ }
1367
+ attached.delete(tabId);
1368
+ networkEnabledTabs.delete(tabId);
1369
+ }
1370
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
1371
+ if (changeInfo.status !== "loading") return;
1372
+ attached.delete(tabId);
1373
+ networkEnabledTabs.delete(tabId);
1374
+ chrome.debugger.detach({ tabId }).catch(() => void 0);
1375
+ });
1329
1376
  var CDP_ALLOWLIST = /* @__PURE__ */ new Set([
1330
1377
  "Accessibility.enable",
1331
1378
  "Accessibility.getFullAXTree",
@@ -1362,10 +1409,11 @@ async function insertText(tabId, text) {
1362
1409
  }
1363
1410
  async function cdpEvaluate(tabId, expression, timeoutMs = CDP_COMMAND_TIMEOUT_MS) {
1364
1411
  await ensureAttached(tabId);
1365
- const result = await chrome.debugger.sendCommand(
1412
+ const result = await send(
1366
1413
  { tabId },
1367
1414
  "Runtime.evaluate",
1368
- { expression, returnByValue: true, awaitPromise: true }
1415
+ { expression, returnByValue: true, awaitPromise: true },
1416
+ timeoutMs
1369
1417
  );
1370
1418
  if (result.exceptionDetails) {
1371
1419
  const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
@@ -1897,6 +1945,19 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
1897
1945
  return err2(e instanceof Error ? e.message : String(e), code);
1898
1946
  }
1899
1947
  }
1948
+ if (command === "hm.liepin.search") {
1949
+ try {
1950
+ const result = await runLiepinSearch(controlArgs, log, {
1951
+ sessionId,
1952
+ groupTitle,
1953
+ explicitTab
1954
+ });
1955
+ return ok(result);
1956
+ } catch (e) {
1957
+ const code = e instanceof ControlBlockedError ? e.code : void 0;
1958
+ return err2(e instanceof Error ? e.message : String(e), code);
1959
+ }
1960
+ }
1900
1961
  let tab = await getControlTab(tabOverride);
1901
1962
  if (!tab?.id) {
1902
1963
  if (command === "cp.goto" && typeof controlArgs.url === "string" && controlArgs.url) {
@@ -2029,6 +2090,17 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
2029
2090
  return err2(e instanceof Error ? e.message : String(e));
2030
2091
  }
2031
2092
  }
2093
+ if (command === "hm.liepin.extract") {
2094
+ if (!isAttachableUrl(tab.url)) {
2095
+ return err2("liepin_extract_requires_http_page");
2096
+ }
2097
+ try {
2098
+ const result = await cdpEvaluate(tab.id, LIEPIN_EXTRACT_SOURCE);
2099
+ return ok(result);
2100
+ } catch (e) {
2101
+ return err2(e instanceof Error ? e.message : String(e));
2102
+ }
2103
+ }
2032
2104
  if (command.startsWith("hm.")) {
2033
2105
  return { status: "error", code: "not_implemented", message: `${command} is not implemented yet` };
2034
2106
  }
@@ -2316,6 +2388,8 @@ async function runLinkedInSearch(args, log, ctx) {
2316
2388
  const filters = args.filters && typeof args.filters === "object" ? args.filters : {};
2317
2389
  const maxPages = Math.min(Math.max(Number(args.maxPages) || 3, 1), 10);
2318
2390
  const keepTab = args.keepTab === true;
2391
+ const activateTab = args.activateTab !== false;
2392
+ const loadTimeoutMs = Math.min(Math.max(Number(args.loadTimeoutMs) || 25e3, 5e3), 3e5);
2319
2393
  const delayBase = typeof args.delayMs === "number" ? Number(args.delayMs) : 2500;
2320
2394
  let effectiveKeywords = keywords;
2321
2395
  if (!filters.geoUrn && typeof filters.location === "string" && filters.location.trim()) {
@@ -2330,7 +2404,12 @@ async function runLinkedInSearch(args, log, ctx) {
2330
2404
  if (typeof filters.company === "string" && filters.company) u.searchParams.set("company", filters.company);
2331
2405
  if (typeof filters.school === "string" && filters.school) u.searchParams.set("school", filters.school);
2332
2406
  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 });
2407
+ log("info", "hm.linkedin.search tab created", { tabId: tab.id, url: u.toString(), maxPages, activateTab });
2408
+ if (activateTab && tab.id != null) {
2409
+ await chrome.tabs.update(tab.id, { active: true }).catch(
2410
+ (err3) => log("warn", "hm.linkedin.search activate tab failed", { tabId: tab.id, error: err3.message })
2411
+ );
2412
+ }
2334
2413
  if (ctx.sessionId && !ctx.explicitTab && tab.id) {
2335
2414
  addTabToSessionGroup(ctx.sessionId, ctx.groupTitle, tab).catch(
2336
2415
  (err3) => log("warn", "hm.linkedin.search addTabToSessionGroup failed", { error: err3.message })
@@ -2342,7 +2421,7 @@ async function runLinkedInSearch(args, log, ctx) {
2342
2421
  try {
2343
2422
  for (let page = 1; page <= maxPages; page++) {
2344
2423
  if (tab.id == null) break;
2345
- await waitTabLoaded(tab.id, 25e3);
2424
+ await waitTabLoaded(tab.id, loadTimeoutMs);
2346
2425
  const blocked = await cdpEvaluate(tab.id, LINKEDIN_WALL_CHECK_SOURCE).catch(() => null);
2347
2426
  if (blocked === "CAPTCHA" || blocked === "LOGIN_WALL") {
2348
2427
  log("warn", "hm.linkedin.search blocked by linkedin risk control", { blocked, page, url: lastUrl });
@@ -2374,8 +2453,20 @@ async function runLinkedInSearch(args, log, ctx) {
2374
2453
  }
2375
2454
  } finally {
2376
2455
  if (!keepTab && tab.id != null) {
2377
- await chrome.tabs.remove(tab.id).catch(() => {
2378
- });
2456
+ await closeTabGracefully(tab.id);
2457
+ }
2458
+ }
2459
+ let diagnostics = null;
2460
+ if (leads.size === 0 && tab.id != null) {
2461
+ const t = await chrome.tabs.get(tab.id).catch(() => null);
2462
+ const signal = t && t.id != null ? await cdpEvaluate(t.id, LIEPIN_WALL_CHECK_SOURCE).catch(() => null) : null;
2463
+ const bodySnippet = t && t.id != null && (!t.url || t.url !== "about:blank") ? await cdpEvaluate(
2464
+ t.id,
2465
+ '(() => { const b = document.body && document.body.innerText; return b ? b.replace(/\\s+/g, " ").trim().slice(0, 300) : null; })()'
2466
+ ).catch(() => null) : null;
2467
+ diagnostics = { url: t?.url ?? null, title: t?.title ?? null, signal, bodySnippet };
2468
+ if (signal) {
2469
+ log("warn", "hm.liepin.search 0 leads with risk-control signal", { signal, url: diagnostics.url });
2379
2470
  }
2380
2471
  }
2381
2472
  return {
@@ -2385,7 +2476,553 @@ async function runLinkedInSearch(args, log, ctx) {
2385
2476
  leadCount: leads.size,
2386
2477
  leads: Array.from(leads.values()),
2387
2478
  keepTab,
2388
- tabId: keepTab ? tab.id ?? null : null
2479
+ tabId: keepTab ? tab.id ?? null : null,
2480
+ // Issue #24 P0:0 leads 不等于"没搜到",可能是登录墙/反爬页根本没渲染结果。
2481
+ // 带出诊断字段,由调用方决定 degraded;真实 0 结果 diagnostics.signal 为 null。
2482
+ ...leads.size === 0 && diagnostics ? { diagnostics, degraded: diagnostics.signal !== null, degradedReason: diagnostics.signal ?? void 0 } : {}
2483
+ };
2484
+ }
2485
+ var LIEPIN_WALL_CHECK_SOURCE = String.raw`(() => {
2486
+ try {
2487
+ if (location.href === 'about:blank' || !location.hostname) return 'BLANK_KILL';
2488
+ const url = location.href;
2489
+ if (/liepin\.com\/(login|passport|signin)/i.test(url)) return 'LOGIN_WALL';
2490
+ if (/liepin\.com\/(safe|verify|check|captcha|slide)/i.test(url)) return 'CAPTCHA';
2491
+ const bodyText = (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 4000);
2492
+ if (/滑块|拖动验证|安全验证|验证码|verify you are human|请完成安全验证/.test(bodyText)) return 'CAPTCHA';
2493
+ if (document.querySelector('[class*="captcha"], [id*="captcha"], iframe[src*="captcha"], iframe[src*="slide"]')) return 'CAPTCHA';
2494
+ if (/请先登录|登录后查看|立即登录/.test(bodyText) && !document.querySelector('#resume-detail-basic-info')) return 'LOGIN_WALL';
2495
+ } catch (e) { /* 检测失败不阻断 */ }
2496
+ return null;
2497
+ })()`;
2498
+ var LIEPIN_EXTRACT_SOURCE = String.raw`(async () => {
2499
+ const waitFor = (fn, timeout, interval) => new Promise((resolve) => {
2500
+ const t0 = Date.now();
2501
+ const tick = () => {
2502
+ let v = null;
2503
+ try { v = fn(); } catch (e) {}
2504
+ if (v) return resolve(v);
2505
+ if (Date.now() - t0 > (timeout || 8000)) return resolve(null);
2506
+ setTimeout(tick, interval || 250);
2507
+ };
2508
+ tick();
2509
+ });
2510
+ if (document.readyState !== 'complete') {
2511
+ await waitFor(() => document.readyState === 'complete', 12000, 200);
2512
+ }
2513
+ const clean = (s) => {
2514
+ if (s == null) return null;
2515
+ const t = String(s).replace(/\s+/g, ' ').trim();
2516
+ return t || null;
2517
+ };
2518
+ const txt = (el) => clean(el ? el.textContent : null);
2519
+ const first = (sel, root) => (root || document).querySelector(sel);
2520
+ const attr = (el, name) => (el ? (el.getAttribute(name) || null) : null);
2521
+ const abs = (href) => {
2522
+ if (!href) return null;
2523
+ try { return new URL(href, location.href).toString(); } catch (e) { return href; }
2524
+ };
2525
+ /* sep-info 用 <i></i> 作分隔符:转 | 后按段切分 */
2526
+ const splitByI = (el) => {
2527
+ if (!el) return [];
2528
+ const html = el.innerHTML || '';
2529
+ const holder = document.createElement('div');
2530
+ return html.replace(/<i[^>]*>\s*<\/i>/g, '|').split('|').map((seg) => {
2531
+ holder.innerHTML = seg;
2532
+ return clean(holder.textContent);
2533
+ }).filter(Boolean);
2534
+ };
2535
+ /* (2012.07 - 至今, 14年2个月) → { start, end, duration } */
2536
+ const parsePeriod = (text) => {
2537
+ const m = (text || '').match(/[((]\s*([^))]*?)\s*[))]/);
2538
+ if (!m) return { start: null, end: null, duration: null };
2539
+ const parts = m[1].split(',').map((s) => clean(s)).filter(Boolean);
2540
+ const range = (parts[0] || '').split('-').map((s) => clean(s));
2541
+ return { start: range[0] || null, end: range[1] || null, duration: parts[1] || null };
2542
+ };
2543
+ /* rd-info-col 映射:每个 col 是 title+cont 对(一行可能两个 col) */
2544
+ const readInfoCols = (root) => {
2545
+ const out = {};
2546
+ (root || document).querySelectorAll('.rd-info-col').forEach((col) => {
2547
+ const title = txt(first('.rd-info-col-title', col));
2548
+ const cont = txt(first('.rd-info-col-cont', col));
2549
+ if (title && cont) out[title.replace(/[::]\s*$/, '')] = cont;
2550
+ });
2551
+ return out;
2552
+ };
2553
+ const resIdFrom = (href, scm) => {
2554
+ if (scm) {
2555
+ const m = scm.match(/cid=([^&]+)/);
2556
+ if (m) return m[1];
2557
+ }
2558
+ if (href) {
2559
+ const m = href.match(/res_id_encode=([^&]+)/);
2560
+ if (m) return m[1];
2561
+ }
2562
+ return null;
2563
+ };
2564
+
2565
+ const url = location.href;
2566
+ const canonical = (document.querySelector('link[rel="canonical"]') || {}).href || url.split('?')[0];
2567
+ const entities = [];
2568
+ const links = { talent: [], company: [], job: [] };
2569
+
2570
+ if (document.querySelector('#resume-detail-basic-info')) {
2571
+ const basic = first('#resume-detail-basic-info');
2572
+ const sepInfos = basic.querySelectorAll('.basic-cont .sep-info');
2573
+ const parts0 = sepInfos.length > 0 ? splitByI(sepInfos[0]) : [];
2574
+ const parts1 = sepInfos.length > 1 ? splitByI(sepInfos[1]) : [];
2575
+ const joined0 = parts0.join(' ');
2576
+ const age = joined0.match(/(\d+)\s*岁/);
2577
+ const workYears = joined0.match(/工作\s*(\d+)\s*年/);
2578
+ const name = txt(first('.name-box .name', basic));
2579
+ const statusBox = first('.res-status-box', basic);
2580
+ const contactTimeRaw = txt(statusBox);
2581
+ const scm = attr(document.querySelector('[data-tlg-scm]'), 'data-tlg-scm');
2582
+
2583
+ const intentRoot = first('#resume-detail-job-exp-info');
2584
+ const workExperiences = [];
2585
+ document.querySelectorAll('#resume-detail-work-info .rd-work-item-cont').forEach((item) => {
2586
+ const cols = readInfoCols(item);
2587
+ workExperiences.push({
2588
+ company: attr(first('.rd-work-comp h5', item), 'title') || txt(first('.rd-work-comp h5', item)),
2589
+ ...parsePeriod(txt(first('.rd-work-time', item))),
2590
+ position: txt(first('.job-name', item)),
2591
+ department: cols['所在部门'] || null,
2592
+ location: cols['工作地点'] || null,
2593
+ subordinates: cols['下属人数'] != null ? Number(cols['下属人数']) : null,
2594
+ reportsTo: cols['汇报对象'] || null,
2595
+ responsibilities: cols['职责业绩'] || null,
2596
+ });
2597
+ });
2598
+ const projects = [];
2599
+ document.querySelectorAll('#resume-detail-project-info .rd-project-item-cont').forEach((item) => {
2600
+ const cols = readInfoCols(item);
2601
+ projects.push({
2602
+ name: attr(first('.rd-project-name h5', item), 'title') || txt(first('.rd-project-name h5', item)),
2603
+ ...parsePeriod(txt(first('.rd-project-time', item))),
2604
+ role: cols['项目职务'] || null,
2605
+ company: cols['所在公司'] || null,
2606
+ description: cols['项目描述'] || null,
2607
+ duties: cols['项目职责'] || null,
2608
+ });
2609
+ });
2610
+ const education = [];
2611
+ document.querySelectorAll('#resume-detail-edu-info .rd-edu-info-item').forEach((item) => {
2612
+ education.push({
2613
+ school: attr(first('.school-name', item), 'title') || txt(first('.school-name', item)),
2614
+ major: attr(first('.school-special', item), 'title') || txt(first('.school-special', item)),
2615
+ degree: attr(first('.school-degree', item), 'title') || txt(first('.school-degree', item)),
2616
+ period: txt(first('.school-time', item)),
2617
+ tags: Array.from(item.querySelectorAll('.edu-school-tags .tag-item')).map((t) => txt(t)).filter(Boolean),
2618
+ });
2619
+ });
2620
+
2621
+ entities.push({
2622
+ entityType: 'talent',
2623
+ platform: 'liepin',
2624
+ resId: resIdFrom(url, scm),
2625
+ name,
2626
+ nameMasked: name ? /\*/.test(name) : undefined,
2627
+ statusTag: txt(first('.user-status-tag', basic)),
2628
+ gender: parts0.find((p) => /^(男|女)$/.test(p)) || null,
2629
+ age: age ? Number(age[1]) : null,
2630
+ city: parts0.find((p) => /^(北京|上海|广州|深圳|天津|重庆|杭州|南京|苏州|成都|武汉|西安|[一-龥]{2,6})$/.test(p) && !/^(男|女|保密)$/.test(p) && !/岁|年/.test(p)) || null,
2631
+ degree: parts0.find((p) => /(博士|硕士|MBA|本科|大专|专科|高中|中专|初中)/.test(p)) || null,
2632
+ workYears: workYears ? Number(workYears[1]) : null,
2633
+ currentCompany: parts0.length ? parts0[parts0.length - 1] : null,
2634
+ currentCompanyMasked: parts0.includes('保密') ? true : undefined,
2635
+ currentPosition: parts1.length ? parts1[0] : null,
2636
+ contactTime: contactTimeRaw ? contactTimeRaw.replace(/^方便联系时间[::]\s*/, '') : null,
2637
+ photoUrl: (() => { const img = first('.logo-box img', basic); return img ? abs(img.getAttribute('src')) : null; })(),
2638
+ intent: intentRoot ? {
2639
+ position: attr(first('.tabs .title', intentRoot), 'title') || txt(first('.tabs .title', intentRoot)),
2640
+ salary: txt(first('.tabs .salary', intentRoot)),
2641
+ city: txt(first('.dqname-wrap', intentRoot)),
2642
+ industries: Array.from(intentRoot.querySelectorAll('.lebels-wrap span')).map((s) => txt(s)).filter(Boolean),
2643
+ } : null,
2644
+ workExperiences,
2645
+ projects,
2646
+ education,
2647
+ languages: Array.from(document.querySelectorAll('#resume-detail-lang-info .rd-lang-item .lang-name')).map((el) => txt(el)).filter(Boolean),
2648
+ selfEvaluation: txt(first('#resume-detail-self-eva-info .resume-detail-template-cont')),
2649
+ statement: txt(first('.c-detail-statement')),
2650
+ hasAntiScrapeWatermark: document.querySelectorAll('.cozue').length > 0,
2651
+ url: canonical,
2652
+ source: 'liepin',
2653
+ });
2654
+ } else if (document.querySelector('.talent-resume-card')) {
2655
+ /* 人才管理页/搜索结果列表:.talent-resume-card 卡片(事实源 §7) */
2656
+ document.querySelectorAll('.talent-resume-card').forEach((card) => {
2657
+ const a = first('a.resume-title', card) || first('a[href*="showresumedetail"]', card);
2658
+ const href = a ? abs(attr(a, 'href')) : null;
2659
+ const scm = attr(card, 'data-tlg-scm') || '';
2660
+ const titleLine = attr(a, 'title') || txt(a);
2661
+ const eduItem = Array.from(card.querySelectorAll('.info-item')).find((el) => /最高学历/.test(el.textContent || ''));
2662
+ const workLines = [];
2663
+ card.querySelectorAll('.work-info .info-cont p, .info-cont p').forEach((p) => {
2664
+ const comp = first('.comp', p);
2665
+ const pos = first('.rw-title', p);
2666
+ if (comp || pos) {
2667
+ workLines.push({
2668
+ period: txt(p.querySelector('i')),
2669
+ company: attr(comp, 'title') || txt(comp),
2670
+ position: attr(pos, 'title') || txt(pos),
2671
+ });
2672
+ }
2673
+ });
2674
+ const acquired = txt(first('.right-extension .temp-side', card));
2675
+ const lead = {
2676
+ url: href,
2677
+ name: titleLine ? (titleLine.split(/\s*[||]\s*/)[0] || titleLine) : null,
2678
+ titleLine,
2679
+ resId: resIdFrom(href, scm),
2680
+ education: eduItem ? clean((eduItem.textContent || '').replace(/^最高学历[::]?/, '')) : null,
2681
+ recentWork: workLines,
2682
+ acquiredAt: acquired ? acquired.replace(/^获取时间[::]?/, '') : null,
2683
+ source: 'liepin',
2684
+ };
2685
+ if (lead.url && !links.talent.some((l) => l.url === lead.url)) links.talent.push(lead);
2686
+ });
2687
+ } else {
2688
+ /* 兜底:任何含简历详情链接的页面,提取链接名单 */
2689
+ document.querySelectorAll('a[href*="showresumedetail"]').forEach((a) => {
2690
+ const href = abs(attr(a, 'href'));
2691
+ if (href && !links.talent.some((l) => l.url === href)) {
2692
+ links.talent.push({ url: href, name: txt(a), resId: resIdFrom(href, ''), source: 'liepin' });
2693
+ }
2694
+ });
2695
+ }
2696
+
2697
+ const pageClass = document.querySelector('#resume-detail-basic-info')
2698
+ ? 'talent'
2699
+ : (document.querySelector('.talent-resume-card') ? 'resume-list' : (links.talent.length ? 'list' : 'unknown'));
2700
+ return {
2701
+ url,
2702
+ canonical,
2703
+ pageClass,
2704
+ title: clean(document.title),
2705
+ extractedAt: new Date().toISOString(),
2706
+ entities,
2707
+ links,
2708
+ };
2709
+ })()`;
2710
+ var LIEPIN_FILL_AND_SEARCH_SOURCE = String.raw`(async (P) => {
2711
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2712
+ const clean = (s) => (s == null ? null : String(s).replace(/\s+/g, ' ').trim() || null);
2713
+ const delay = P.fieldDelayMs && P.fieldDelayMs >= 0 ? P.fieldDelayMs : 1500;
2714
+ const applied = [];
2715
+ const errors = [];
2716
+
2717
+ /* React 受控输入兼容:native setter + input/change 事件 */
2718
+ const setNativeValue = (el, value) => {
2719
+ const setter = Object.getOwnPropertyDescriptor(
2720
+ el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
2721
+ 'value',
2722
+ ).set;
2723
+ setter.call(el, value);
2724
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2725
+ el.dispatchEvent(new Event('change', { bubbles: true }));
2726
+ };
2727
+ const setInput = (sel, value) => {
2728
+ const el = document.querySelector(sel);
2729
+ if (!el) { errors.push('missing_input:' + sel); return false; }
2730
+ if (el.disabled) { errors.push('disabled_input:' + sel); return false; }
2731
+ el.focus();
2732
+ setNativeValue(el, String(value));
2733
+ el.blur();
2734
+ return true;
2735
+ };
2736
+ /* 在某个容器内点击包含指定文本的按钮(如"确 定"/"搜 索") */
2737
+ const clickButtonIn = (rootSel, text) => {
2738
+ const root = rootSel ? document.querySelector(rootSel) : document;
2739
+ if (!root) { errors.push('missing_root:' + rootSel); return false; }
2740
+ const btn = Array.from(root.querySelectorAll('button, span, a')).find((b) => {
2741
+ const t = clean(b.textContent);
2742
+ return t && t.replace(/\s+/g, '') === text;
2743
+ });
2744
+ if (!btn) { errors.push('missing_button:' + text + '@' + rootSel); return false; }
2745
+ btn.click();
2746
+ return true;
2747
+ };
2748
+ /* 点击条件 tag(容器可带 #N 出现序号:如 '.sfilter-city#2' = 第 2 个 sfilter-city,即期望城市) */
2749
+ const clickTag = (containerSel, text) => {
2750
+ let sel = containerSel;
2751
+ let idx = 0;
2752
+ const m = String(containerSel).match(/^(.*)#(\d+)$/);
2753
+ if (m) { sel = m[1]; idx = parseInt(m[2], 10) - 1; }
2754
+ const containers = document.querySelectorAll(sel);
2755
+ const container = containers.length > idx ? containers[idx] : null;
2756
+ if (!container) { errors.push('missing_container:' + containerSel); return false; }
2757
+ const tag = Array.from(container.querySelectorAll('label.tag-item, .tag-item, span')).find((el) => {
2758
+ const t = clean(el.textContent);
2759
+ return t && (t === text || t.replace(/\s+/g, '') === String(text).replace(/\s+/g, ''));
2760
+ });
2761
+ if (!tag) {
2762
+ const available = Array.from(container.querySelectorAll('label.tag-item, .tag-item'))
2763
+ .map((el) => clean(el.textContent))
2764
+ .filter(Boolean)
2765
+ .slice(0, 20);
2766
+ errors.push('missing_tag:' + text + '@' + containerSel
2767
+ + (available.length ? '; available=[' + available.join(', ') + ']' : ''));
2768
+ return false;
2769
+ }
2770
+ tag.click();
2771
+ return true;
2772
+ };
2773
+
2774
+ try {
2775
+ /* 1) 关键词:顶部自动补全输入框(与 .search-btn 同容器,placeholder 搜职位/公司/行业等) */
2776
+ if (P.keyword) {
2777
+ const okInput = (() => {
2778
+ const box = document.querySelector('.auto-input-wrap-v3') || document.querySelector('.ant-select-auto-complete');
2779
+ const el = box ? box.querySelector('input.ant-select-selection-search-input') : null;
2780
+ if (!el) { errors.push('missing_input:keyword'); return false; }
2781
+ el.focus();
2782
+ setNativeValue(el, String(P.keyword));
2783
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2784
+ el.dispatchEvent(new Event('change', { bubbles: true }));
2785
+ el.blur();
2786
+ return true;
2787
+ })();
2788
+ if (okInput) applied.push('keyword');
2789
+ await sleep(delay);
2790
+ }
2791
+
2792
+ /* 2) tag 类条件(每次点击后 human-paced) */
2793
+ const tagSteps = [];
2794
+ if (P.city) tagSteps.push(['.sfilter-city', P.city, 'city']);
2795
+ if (P.expectCity) tagSteps.push(['.sfilter-city#2', P.expectCity, 'expectCity']);
2796
+ if (P.workYear) tagSteps.push(['.sfilter-work-year', P.workYear, 'workYear']);
2797
+ if (P.education) tagSteps.push(['.sfilter-edu', P.education, 'education']);
2798
+ if (P.schoolTag) tagSteps.push(['.sfilter-additional', P.schoolTag, 'schoolTag']);
2799
+ if (P.language) tagSteps.push(['.sfilter-lang', P.language, 'language']);
2800
+ for (const [sel, text, key] of tagSteps) {
2801
+ if (clickTag(sel, text)) applied.push(key);
2802
+ await sleep(delay);
2803
+ }
2804
+
2805
+ /* 3) 区间输入类条件:填输入 + 点容器内"确定" */
2806
+ const rangeSteps = [];
2807
+ if (P.ageMin != null || P.ageMax != null) {
2808
+ rangeSteps.push(['#ageLow', P.ageMin, '#ageHigh', P.ageMax, '.age-shadow-box', 'age']);
2809
+ }
2810
+ if (P.wantSalaryMin != null || P.wantSalaryMax != null) {
2811
+ rangeSteps.push(['#wantSalaryLow', P.wantSalaryMin, '#wantSalaryHigh', P.wantSalaryMax, '.sfilter-salary .search-resume-shadow-box', 'wantSalary']);
2812
+ }
2813
+ if (P.nowSalaryMin != null || P.nowSalaryMax != null) {
2814
+ rangeSteps.push(['#nowSalaryLow', P.nowSalaryMin, '#nowSalaryHigh', P.nowSalaryMax, null, 'nowSalary']);
2815
+ }
2816
+ for (const [loSel, lo, hiSel, hi, boxSel, key] of rangeSteps) {
2817
+ let ok = true;
2818
+ if (lo != null) ok = setInput(loSel, lo) && ok;
2819
+ await sleep(Math.max(600, Math.floor(delay / 2)));
2820
+ if (hi != null) ok = setInput(hiSel, hi) && ok;
2821
+ await sleep(Math.max(600, Math.floor(delay / 2)));
2822
+ /* 确定按钮:优先容器内,其次全局匹配(目前年薪容器无专属 class) */
2823
+ if (ok && !clickButtonIn(boxSel, '确定')) {
2824
+ ok = clickButtonIn(null, '确定');
2825
+ }
2826
+ if (ok) applied.push(key);
2827
+ await sleep(delay);
2828
+ }
2829
+
2830
+ /* 4) 工作年限自定义:先点"自定义"tag 解锁输入,再填 + 确定 */
2831
+ if (P.workYearsMin != null || P.workYearsMax != null) {
2832
+ if (clickTag('.sfilter-work-year', '自定义')) {
2833
+ applied.push('workYearCustom');
2834
+ await sleep(delay);
2835
+ let ok = true;
2836
+ if (P.workYearsMin != null) ok = setInput('#workYearsLow', P.workYearsMin) && ok;
2837
+ await sleep(Math.max(600, Math.floor(delay / 2)));
2838
+ if (P.workYearsMax != null) ok = setInput('#workYearsHigh', P.workYearsMax) && ok;
2839
+ await sleep(Math.max(600, Math.floor(delay / 2)));
2840
+ if (ok && clickButtonIn('.year-custom-box', '确定')) applied.push('workYears');
2841
+ }
2842
+ await sleep(delay);
2843
+ }
2844
+
2845
+ /* 5) 复选框:海外工作经验 / 管理经验 */
2846
+ for (const [name, key] of [['workAbroad', 'workAbroad'], ['managerial', 'managerial']]) {
2847
+ if (P[key]) {
2848
+ const input = document.querySelector('input[name="' + name + '"]');
2849
+ if (input && !input.checked) {
2850
+ const wrapper = input.closest('label') || input;
2851
+ wrapper.click();
2852
+ applied.push(key);
2853
+ await sleep(delay);
2854
+ } else if (!input) {
2855
+ errors.push('missing_checkbox:' + name);
2856
+ }
2857
+ }
2858
+ }
2859
+
2860
+ /* 6) 提交搜索前停顿(类人工审阅),点击顶部"搜 索" */
2861
+ await sleep(Math.max(1500, delay));
2862
+ const searchBtn = document.querySelector('button.search-btn');
2863
+ if (!searchBtn) { errors.push('missing_button:search'); return { applied, errors, submitted: false }; }
2864
+ searchBtn.click();
2865
+ applied.push('submit');
2866
+ return { applied, errors, submitted: true };
2867
+ } catch (e) {
2868
+ errors.push('fill_exception:' + String((e && e.message) || e));
2869
+ return { applied, errors, submitted: false };
2870
+ }
2871
+ })`;
2872
+ var LIEPIN_RESULT_WAIT_SOURCE = String.raw`(() => {
2873
+ return new Promise((resolve) => {
2874
+ const t0 = Date.now();
2875
+ const tick = () => {
2876
+ let hit = false;
2877
+ try {
2878
+ hit = !!(document.querySelector('.talent-resume-card') || document.querySelector('a[href*="showresumedetail"]'));
2879
+ if (!hit && /没有找到|暂无相关|0\s*条结果|共\s*0\s*条/.test(document.body ? document.body.innerText : '')) return resolve('empty');
2880
+ } catch (e) {}
2881
+ if (hit) return resolve(true);
2882
+ if (Date.now() - t0 > 20000) return resolve(false);
2883
+ setTimeout(tick, 400);
2884
+ };
2885
+ tick();
2886
+ });
2887
+ })()`;
2888
+ var LIEPIN_NEXT_CLICK_SOURCE = String.raw`(async () => {
2889
+ const next = document.querySelector('.ant-pagination-next button')
2890
+ || document.querySelector('.ant-pagination-next')
2891
+ || document.querySelector('button[aria-label="下一页"]');
2892
+ if (!next || next.getAttribute('aria-disabled') === 'true' || next.disabled) return false;
2893
+ next.click();
2894
+ return true;
2895
+ })()`;
2896
+ function liepinBlockedCode(blocked) {
2897
+ if (blocked === "LOGIN_WALL") return "LIE_PIN_LOGIN_WALL";
2898
+ if (blocked === "BLANK_KILL") return "LIE_PIN_BLANK_KILL";
2899
+ return "LIE_PIN_CAPTCHA";
2900
+ }
2901
+ async function runLiepinSearch(args, log, ctx) {
2902
+ const keyword = typeof args.keyword === "string" ? args.keyword.trim() : "";
2903
+ if (!keyword) throw new Error("liepin_search_requires_keyword");
2904
+ const maxPages = Math.min(Math.max(Number(args.maxPages) || 1, 1), 5);
2905
+ const keepTab = args.keepTab === true;
2906
+ const fieldDelayMs = typeof args.delayMs === "number" ? Number(args.delayMs) : 1500;
2907
+ const fillParams = { keyword, fieldDelayMs };
2908
+ for (const k of [
2909
+ "city",
2910
+ "expectCity",
2911
+ "workYear",
2912
+ "education",
2913
+ "schoolTag",
2914
+ "language",
2915
+ "ageMin",
2916
+ "ageMax",
2917
+ "wantSalaryMin",
2918
+ "wantSalaryMax",
2919
+ "nowSalaryMin",
2920
+ "nowSalaryMax",
2921
+ "workYearsMin",
2922
+ "workYearsMax",
2923
+ "workAbroad",
2924
+ "managerial"
2925
+ ]) {
2926
+ if (args[k] !== void 0) fillParams[k] = args[k];
2927
+ }
2928
+ const searchUrl = "https://h.liepin.com/search/getConditionItem";
2929
+ const existing = await chrome.tabs.query({ url: "https://h.liepin.com/search/getConditionItem*" }).catch(() => []);
2930
+ const reusable = existing.find((t) => !!t.id && !!t.url && t.url.startsWith("https://h.liepin.com/search/"));
2931
+ let tab;
2932
+ let reusedExisting = false;
2933
+ if (reusable && reusable.id != null) {
2934
+ tab = reusable;
2935
+ reusedExisting = true;
2936
+ log("info", "hm.liepin.search reusing existing search tab", { tabId: tab.id, url: tab.url });
2937
+ } else {
2938
+ tab = await chrome.tabs.create({ url: searchUrl, active: false });
2939
+ log("info", "hm.liepin.search tab created", { tabId: tab.id, url: searchUrl, maxPages, keyword });
2940
+ }
2941
+ if (ctx.sessionId && !ctx.explicitTab && tab.id) {
2942
+ addTabToSessionGroup(ctx.sessionId, ctx.groupTitle, tab).catch(
2943
+ (err3) => log("warn", "hm.liepin.search addTabToSessionGroup failed", { error: err3.message })
2944
+ );
2945
+ }
2946
+ const leads = /* @__PURE__ */ new Map();
2947
+ const applied = [];
2948
+ const fillErrors = [];
2949
+ let pagesScanned = 0;
2950
+ const assertNotBlocked = async (phase) => {
2951
+ if (tab.id == null) return;
2952
+ const t = await chrome.tabs.get(tab.id).catch(() => null);
2953
+ if (!t) throw new Error(`tab closed while ${phase}`);
2954
+ if (!t.url || t.url === "about:blank") {
2955
+ throw new ControlBlockedError("LIE_PIN_BLANK_KILL", `liepin search aborted at ${phase}: page killed to about:blank (secscan)`);
2956
+ }
2957
+ const blocked = await cdpEvaluate(tab.id, LIEPIN_WALL_CHECK_SOURCE).catch(() => null);
2958
+ if (blocked) {
2959
+ const code = liepinBlockedCode(blocked);
2960
+ log("warn", "hm.liepin.search blocked by liepin risk control", { blocked, code, phase });
2961
+ throw new ControlBlockedError(code, `liepin search aborted at ${phase}: ${blocked}`);
2962
+ }
2963
+ };
2964
+ try {
2965
+ if (tab.id == null) throw new Error("failed_to_create_tab");
2966
+ await waitTabLoaded(tab.id, 3e4);
2967
+ await sleep(1500);
2968
+ await assertNotBlocked("after-load");
2969
+ const fillExpr = "(() => { const P = " + JSON.stringify(fillParams) + "; return (" + LIEPIN_FILL_AND_SEARCH_SOURCE + ")(P); })()";
2970
+ const fillResult = await cdpEvaluate(tab.id, fillExpr, 6e4);
2971
+ applied.push(...fillResult?.applied ?? []);
2972
+ fillErrors.push(...fillResult?.errors ?? []);
2973
+ if (!fillResult?.submitted) {
2974
+ log("warn", "hm.liepin.search fill did not submit", { fillErrors });
2975
+ throw new Error("liepin_search_fill_failed: " + fillErrors.join(";"));
2976
+ }
2977
+ log("info", "hm.liepin.search form submitted", { applied, fillErrors });
2978
+ for (let page = 1; page <= maxPages; page++) {
2979
+ if (tab.id == null) break;
2980
+ const waited = await cdpEvaluate(tab.id, LIEPIN_RESULT_WAIT_SOURCE, 3e4).catch(() => false);
2981
+ await sleep(1200);
2982
+ await assertNotBlocked(`page-${page}`);
2983
+ if (waited === "empty") {
2984
+ log("info", "hm.liepin.search empty result", { page });
2985
+ break;
2986
+ }
2987
+ const extracted = await cdpEvaluate(tab.id, LIEPIN_EXTRACT_SOURCE, 3e4);
2988
+ const pageLeads = extracted?.links?.talent || [];
2989
+ for (const l of pageLeads) {
2990
+ if (l.url && !leads.has(l.url)) {
2991
+ leads.set(l.url, {
2992
+ url: l.url,
2993
+ name: l.name ?? null,
2994
+ titleLine: l.titleLine ?? null,
2995
+ resId: l.resId ?? null,
2996
+ page,
2997
+ source: "liepin"
2998
+ });
2999
+ }
3000
+ }
3001
+ pagesScanned = page;
3002
+ log("info", "hm.liepin.search page extracted", { page, pageLeads: pageLeads.length, total: leads.size });
3003
+ if (page >= maxPages) break;
3004
+ const clicked = await cdpEvaluate(tab.id, LIEPIN_NEXT_CLICK_SOURCE).catch(() => false);
3005
+ if (!clicked) break;
3006
+ await sleep(3e3 + Math.random() * 2e3);
3007
+ await assertNotBlocked(`page-${page}-next`);
3008
+ }
3009
+ } finally {
3010
+ if (!keepTab && !reusedExisting && tab.id != null) {
3011
+ await closeTabGracefully(tab.id);
3012
+ }
3013
+ }
3014
+ return {
3015
+ keyword,
3016
+ searchUrl,
3017
+ pagesScanned,
3018
+ leadCount: leads.size,
3019
+ leads: Array.from(leads.values()),
3020
+ applied,
3021
+ fillErrors,
3022
+ keepTab,
3023
+ reusedExisting,
3024
+ // tabId 如实记录:自建 tab 关闭后置 null;复用 tab 一定返回其 id(调用方可追踪/不再开)
3025
+ tabId: reusedExisting ? tab.id ?? null : keepTab ? tab.id ?? null : null
2389
3026
  };
2390
3027
  }
2391
3028
  var HIREDCHINA_EXTRACT_SOURCE = String.raw`(async () => {
@@ -2611,6 +3248,14 @@ async function handleCookiesCommand(args) {
2611
3248
  }
2612
3249
  throw new Error(`cookies_unknown_action: ${action}`);
2613
3250
  }
3251
+ async function closeTabGracefully(tabId) {
3252
+ await detachIfAttached(tabId);
3253
+ await Promise.race([
3254
+ chrome.tabs.remove(tabId),
3255
+ new Promise((_, reject) => setTimeout(() => reject(new Error("tab_remove_timeout")), 5e3))
3256
+ ]).catch(() => {
3257
+ });
3258
+ }
2614
3259
  async function handleTabCommand(command, args, log) {
2615
3260
  const tabArg = args.tab ?? args.targetId ?? args.page;
2616
3261
  switch (command) {
@@ -2632,6 +3277,23 @@ async function handleTabCommand(command, args, log) {
2632
3277
  log("info", "cp.tab.new created", { page: String(tab.id), url: tab.url ?? url, active });
2633
3278
  return { page: String(tab.id), url: tab.url ?? url };
2634
3279
  }
3280
+ case "cp.tab.open": {
3281
+ const url = typeof args.url === "string" && args.url.trim() ? args.url.trim() : "";
3282
+ if (!url) throw new Error("tab_open_requires_url");
3283
+ if (!/^https?:\/\//.test(url)) throw new Error("tab_open_requires_http_url: " + url);
3284
+ const active = args.active === true;
3285
+ const pattern = url.includes("*") ? url : url + "*";
3286
+ const candidates = await chrome.tabs.query({ url: pattern }).catch(() => []);
3287
+ const hit = candidates.find((t) => !!t.id && !!t.url && t.url.startsWith("http"));
3288
+ if (hit && hit.id != null) {
3289
+ await chrome.tabs.update(hit.id, { active }).catch(() => void 0);
3290
+ log("info", "cp.tab.open reused existing tab", { page: String(hit.id), url: hit.url, active });
3291
+ return { page: String(hit.id), url: hit.url, reused: true };
3292
+ }
3293
+ const tab = await chrome.tabs.create({ url, active });
3294
+ log("info", "cp.tab.open created tab", { page: String(tab.id), url: tab.url ?? url, active });
3295
+ return { page: String(tab.id), url: tab.url ?? url, reused: false };
3296
+ }
2635
3297
  case "cp.tab.select": {
2636
3298
  if (!tabArg) throw new Error("tab_select_requires_tab_id");
2637
3299
  const tabId = Number(tabArg);
@@ -2642,7 +3304,7 @@ async function handleTabCommand(command, args, log) {
2642
3304
  }
2643
3305
  case "cp.tab.close": {
2644
3306
  if (!tabArg) throw new Error("tab_close_requires_tab_id");
2645
- await chrome.tabs.remove(Number(tabArg));
3307
+ await closeTabGracefully(Number(tabArg));
2646
3308
  return { closed: true, page: String(tabArg) };
2647
3309
  }
2648
3310
  case "cp.tab.bind": {
@@ -2825,7 +3487,7 @@ function maybeDiscover(url, title) {
2825
3487
  });
2826
3488
  }
2827
3489
 
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
3490
+ // ../../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
3491
  function stringifyArg(arg) {
2830
3492
  if (typeof arg === "string")
2831
3493
  return arg;
@@ -2977,7 +3639,7 @@ var Logger = class {
2977
3639
  }
2978
3640
  };
2979
3641
 
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
3642
+ // ../../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
3643
  async function createHttpTransport(opts) {
2982
3644
  const serverUrl = opts.serverUrl.replace(/\/$/, "");
2983
3645
  return async (logs) => {
@@ -3164,36 +3826,58 @@ createBackgroundLogForwarder({
3164
3826
  }).then((f) => {
3165
3827
  logForwarder = f;
3166
3828
  });
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();
3829
+ var OFFSCREEN_URL = "offscreen/offscreen.html";
3830
+ function sendToOffscreen(raw) {
3831
+ chrome.runtime.sendMessage({ type: "offscreen:send", raw }).catch(() => {
3832
+ });
3833
+ }
3834
+ function sendInitToOffscreen() {
3835
+ const manifest = chrome.runtime.getManifest();
3836
+ chrome.runtime.sendMessage({ type: "offscreen:init", extensionId: chrome.runtime.id, version: manifest.version }).catch(() => {
3837
+ });
3838
+ }
3839
+ async function ensureOffscreen() {
3840
+ try {
3841
+ const contexts = await chrome.runtime.getContexts({
3842
+ contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT]
3843
+ });
3844
+ if (contexts.length > 0) {
3845
+ sendInitToOffscreen();
3846
+ return;
3847
+ }
3187
3848
  try {
3188
- socket.send(item.raw);
3189
- } catch (e) {
3190
- console.error("[HunterMate] flush send failed", e);
3191
- break;
3849
+ await chrome.offscreen.createDocument({
3850
+ url: OFFSCREEN_URL,
3851
+ // D1: WEBSOCKET 枚举从未在正式 Chrome 落地(152 实测抛枚举非法),必须用 WORKERS;
3852
+ // @types/chrome 0.0.268 的 Reason 枚举同样没有 WEBSOCKET 成员,断言方向一致
3853
+ reasons: ["WORKERS"],
3854
+ justification: "hunter-mate \u9700\u4E0E\u672C\u5730 server \u4FDD\u6301\u5E38\u9A7B WebSocket \u8FDE\u63A5(MV3 SW \u4F1A\u88AB\u7A7A\u95F2\u56DE\u6536)"
3855
+ });
3856
+ sendInitToOffscreen();
3857
+ } catch (createErr) {
3858
+ const msg = createErr instanceof Error ? createErr.message : String(createErr);
3859
+ sendLog("warn", "ensureOffscreen createDocument failed, retry after closeDocument", { error: msg });
3860
+ try {
3861
+ await chrome.offscreen.closeDocument();
3862
+ } catch {
3863
+ }
3864
+ await new Promise((r) => setTimeout(r, 1e3));
3865
+ await chrome.offscreen.createDocument({
3866
+ url: OFFSCREEN_URL,
3867
+ reasons: ["WORKERS"],
3868
+ justification: "hunter-mate \u9700\u4E0E\u672C\u5730 server \u4FDD\u6301\u5E38\u9A7B WebSocket \u8FDE\u63A5(MV3 SW \u4F1A\u88AB\u7A7A\u95F2\u56DE\u6536)"
3869
+ });
3870
+ sendInitToOffscreen();
3192
3871
  }
3872
+ } catch (err3) {
3873
+ console.error("[HunterMate] ensureOffscreen failed", err3);
3874
+ sendLog("error", "ensureOffscreen failed", { error: err3 instanceof Error ? err3.message : String(err3) });
3193
3875
  }
3194
- if (queuedDropped > 0) {
3195
- console.warn(`[HunterMate] send queue overflow, dropped ${queuedDropped} incremental events (snapshots kept)`);
3196
- queuedDropped = 0;
3876
+ try {
3877
+ const st = await chrome.runtime.sendMessage({ type: "offscreen:get-state" });
3878
+ state.wsConnected = !!st?.wsConnected;
3879
+ broadcastState();
3880
+ } catch {
3197
3881
  }
3198
3882
  }
3199
3883
  function sendLog(level, message, meta) {
@@ -3201,14 +3885,7 @@ function sendLog(level, message, meta) {
3201
3885
  backgroundLogger[level === "debug" ? "debug" : level]("extension:sendLog", { message, meta });
3202
3886
  } catch {
3203
3887
  }
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
- }
3888
+ sendToOffscreen(JSON.stringify({ type: "log", level, message, meta }));
3212
3889
  const prefix = `[HunterMate] ${message}`;
3213
3890
  if (level === "debug") {
3214
3891
  console.debug(prefix, meta ?? "");
@@ -3281,101 +3958,72 @@ function broadcastState() {
3281
3958
  });
3282
3959
  });
3283
3960
  }
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,
3961
+ async function handleServerMessage(data) {
3962
+ if (!data || typeof data !== "object") return;
3963
+ if (data.type === "pong" || data.type === "status" || data.type === "ack") return;
3964
+ if (data.type === "reload") {
3965
+ console.log("[HunterMate] received reload broadcast, reloading extension in 200ms...");
3966
+ setTimeout(() => chrome.runtime.reload(), 200);
3967
+ return;
3968
+ }
3969
+ if (data.type === "cmd" && data.requestId) {
3970
+ const startedAt = Date.now();
3971
+ sendLog("info", "extension handling control command", { command: data.command, requestId: data.requestId, params: data.params });
3972
+ let outcome;
3973
+ if (data.command === "hm.status") {
3974
+ const manifest = chrome.runtime.getManifest();
3975
+ outcome = {
3355
3976
  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
3977
+ result: {
3978
+ ...state,
3979
+ tabStatesCount: tabStates.size,
3980
+ extBasePath: chrome.runtime.getURL(""),
3981
+ manifestPermissions: manifest.permissions,
3982
+ hasSidePanelInManifest: manifest.permissions?.includes("sidePanel"),
3983
+ chromeSidePanelType: typeof chrome.sidePanel,
3984
+ wsHeldBy: "offscreen"
3985
+ }
3366
3986
  };
3987
+ } else {
3367
3988
  try {
3368
- socket?.send(JSON.stringify(resp));
3369
- sendLog("debug", "cmd_response sent", { requestId: data.requestId, status: resp.status, durationMs: resp.durationMs });
3989
+ outcome = await executeControlCommand(String(data.command ?? ""), data.params ?? {}, {
3990
+ onLog: (level, message, meta) => sendLog(level, message, meta),
3991
+ sessionId: typeof data.sessionId === "string" ? data.sessionId : void 0
3992
+ });
3370
3993
  } catch (e) {
3371
- sendLog("error", "cmd_response send failed", { requestId: data.requestId, error: e instanceof Error ? e.message : String(e) });
3994
+ outcome = { status: "error", message: e instanceof Error ? e.message : String(e) };
3372
3995
  }
3373
- return;
3374
3996
  }
3375
- console.log("[HunterMate] server message", data);
3376
- };
3997
+ const resp = outcome.status === "ok" ? {
3998
+ type: "cmd_response",
3999
+ requestId: data.requestId,
4000
+ status: "ok",
4001
+ // undefined 显式归一为 null:JSON.stringify 会丢 undefined 键,
4002
+ // 调用方无法区分"无 result 键"与"result 为 undefined"
4003
+ result: outcome.result === void 0 ? null : outcome.result,
4004
+ durationMs: Date.now() - startedAt
4005
+ } : {
4006
+ type: "cmd_response",
4007
+ requestId: data.requestId,
4008
+ status: "error",
4009
+ error: { code: outcome.code, message: outcome.message },
4010
+ durationMs: Date.now() - startedAt
4011
+ };
4012
+ sendToOffscreen(JSON.stringify(resp));
4013
+ sendLog("debug", "cmd_response sent", { requestId: data.requestId, status: resp.status, durationMs: resp.durationMs });
4014
+ }
3377
4015
  }
3378
- connect();
4016
+ chrome.runtime.onMessage.addListener((message) => {
4017
+ if (message?.type === "offscreen:ws-state") {
4018
+ state.wsConnected = !!message.wsConnected;
4019
+ broadcastState();
4020
+ return;
4021
+ }
4022
+ if (message?.type === "offscreen:ws-message") {
4023
+ void handleServerMessage(message.payload);
4024
+ return;
4025
+ }
4026
+ });
3379
4027
  var RECONNECT_ALARM_NAME = "hm:reconnect";
3380
4028
  function ensureReconnectAlarm() {
3381
4029
  chrome.alarms.create(RECONNECT_ALARM_NAME, { periodInMinutes: 0.5 }).catch((err3) => console.error("[HunterMate] create reconnect alarm failed", err3));
@@ -3383,21 +4031,24 @@ function ensureReconnectAlarm() {
3383
4031
  chrome.alarms.onAlarm.addListener((alarm) => {
3384
4032
  if (alarm.name !== RECONNECT_ALARM_NAME) return;
3385
4033
  checkHttpHealth();
3386
- if (socket && socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) {
3387
- socket = null;
3388
- }
3389
- if (!socket) {
3390
- connect();
3391
- }
4034
+ sendLog("debug", "sw:alarm-tick", { wsConnected: state.wsConnected });
4035
+ void ensureOffscreen();
3392
4036
  });
3393
4037
  ensureReconnectAlarm();
4038
+ void ensureOffscreen();
4039
+ chrome.runtime.onInstalled.addListener(() => {
4040
+ console.log("[HunterMate] onInstalled: cold-start offscreen");
4041
+ ensureReconnectAlarm();
4042
+ void ensureOffscreen();
4043
+ });
4044
+ chrome.runtime.onStartup.addListener(() => {
4045
+ console.log("[HunterMate] onStartup: cold-start offscreen");
4046
+ ensureReconnectAlarm();
4047
+ void ensureOffscreen();
4048
+ });
3394
4049
  setInterval(() => {
3395
4050
  checkHttpHealth();
3396
- if (socket?.readyState === WebSocket.OPEN) {
3397
- socket.send(JSON.stringify({ type: "ping" }));
3398
- } else {
3399
- connect();
3400
- }
4051
+ void ensureOffscreen();
3401
4052
  broadcastState();
3402
4053
  }, 2e4);
3403
4054
  setInterval(() => {
@@ -3433,6 +4084,14 @@ chrome.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
3433
4084
  }
3434
4085
  if (changeInfo.url) {
3435
4086
  broadcastState();
4087
+ if (state.wsConnected) {
4088
+ sendToOffscreen(JSON.stringify({ type: "tab_event", event: "navigated", tabId: _tabId, url: changeInfo.url, ts: Date.now() }));
4089
+ }
4090
+ }
4091
+ });
4092
+ chrome.tabs.onCreated.addListener((t) => {
4093
+ if (state.wsConnected) {
4094
+ sendToOffscreen(JSON.stringify({ type: "tab_event", event: "created", tabId: t.id, url: t.url ?? null, openerTabId: t.openerTabId ?? null, windowId: t.windowId, index: t.index, ts: Date.now() }));
3436
4095
  }
3437
4096
  });
3438
4097
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
@@ -3482,11 +4141,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
3482
4141
  if (message.type === "recording-event") {
3483
4142
  const tabId = sender.tab?.id ?? -1;
3484
4143
  const payload = { ...message.payload, tabId };
3485
- if (socket?.readyState === WebSocket.OPEN) {
3486
- socket.send(JSON.stringify(payload));
3487
- } else {
3488
- enqueueRecordingEvent(payload);
3489
- }
4144
+ sendToOffscreen(JSON.stringify(payload));
3490
4145
  const ts = tabStates.get(tabId);
3491
4146
  if (ts && message.payload?.sessionId === ts.recordingSessionId) {
3492
4147
  ts.eventCount++;