hunter-mate 0.1.2 → 0.1.3
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.
- package/dist/assets/extension/_locales/en_US/messages.json +5 -0
- package/dist/assets/extension/_locales/zh_CN/messages.json +5 -0
- package/dist/assets/extension/background/service-worker.js +3253 -0
- package/dist/assets/extension/content/index.js +13660 -0
- package/dist/assets/extension/manifest.json +49 -0
- package/dist/assets/extension/package.json +24 -0
- package/dist/assets/extension/sidepanel/index.html +400 -0
- package/dist/assets/extension/sidepanel/index.js +852 -0
- package/dist/assets/server/agent-session/agent-session.controller.d.ts +32 -0
- package/dist/assets/server/agent-session/agent-session.controller.js +83 -0
- package/dist/assets/server/agent-session/agent-session.controller.js.map +1 -0
- package/dist/assets/server/agent-session/agent-session.guard.d.ts +4 -0
- package/dist/assets/server/agent-session/agent-session.guard.js +32 -0
- package/dist/assets/server/agent-session/agent-session.guard.js.map +1 -0
- package/dist/assets/server/agent-session/agent-session.module.d.ts +2 -0
- package/dist/assets/server/agent-session/agent-session.module.js +28 -0
- package/dist/assets/server/agent-session/agent-session.module.js.map +1 -0
- package/dist/assets/server/agent-session/agent-session.service.d.ts +54 -0
- package/dist/assets/server/agent-session/agent-session.service.js +431 -0
- package/dist/assets/server/agent-session/agent-session.service.js.map +1 -0
- package/dist/assets/server/agent-session/agent-session.types.d.ts +56 -0
- package/dist/assets/server/agent-session/agent-session.types.js +3 -0
- package/dist/assets/server/agent-session/agent-session.types.js.map +1 -0
- package/dist/assets/server/agent-session/entities/agent-event.entity.d.ts +12 -0
- package/dist/assets/server/agent-session/entities/agent-event.entity.js +73 -0
- package/dist/assets/server/agent-session/entities/agent-event.entity.js.map +1 -0
- package/dist/assets/server/agent-session/entities/agent-message.entity.d.ts +13 -0
- package/dist/assets/server/agent-session/entities/agent-message.entity.js +77 -0
- package/dist/assets/server/agent-session/entities/agent-message.entity.js.map +1 -0
- package/dist/assets/server/agent-session/entities/agent-session.entity.d.ts +14 -0
- package/dist/assets/server/agent-session/entities/agent-session.entity.js +83 -0
- package/dist/assets/server/agent-session/entities/agent-session.entity.js.map +1 -0
- package/dist/assets/server/app.module.d.ts +4 -0
- package/dist/assets/server/app.module.js +125 -0
- package/dist/assets/server/app.module.js.map +1 -0
- package/dist/assets/server/auth/auth.controller.d.ts +22 -0
- package/dist/assets/server/auth/auth.controller.js +88 -0
- package/dist/assets/server/auth/auth.controller.js.map +1 -0
- package/dist/assets/server/auth/auth.middleware.d.ts +5 -0
- package/dist/assets/server/auth/auth.middleware.js +34 -0
- package/dist/assets/server/auth/auth.middleware.js.map +1 -0
- package/dist/assets/server/auth/auth.module.d.ts +2 -0
- package/dist/assets/server/auth/auth.module.js +24 -0
- package/dist/assets/server/auth/auth.module.js.map +1 -0
- package/dist/assets/server/auth/auth.service.d.ts +30 -0
- package/dist/assets/server/auth/auth.service.js +183 -0
- package/dist/assets/server/auth/auth.service.js.map +1 -0
- package/dist/assets/server/auth/auth.types.d.ts +28 -0
- package/dist/assets/server/auth/auth.types.js +3 -0
- package/dist/assets/server/auth/auth.types.js.map +1 -0
- package/dist/assets/server/build-info.json +4 -0
- package/dist/assets/server/common/common.module.d.ts +2 -0
- package/dist/assets/server/common/common.module.js +22 -0
- package/dist/assets/server/common/common.module.js.map +1 -0
- package/dist/assets/server/common/crypto.service.d.ts +11 -0
- package/dist/assets/server/common/crypto.service.js +73 -0
- package/dist/assets/server/common/crypto.service.js.map +1 -0
- package/dist/assets/server/communication/communication-record.entity.d.ts +15 -0
- package/dist/assets/server/communication/communication-record.entity.js +80 -0
- package/dist/assets/server/communication/communication-record.entity.js.map +1 -0
- package/dist/assets/server/communication/communication.controller.d.ts +12 -0
- package/dist/assets/server/communication/communication.controller.js +61 -0
- package/dist/assets/server/communication/communication.controller.js.map +1 -0
- package/dist/assets/server/communication/communication.module.d.ts +2 -0
- package/dist/assets/server/communication/communication.module.js +25 -0
- package/dist/assets/server/communication/communication.module.js.map +1 -0
- package/dist/assets/server/communication/communication.service.d.ts +23 -0
- package/dist/assets/server/communication/communication.service.js +51 -0
- package/dist/assets/server/communication/communication.service.js.map +1 -0
- package/dist/assets/server/db/schema-validator.service.d.ts +25 -0
- package/dist/assets/server/db/schema-validator.service.js +107 -0
- package/dist/assets/server/db/schema-validator.service.js.map +1 -0
- package/dist/assets/server/db/schema-version.entity.d.ts +5 -0
- package/dist/assets/server/db/schema-version.entity.js +35 -0
- package/dist/assets/server/db/schema-version.entity.js.map +1 -0
- package/dist/assets/server/db/sqlite.module.d.ts +2 -0
- package/dist/assets/server/db/sqlite.module.js +79 -0
- package/dist/assets/server/db/sqlite.module.js.map +1 -0
- package/dist/assets/server/devops/devops-sink-init.d.ts +10 -0
- package/dist/assets/server/devops/devops-sink-init.js +96 -0
- package/dist/assets/server/devops/devops-sink-init.js.map +1 -0
- package/dist/assets/server/devops/devops-sink-init.spec.d.ts +1 -0
- package/dist/assets/server/devops/devops-sink-init.spec.js +71 -0
- package/dist/assets/server/devops/devops-sink-init.spec.js.map +1 -0
- package/dist/assets/server/devops/devops-sink.module.d.ts +2 -0
- package/dist/assets/server/devops/devops-sink.module.js +22 -0
- package/dist/assets/server/devops/devops-sink.module.js.map +1 -0
- package/dist/assets/server/extension/extension-control.controller.d.ts +67 -0
- package/dist/assets/server/extension/extension-control.controller.js +173 -0
- package/dist/assets/server/extension/extension-control.controller.js.map +1 -0
- package/dist/assets/server/extension/extension-control.service.d.ts +46 -0
- package/dist/assets/server/extension/extension-control.service.js +212 -0
- package/dist/assets/server/extension/extension-control.service.js.map +1 -0
- package/dist/assets/server/extension/extension-core.module.d.ts +2 -0
- package/dist/assets/server/extension/extension-core.module.js +21 -0
- package/dist/assets/server/extension/extension-core.module.js.map +1 -0
- package/dist/assets/server/extension/extension-version.controller.d.ts +6 -0
- package/dist/assets/server/extension/extension-version.controller.js +41 -0
- package/dist/assets/server/extension/extension-version.controller.js.map +1 -0
- package/dist/assets/server/extension/extension.module.d.ts +2 -0
- package/dist/assets/server/extension/extension.module.js +25 -0
- package/dist/assets/server/extension/extension.module.js.map +1 -0
- package/dist/assets/server/extraction/adapter.service.d.ts +41 -0
- package/dist/assets/server/extraction/adapter.service.js +161 -0
- package/dist/assets/server/extraction/adapter.service.js.map +1 -0
- package/dist/assets/server/extraction/entities/company-profile.entity.d.ts +14 -0
- package/dist/assets/server/extraction/entities/company-profile.entity.js +81 -0
- package/dist/assets/server/extraction/entities/company-profile.entity.js.map +1 -0
- package/dist/assets/server/extraction/entities/job-posting.entity.d.ts +15 -0
- package/dist/assets/server/extraction/entities/job-posting.entity.js +86 -0
- package/dist/assets/server/extraction/entities/job-posting.entity.js.map +1 -0
- package/dist/assets/server/extraction/entities/parse-log.entity.d.ts +15 -0
- package/dist/assets/server/extraction/entities/parse-log.entity.js +87 -0
- package/dist/assets/server/extraction/entities/parse-log.entity.js.map +1 -0
- package/dist/assets/server/extraction/entities/talent-profile.entity.d.ts +15 -0
- package/dist/assets/server/extraction/entities/talent-profile.entity.js +86 -0
- package/dist/assets/server/extraction/entities/talent-profile.entity.js.map +1 -0
- package/dist/assets/server/extraction/extraction-entity.controller.d.ts +100 -0
- package/dist/assets/server/extraction/extraction-entity.controller.js +310 -0
- package/dist/assets/server/extraction/extraction-entity.controller.js.map +1 -0
- package/dist/assets/server/extraction/extraction-rule.controller.d.ts +7 -0
- package/dist/assets/server/extraction/extraction-rule.controller.js +48 -0
- package/dist/assets/server/extraction/extraction-rule.controller.js.map +1 -0
- package/dist/assets/server/extraction/extraction-rule.entity.d.ts +15 -0
- package/dist/assets/server/extraction/extraction-rule.entity.js +80 -0
- package/dist/assets/server/extraction/extraction-rule.entity.js.map +1 -0
- package/dist/assets/server/extraction/extraction-rule.service.d.ts +24 -0
- package/dist/assets/server/extraction/extraction-rule.service.js +81 -0
- package/dist/assets/server/extraction/extraction-rule.service.js.map +1 -0
- package/dist/assets/server/extraction/extraction.module.d.ts +2 -0
- package/dist/assets/server/extraction/extraction.module.js +43 -0
- package/dist/assets/server/extraction/extraction.module.js.map +1 -0
- package/dist/assets/server/extraction/extraction.service.d.ts +62 -0
- package/dist/assets/server/extraction/extraction.service.js +347 -0
- package/dist/assets/server/extraction/extraction.service.js.map +1 -0
- package/dist/assets/server/extraction/script-rule.d.ts +13 -0
- package/dist/assets/server/extraction/script-rule.js +45 -0
- package/dist/assets/server/extraction/script-rule.js.map +1 -0
- package/dist/assets/server/extraction/structured-data.d.ts +7 -0
- package/dist/assets/server/extraction/structured-data.js +96 -0
- package/dist/assets/server/extraction/structured-data.js.map +1 -0
- package/dist/assets/server/hcapi/hcapi.module.d.ts +2 -0
- package/dist/assets/server/hcapi/hcapi.module.js +23 -0
- package/dist/assets/server/hcapi/hcapi.module.js.map +1 -0
- package/dist/assets/server/hcapi/hcapi.service.d.ts +28 -0
- package/dist/assets/server/hcapi/hcapi.service.js +81 -0
- package/dist/assets/server/hcapi/hcapi.service.js.map +1 -0
- package/dist/assets/server/health/health.controller.d.ts +19 -0
- package/dist/assets/server/health/health.controller.js +59 -0
- package/dist/assets/server/health/health.controller.js.map +1 -0
- package/dist/assets/server/jobcase/jobcase.controller.d.ts +12 -0
- package/dist/assets/server/jobcase/jobcase.controller.js +61 -0
- package/dist/assets/server/jobcase/jobcase.controller.js.map +1 -0
- package/dist/assets/server/jobcase/jobcase.entity.d.ts +13 -0
- package/dist/assets/server/jobcase/jobcase.entity.js +71 -0
- package/dist/assets/server/jobcase/jobcase.entity.js.map +1 -0
- package/dist/assets/server/jobcase/jobcase.module.d.ts +2 -0
- package/dist/assets/server/jobcase/jobcase.module.js +26 -0
- package/dist/assets/server/jobcase/jobcase.module.js.map +1 -0
- package/dist/assets/server/jobcase/jobcase.service.d.ts +21 -0
- package/dist/assets/server/jobcase/jobcase.service.js +56 -0
- package/dist/assets/server/jobcase/jobcase.service.js.map +1 -0
- package/dist/assets/server/llm/llm.module.d.ts +2 -0
- package/dist/assets/server/llm/llm.module.js +21 -0
- package/dist/assets/server/llm/llm.module.js.map +1 -0
- package/dist/assets/server/llm/llm.service.d.ts +18 -0
- package/dist/assets/server/llm/llm.service.js +65 -0
- package/dist/assets/server/llm/llm.service.js.map +1 -0
- package/dist/assets/server/main.d.ts +1 -0
- package/dist/assets/server/main.js +30 -0
- package/dist/assets/server/main.js.map +1 -0
- package/dist/assets/server/matching/matching.controller.d.ts +16 -0
- package/dist/assets/server/matching/matching.controller.js +97 -0
- package/dist/assets/server/matching/matching.controller.js.map +1 -0
- package/dist/assets/server/matching/matching.module.d.ts +2 -0
- package/dist/assets/server/matching/matching.module.js +36 -0
- package/dist/assets/server/matching/matching.module.js.map +1 -0
- package/dist/assets/server/matching/matching.service.d.ts +55 -0
- package/dist/assets/server/matching/matching.service.js +409 -0
- package/dist/assets/server/matching/matching.service.js.map +1 -0
- package/dist/assets/server/package.json +57 -0
- package/dist/assets/server/parsed-resume/parsed-resume.controller.d.ts +6 -0
- package/dist/assets/server/parsed-resume/parsed-resume.controller.js +42 -0
- package/dist/assets/server/parsed-resume/parsed-resume.controller.js.map +1 -0
- package/dist/assets/server/parsed-resume/parsed-resume.entity.d.ts +124 -0
- package/dist/assets/server/parsed-resume/parsed-resume.entity.js +72 -0
- package/dist/assets/server/parsed-resume/parsed-resume.entity.js.map +1 -0
- package/dist/assets/server/parsed-resume/parsed-resume.module.d.ts +2 -0
- package/dist/assets/server/parsed-resume/parsed-resume.module.js +26 -0
- package/dist/assets/server/parsed-resume/parsed-resume.module.js.map +1 -0
- package/dist/assets/server/parsed-resume/parsed-resume.service.d.ts +10 -0
- package/dist/assets/server/parsed-resume/parsed-resume.service.js +45 -0
- package/dist/assets/server/parsed-resume/parsed-resume.service.js.map +1 -0
- package/dist/assets/server/parsing/parse-error-report.entity.d.ts +11 -0
- package/dist/assets/server/parsing/parse-error-report.entity.js +66 -0
- package/dist/assets/server/parsing/parse-error-report.entity.js.map +1 -0
- package/dist/assets/server/parsing/parsing.controller.d.ts +19 -0
- package/dist/assets/server/parsing/parsing.controller.js +97 -0
- package/dist/assets/server/parsing/parsing.controller.js.map +1 -0
- package/dist/assets/server/parsing/parsing.module.d.ts +2 -0
- package/dist/assets/server/parsing/parsing.module.js +38 -0
- package/dist/assets/server/parsing/parsing.module.js.map +1 -0
- package/dist/assets/server/parsing/parsing.service.d.ts +38 -0
- package/dist/assets/server/parsing/parsing.service.js +263 -0
- package/dist/assets/server/parsing/parsing.service.js.map +1 -0
- package/dist/assets/server/query/fields-meta.controller.d.ts +28 -0
- package/dist/assets/server/query/fields-meta.controller.js +57 -0
- package/dist/assets/server/query/fields-meta.controller.js.map +1 -0
- package/dist/assets/server/query/query-builder.controller.d.ts +11 -0
- package/dist/assets/server/query/query-builder.controller.js +40 -0
- package/dist/assets/server/query/query-builder.controller.js.map +1 -0
- package/dist/assets/server/query/query-builder.service.d.ts +8 -0
- package/dist/assets/server/query/query-builder.service.js +41 -0
- package/dist/assets/server/query/query-builder.service.js.map +1 -0
- package/dist/assets/server/query/query.module.d.ts +2 -0
- package/dist/assets/server/query/query.module.js +23 -0
- package/dist/assets/server/query/query.module.js.map +1 -0
- package/dist/assets/server/recording/entities/recording-event.entity.d.ts +9 -0
- package/dist/assets/server/recording/entities/recording-event.entity.js +56 -0
- package/dist/assets/server/recording/entities/recording-event.entity.js.map +1 -0
- package/dist/assets/server/recording/recording.controller.d.ts +52 -0
- package/dist/assets/server/recording/recording.controller.js +73 -0
- package/dist/assets/server/recording/recording.controller.js.map +1 -0
- package/dist/assets/server/recording/recording.gateway.d.ts +15 -0
- package/dist/assets/server/recording/recording.gateway.js +117 -0
- package/dist/assets/server/recording/recording.gateway.js.map +1 -0
- package/dist/assets/server/recording/recording.module.d.ts +2 -0
- package/dist/assets/server/recording/recording.module.js +29 -0
- package/dist/assets/server/recording/recording.module.js.map +1 -0
- package/dist/assets/server/recording/recording.service.d.ts +59 -0
- package/dist/assets/server/recording/recording.service.js +111 -0
- package/dist/assets/server/recording/recording.service.js.map +1 -0
- package/dist/assets/server/remark/remark.controller.d.ts +21 -0
- package/dist/assets/server/remark/remark.controller.js +59 -0
- package/dist/assets/server/remark/remark.controller.js.map +1 -0
- package/dist/assets/server/remark/remark.entity.d.ts +18 -0
- package/dist/assets/server/remark/remark.entity.js +67 -0
- package/dist/assets/server/remark/remark.entity.js.map +1 -0
- package/dist/assets/server/remark/remark.module.d.ts +2 -0
- package/dist/assets/server/remark/remark.module.js +26 -0
- package/dist/assets/server/remark/remark.module.js.map +1 -0
- package/dist/assets/server/remark/remark.service.d.ts +8 -0
- package/dist/assets/server/remark/remark.service.js +39 -0
- package/dist/assets/server/remark/remark.service.js.map +1 -0
- package/dist/assets/server/report/report.controller.d.ts +18 -0
- package/dist/assets/server/report/report.controller.js +41 -0
- package/dist/assets/server/report/report.controller.js.map +1 -0
- package/dist/assets/server/report/report.module.d.ts +2 -0
- package/dist/assets/server/report/report.module.js +24 -0
- package/dist/assets/server/report/report.module.js.map +1 -0
- package/dist/assets/server/report/report.service.d.ts +23 -0
- package/dist/assets/server/report/report.service.js +54 -0
- package/dist/assets/server/report/report.service.js.map +1 -0
- package/dist/assets/server/rules/rules.controller.d.ts +6 -0
- package/dist/assets/server/rules/rules.controller.js +35 -0
- package/dist/assets/server/rules/rules.controller.js.map +1 -0
- package/dist/assets/server/rules/rules.module.d.ts +2 -0
- package/dist/assets/server/rules/rules.module.js +23 -0
- package/dist/assets/server/rules/rules.module.js.map +1 -0
- package/dist/assets/server/rules/rules.service.d.ts +10 -0
- package/dist/assets/server/rules/rules.service.js +26 -0
- package/dist/assets/server/rules/rules.service.js.map +1 -0
- package/dist/assets/server/search/search.controller.d.ts +6 -0
- package/dist/assets/server/search/search.controller.js +53 -0
- package/dist/assets/server/search/search.controller.js.map +1 -0
- package/dist/assets/server/search/search.module.d.ts +2 -0
- package/dist/assets/server/search/search.module.js +24 -0
- package/dist/assets/server/search/search.module.js.map +1 -0
- package/dist/assets/server/search/search.service.d.ts +6 -0
- package/dist/assets/server/search/search.service.js +46 -0
- package/dist/assets/server/search/search.service.js.map +1 -0
- package/dist/assets/server/sentry.d.ts +3 -0
- package/dist/assets/server/sentry.js +10 -0
- package/dist/assets/server/sentry.js.map +1 -0
- package/dist/assets/server/site/entities/site-registry.entity.d.ts +11 -0
- package/dist/assets/server/site/entities/site-registry.entity.js +66 -0
- package/dist/assets/server/site/entities/site-registry.entity.js.map +1 -0
- package/dist/assets/server/site/site.controller.d.ts +36 -0
- package/dist/assets/server/site/site.controller.js +60 -0
- package/dist/assets/server/site/site.controller.js.map +1 -0
- package/dist/assets/server/site/site.module.d.ts +2 -0
- package/dist/assets/server/site/site.module.js +26 -0
- package/dist/assets/server/site/site.module.js.map +1 -0
- package/dist/assets/server/site/site.service.d.ts +37 -0
- package/dist/assets/server/site/site.service.js +134 -0
- package/dist/assets/server/site/site.service.js.map +1 -0
- package/dist/assets/server/snapshot/raw-snapshot.entity.d.ts +35 -0
- package/dist/assets/server/snapshot/raw-snapshot.entity.js +62 -0
- package/dist/assets/server/snapshot/raw-snapshot.entity.js.map +1 -0
- package/dist/assets/server/snapshot/snapshot.controller.d.ts +18 -0
- package/dist/assets/server/snapshot/snapshot.controller.js +76 -0
- package/dist/assets/server/snapshot/snapshot.controller.js.map +1 -0
- package/dist/assets/server/snapshot/snapshot.module.d.ts +2 -0
- package/dist/assets/server/snapshot/snapshot.module.js +26 -0
- package/dist/assets/server/snapshot/snapshot.module.js.map +1 -0
- package/dist/assets/server/snapshot/snapshot.service.d.ts +10 -0
- package/dist/assets/server/snapshot/snapshot.service.js +48 -0
- package/dist/assets/server/snapshot/snapshot.service.js.map +1 -0
- package/dist/assets/server/system/system.controller.d.ts +39 -0
- package/dist/assets/server/system/system.controller.js +93 -0
- package/dist/assets/server/system/system.controller.js.map +1 -0
- package/dist/assets/server/system/system.module.d.ts +2 -0
- package/dist/assets/server/system/system.module.js +26 -0
- package/dist/assets/server/system/system.module.js.map +1 -0
- package/dist/assets/server/system/system.service.d.ts +86 -0
- package/dist/assets/server/system/system.service.js +433 -0
- package/dist/assets/server/system/system.service.js.map +1 -0
- package/dist/assets/server/talent-fingerprint/fingerprint-match.service.d.ts +28 -0
- package/dist/assets/server/talent-fingerprint/fingerprint-match.service.js +186 -0
- package/dist/assets/server/talent-fingerprint/fingerprint-match.service.js.map +1 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.entity.d.ts +30 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.entity.js +55 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.entity.js.map +1 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.module.d.ts +2 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.module.js +25 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.module.js.map +1 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.service.d.ts +8 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.service.js +44 -0
- package/dist/assets/server/talent-fingerprint/talent-fingerprint.service.js.map +1 -0
- package/dist/assets/server/talent-job-match/talent-job-match.controller.d.ts +6 -0
- package/dist/assets/server/talent-job-match/talent-job-match.controller.js +42 -0
- package/dist/assets/server/talent-job-match/talent-job-match.controller.js.map +1 -0
- package/dist/assets/server/talent-job-match/talent-job-match.entity.d.ts +33 -0
- package/dist/assets/server/talent-job-match/talent-job-match.entity.js +73 -0
- package/dist/assets/server/talent-job-match/talent-job-match.entity.js.map +1 -0
- package/dist/assets/server/talent-job-match/talent-job-match.module.d.ts +2 -0
- package/dist/assets/server/talent-job-match/talent-job-match.module.js +26 -0
- package/dist/assets/server/talent-job-match/talent-job-match.module.js.map +1 -0
- package/dist/assets/server/talent-job-match/talent-job-match.service.d.ts +9 -0
- package/dist/assets/server/talent-job-match/talent-job-match.service.js +42 -0
- package/dist/assets/server/talent-job-match/talent-job-match.service.js.map +1 -0
- package/dist/assets/server/talent-portrait/portrait-merge.service.d.ts +17 -0
- package/dist/assets/server/talent-portrait/portrait-merge.service.js +200 -0
- package/dist/assets/server/talent-portrait/portrait-merge.service.js.map +1 -0
- package/dist/assets/server/talent-portrait/talent-portrait.controller.d.ts +7 -0
- package/dist/assets/server/talent-portrait/talent-portrait.controller.js +52 -0
- package/dist/assets/server/talent-portrait/talent-portrait.controller.js.map +1 -0
- package/dist/assets/server/talent-portrait/talent-portrait.entity.d.ts +91 -0
- package/dist/assets/server/talent-portrait/talent-portrait.entity.js +71 -0
- package/dist/assets/server/talent-portrait/talent-portrait.entity.js.map +1 -0
- package/dist/assets/server/talent-portrait/talent-portrait.module.d.ts +2 -0
- package/dist/assets/server/talent-portrait/talent-portrait.module.js +27 -0
- package/dist/assets/server/talent-portrait/talent-portrait.module.js.map +1 -0
- package/dist/assets/server/talent-portrait/talent-portrait.service.d.ts +10 -0
- package/dist/assets/server/talent-portrait/talent-portrait.service.js +46 -0
- package/dist/assets/server/talent-portrait/talent-portrait.service.js.map +1 -0
- package/dist/assets/server/telemetry/telemetry.module.d.ts +2 -0
- package/dist/assets/server/telemetry/telemetry.module.js +24 -0
- package/dist/assets/server/telemetry/telemetry.module.js.map +1 -0
- package/dist/assets/server/telemetry/telemetry.service.d.ts +29 -0
- package/dist/assets/server/telemetry/telemetry.service.js +116 -0
- package/dist/assets/server/telemetry/telemetry.service.js.map +1 -0
- package/dist/bundled-assets.d.ts +22 -0
- package/dist/bundled-assets.js +80 -0
- package/dist/cache/db.js +10 -54
- package/dist/commands/addons.js +46 -71
- package/dist/commands/cache.js +29 -27
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +8 -12
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/doctor.js +52 -67
- package/dist/commands/extension.js +82 -96
- package/dist/commands/extras.d.ts +31 -1
- package/dist/commands/extras.js +248 -61
- package/dist/commands/init.js +18 -46
- package/dist/commands/install.js +20 -52
- package/dist/commands/job.js +27 -65
- package/dist/commands/login.js +50 -37
- package/dist/commands/logout.js +8 -12
- package/dist/commands/ops.js +94 -67
- package/dist/commands/record.js +21 -59
- package/dist/commands/report.js +14 -18
- package/dist/commands/search.js +14 -21
- package/dist/commands/server.d.ts +1 -1
- package/dist/commands/server.js +126 -112
- package/dist/commands/status.js +23 -27
- package/dist/commands/update.js +44 -79
- package/dist/config.d.ts +3 -0
- package/dist/config.js +12 -46
- package/dist/i18n/index.js +5 -11
- package/dist/i18n/locales/en-US.json +24 -1
- package/dist/i18n/locales/zh-CN.json +24 -1
- package/dist/index.cmd +3 -0
- package/dist/index.js +83 -98
- package/dist/index.ps1 +3 -0
- package/dist/keychain.js +9 -44
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +80 -0
- package/dist/native-check.d.ts +6 -0
- package/dist/native-check.js +23 -0
- package/dist/sentry.js +4 -8
- package/dist/updater.js +15 -52
- package/dist/utils/proc.d.ts +35 -0
- package/dist/utils/proc.js +81 -0
- package/dist/utils/spawn.d.ts +19 -0
- package/dist/utils/spawn.js +60 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +183 -81
- package/dist/watchdog.d.ts +1 -0
- package/dist/watchdog.js +128 -0
- package/package.json +5 -4
- package/dist/commands/native-check.d.ts +0 -22
- package/dist/commands/native-check.js +0 -93
|
@@ -0,0 +1,3253 @@
|
|
|
1
|
+
// src/shared/messaging.ts
|
|
2
|
+
var SERVER_PORT = 34570;
|
|
3
|
+
var WS_URL = `ws://localhost:${SERVER_PORT}/ws`;
|
|
4
|
+
var SERVER_HTTP_URL = `http://localhost:${SERVER_PORT}`;
|
|
5
|
+
|
|
6
|
+
// src/shared/control-runtime.ts
|
|
7
|
+
function getRefStore() {
|
|
8
|
+
const g = globalThis;
|
|
9
|
+
if (!g.__cpRefs) {
|
|
10
|
+
g.__cpRefs = { counter: 1, refs: /* @__PURE__ */ new Map(), cssPaths: /* @__PURE__ */ new Map() };
|
|
11
|
+
}
|
|
12
|
+
return g.__cpRefs;
|
|
13
|
+
}
|
|
14
|
+
function allocRef(el) {
|
|
15
|
+
const store = getRefStore();
|
|
16
|
+
for (const [ref2, existing] of store.refs) {
|
|
17
|
+
if (existing === el) return ref2;
|
|
18
|
+
}
|
|
19
|
+
const ref = store.counter++;
|
|
20
|
+
store.refs.set(ref, el);
|
|
21
|
+
store.cssPaths.set(ref, uniqueCssPath(el));
|
|
22
|
+
return ref;
|
|
23
|
+
}
|
|
24
|
+
function uniqueCssPath(el) {
|
|
25
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
26
|
+
const parts = [];
|
|
27
|
+
let cur = el;
|
|
28
|
+
while (cur && cur !== document.body && cur !== document.documentElement) {
|
|
29
|
+
let sel = cur.tagName.toLowerCase();
|
|
30
|
+
if (cur.id) {
|
|
31
|
+
parts.unshift(`#${CSS.escape(cur.id)}`);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
const parent = cur.parentElement;
|
|
35
|
+
if (parent) {
|
|
36
|
+
const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
|
|
37
|
+
if (sameTag.length > 1) {
|
|
38
|
+
const idx = sameTag.indexOf(cur) + 1;
|
|
39
|
+
sel += `:nth-of-type(${idx})`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
parts.unshift(sel);
|
|
43
|
+
cur = parent;
|
|
44
|
+
}
|
|
45
|
+
return parts.join(" > ");
|
|
46
|
+
}
|
|
47
|
+
function err(code, message, extra) {
|
|
48
|
+
return { error: { code, message, ...extra || {} } };
|
|
49
|
+
}
|
|
50
|
+
var ERROR_CODES = {
|
|
51
|
+
NOT_FOUND: "not_found",
|
|
52
|
+
STALE_REF: "stale_ref",
|
|
53
|
+
INVALID_SELECTOR: "invalid_selector",
|
|
54
|
+
SELECTOR_NOT_FOUND: "selector_not_found",
|
|
55
|
+
SELECTOR_AMBIGUOUS: "selector_ambiguous",
|
|
56
|
+
SELECTOR_NTH_OUT_OF_RANGE: "selector_nth_out_of_range",
|
|
57
|
+
OPTION_NOT_FOUND: "option_not_found",
|
|
58
|
+
NOT_A_SELECT: "not_a_select"
|
|
59
|
+
};
|
|
60
|
+
var IMPLICIT_ROLE = {
|
|
61
|
+
a: "link",
|
|
62
|
+
button: "button",
|
|
63
|
+
select: "combobox",
|
|
64
|
+
textarea: "textbox",
|
|
65
|
+
h1: "heading",
|
|
66
|
+
h2: "heading",
|
|
67
|
+
h3: "heading",
|
|
68
|
+
h4: "heading",
|
|
69
|
+
h5: "heading",
|
|
70
|
+
h6: "heading",
|
|
71
|
+
img: "img",
|
|
72
|
+
nav: "navigation",
|
|
73
|
+
ul: "list",
|
|
74
|
+
ol: "list",
|
|
75
|
+
li: "listitem",
|
|
76
|
+
main: "main",
|
|
77
|
+
article: "article",
|
|
78
|
+
aside: "complementary",
|
|
79
|
+
header: "banner",
|
|
80
|
+
footer: "contentinfo",
|
|
81
|
+
form: "form",
|
|
82
|
+
table: "table",
|
|
83
|
+
summary: "button",
|
|
84
|
+
option: "option",
|
|
85
|
+
output: "status"
|
|
86
|
+
};
|
|
87
|
+
function roleOf(el) {
|
|
88
|
+
const explicit = el.getAttribute("role");
|
|
89
|
+
if (explicit) return explicit.split(/\s+/)[0];
|
|
90
|
+
const tag = el.tagName.toLowerCase();
|
|
91
|
+
if (tag === "input") {
|
|
92
|
+
const type = el.type || "text";
|
|
93
|
+
if (type === "hidden") return null;
|
|
94
|
+
if (type === "checkbox") return "checkbox";
|
|
95
|
+
if (type === "radio") return "radio";
|
|
96
|
+
if (type === "button" || type === "submit" || type === "reset") return "button";
|
|
97
|
+
if (type === "range") return "slider";
|
|
98
|
+
return "textbox";
|
|
99
|
+
}
|
|
100
|
+
if (tag === "progress") return "progressbar";
|
|
101
|
+
if (tag === "meter") return "meter";
|
|
102
|
+
return IMPLICIT_ROLE[tag] ?? null;
|
|
103
|
+
}
|
|
104
|
+
function accessibleName(el) {
|
|
105
|
+
const aria = el.getAttribute("aria-label");
|
|
106
|
+
if (aria) return aria.trim();
|
|
107
|
+
const labelledby = el.getAttribute("aria-labelledby");
|
|
108
|
+
if (labelledby) {
|
|
109
|
+
const parts = labelledby.split(/\s+/).map((id2) => document.getElementById(id2)?.textContent?.trim() || "");
|
|
110
|
+
const joined = parts.filter(Boolean).join(" ");
|
|
111
|
+
if (joined) return joined.slice(0, 120);
|
|
112
|
+
}
|
|
113
|
+
const id = el.getAttribute("id");
|
|
114
|
+
if (id) {
|
|
115
|
+
const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);
|
|
116
|
+
if (label?.textContent?.trim()) return label.textContent.trim().slice(0, 120);
|
|
117
|
+
}
|
|
118
|
+
if (el.tagName.toLowerCase() === "input") {
|
|
119
|
+
const type = el.type;
|
|
120
|
+
if (type === "button" || type === "submit" || type === "reset") {
|
|
121
|
+
const v = el.value;
|
|
122
|
+
if (v) return v;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const alt = el.getAttribute("alt");
|
|
126
|
+
if (alt) return alt;
|
|
127
|
+
const title = el.getAttribute("title");
|
|
128
|
+
if (title) return title;
|
|
129
|
+
const placeholder = el.getAttribute("placeholder");
|
|
130
|
+
if (placeholder) return placeholder;
|
|
131
|
+
return (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
132
|
+
}
|
|
133
|
+
function isVisible(el) {
|
|
134
|
+
if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) return false;
|
|
135
|
+
const html = el;
|
|
136
|
+
if (html.hasAttribute("hidden")) return false;
|
|
137
|
+
if (html.closest("[hidden]")) return false;
|
|
138
|
+
const style = window.getComputedStyle(html);
|
|
139
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
140
|
+
if (el instanceof HTMLElement && el.offsetParent === null) {
|
|
141
|
+
const pos = style.position;
|
|
142
|
+
if (pos !== "fixed" && pos !== "sticky") return false;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
var INTERACTIVE_SELECTOR = "a[href], button, input, textarea, select, summary, [role], [contenteditable=''], [contenteditable='true'], [onclick], [tabindex]";
|
|
147
|
+
function isInteractive(el) {
|
|
148
|
+
return el.matches(INTERACTIVE_SELECTOR);
|
|
149
|
+
}
|
|
150
|
+
function testidSelector(testid) {
|
|
151
|
+
return `[data-testid="${CSS.escape(testid)}"], [data-test-id="${CSS.escape(testid)}"], [data-test="${CSS.escape(testid)}"], [data-cy="${CSS.escape(testid)}"]`;
|
|
152
|
+
}
|
|
153
|
+
function resolveTargetRich(sel) {
|
|
154
|
+
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() : "");
|
|
155
|
+
if (sel.ref != null) {
|
|
156
|
+
const store = getRefStore();
|
|
157
|
+
const el = store.refs.get(sel.ref);
|
|
158
|
+
if (el && document.contains(el) && isVisible(el)) {
|
|
159
|
+
return { el, matches_n: 1, match_level: "exact", target: targetDesc };
|
|
160
|
+
}
|
|
161
|
+
if (el && !document.contains(el)) {
|
|
162
|
+
const cssPath = store.cssPaths.get(sel.ref);
|
|
163
|
+
if (cssPath) {
|
|
164
|
+
const re = document.querySelector(cssPath);
|
|
165
|
+
if (re && isVisible(re)) {
|
|
166
|
+
store.refs.set(sel.ref, re);
|
|
167
|
+
return { el: re, matches_n: 1, match_level: "reidentified", target: targetDesc };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { errorEnvelope: err(ERROR_CODES.STALE_REF, "Numeric ref is no longer in the DOM. Re-run state.") };
|
|
171
|
+
}
|
|
172
|
+
return { errorEnvelope: err(ERROR_CODES.NOT_FOUND, "Numeric ref is no longer in the DOM. Re-run state.") };
|
|
173
|
+
}
|
|
174
|
+
let candidates = [];
|
|
175
|
+
if (sel.css) {
|
|
176
|
+
try {
|
|
177
|
+
candidates = Array.from(document.querySelectorAll(sel.css));
|
|
178
|
+
} catch {
|
|
179
|
+
return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: sel.css }) };
|
|
180
|
+
}
|
|
181
|
+
if (candidates.length === 0) {
|
|
182
|
+
return { errorEnvelope: err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: sel.css, candidates: suggestCandidates() }) };
|
|
183
|
+
}
|
|
184
|
+
} else if (sel.testid) {
|
|
185
|
+
try {
|
|
186
|
+
candidates = Array.from(document.querySelectorAll(testidSelector(sel.testid)));
|
|
187
|
+
} catch {
|
|
188
|
+
return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.", { testid: sel.testid }) };
|
|
189
|
+
}
|
|
190
|
+
} else if (sel.role || sel.name || sel.label || sel.text) {
|
|
191
|
+
const wantRole = sel.role ? sel.role.toLowerCase() : null;
|
|
192
|
+
const wantName = sel.name ? sel.name.toLowerCase() : null;
|
|
193
|
+
const wantText = sel.text ? sel.text.toLowerCase() : null;
|
|
194
|
+
let labelTargets = [];
|
|
195
|
+
if (sel.label) {
|
|
196
|
+
const lower = sel.label.toLowerCase();
|
|
197
|
+
labelTargets = Array.from(document.querySelectorAll("label")).filter((l) => (l.textContent || "").trim().toLowerCase().includes(lower)).map((l) => {
|
|
198
|
+
const forId = l.getAttribute("for");
|
|
199
|
+
if (forId) return document.getElementById(forId);
|
|
200
|
+
return l.querySelector("input, select, textarea");
|
|
201
|
+
}).filter((x) => !!x);
|
|
202
|
+
}
|
|
203
|
+
const all = Array.from(document.querySelectorAll("*"));
|
|
204
|
+
candidates = all.filter((el) => {
|
|
205
|
+
if (!isVisible(el)) return false;
|
|
206
|
+
const role = roleOf(el);
|
|
207
|
+
if (wantRole && (!role || role.toLowerCase() !== wantRole)) return false;
|
|
208
|
+
if (wantName) {
|
|
209
|
+
const name = accessibleName(el).toLowerCase();
|
|
210
|
+
if (!name.includes(wantName)) return false;
|
|
211
|
+
}
|
|
212
|
+
if (wantText) {
|
|
213
|
+
const t = (el.textContent || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
214
|
+
if (!t.includes(wantText)) return false;
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
if (labelTargets.length) {
|
|
219
|
+
const set = new Set(labelTargets);
|
|
220
|
+
candidates = labelTargets.concat(candidates.filter((c) => !set.has(c)));
|
|
221
|
+
}
|
|
222
|
+
if (candidates.length === 0) {
|
|
223
|
+
return {
|
|
224
|
+
errorEnvelope: err(
|
|
225
|
+
ERROR_CODES.SELECTOR_NOT_FOUND,
|
|
226
|
+
"Semantic locator matches 0 elements.",
|
|
227
|
+
{ role: sel.role, name: sel.name, label: sel.label, text: sel.text, candidates: suggestCandidates() }
|
|
228
|
+
)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
return { errorEnvelope: err("no_target", "Provide a numeric ref, CSS selector, or semantic locator (--role/--name/--label/--text/--testid).") };
|
|
233
|
+
}
|
|
234
|
+
const visible = candidates.filter(isVisible);
|
|
235
|
+
const pool = visible.length ? visible : candidates;
|
|
236
|
+
if (sel.nth != null) {
|
|
237
|
+
if (sel.nth < 0 || sel.nth >= pool.length) {
|
|
238
|
+
return { errorEnvelope: err(ERROR_CODES.SELECTOR_NTH_OUT_OF_RANGE, `--nth ${sel.nth} beyond match count ${pool.length}.`) };
|
|
239
|
+
}
|
|
240
|
+
const el = pool[sel.nth];
|
|
241
|
+
return { el, matches_n: pool.length, match_level: "reidentified", target: targetDesc };
|
|
242
|
+
}
|
|
243
|
+
if (pool.length > 1) {
|
|
244
|
+
return {
|
|
245
|
+
errorEnvelope: err(ERROR_CODES.SELECTOR_AMBIGUOUS, `CSS matches ${pool.length} elements and no --nth given.`, {
|
|
246
|
+
matches_n: pool.length,
|
|
247
|
+
candidates: pool.slice(0, 5).map((el) => uniqueCssPath(el))
|
|
248
|
+
})
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return { el: pool[0], matches_n: 1, match_level: "reidentified", target: targetDesc };
|
|
252
|
+
}
|
|
253
|
+
function suggestCandidates() {
|
|
254
|
+
const store = getRefStore();
|
|
255
|
+
const recent = Array.from(store.refs.entries()).slice(-5).map(([, el]) => uniqueCssPath(el));
|
|
256
|
+
return recent;
|
|
257
|
+
}
|
|
258
|
+
function compoundOf(el) {
|
|
259
|
+
const tag = el.tagName.toLowerCase();
|
|
260
|
+
if (tag === "input") {
|
|
261
|
+
const input = el;
|
|
262
|
+
const type = input.type;
|
|
263
|
+
if (["date", "time", "datetime-local", "month", "week"].includes(type)) {
|
|
264
|
+
const formatMap = {
|
|
265
|
+
date: "YYYY-MM-DD",
|
|
266
|
+
time: "HH:mm",
|
|
267
|
+
"datetime-local": "YYYY-MM-DDTHH:mm",
|
|
268
|
+
month: "YYYY-MM",
|
|
269
|
+
week: "YYYY-Www"
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
control: type,
|
|
273
|
+
format: formatMap[type] ?? type,
|
|
274
|
+
current: input.value || "",
|
|
275
|
+
min: input.min || null,
|
|
276
|
+
max: input.max || null
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (type === "file") {
|
|
280
|
+
const current = Array.from(input.files ?? []).map((f) => f.name);
|
|
281
|
+
return { control: "file", multiple: input.multiple, current, accept: input.accept || null };
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (tag === "select") {
|
|
286
|
+
const select = el;
|
|
287
|
+
const options = Array.from(select.options).slice(0, 50).map((o) => ({
|
|
288
|
+
label: (o.textContent || "").trim(),
|
|
289
|
+
value: o.value,
|
|
290
|
+
selected: o.selected
|
|
291
|
+
}));
|
|
292
|
+
return {
|
|
293
|
+
control: "select",
|
|
294
|
+
multiple: select.multiple,
|
|
295
|
+
current: select.selectedOptions[0] ? (select.selectedOptions[0].textContent || "").trim() : "",
|
|
296
|
+
options,
|
|
297
|
+
options_total: select.options.length
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "meta", "link", "head", "base", "template", "path", "circle", "rect", "g", "defs", "polygon", "line"]);
|
|
303
|
+
function describeElement(el, ref) {
|
|
304
|
+
const tag = el.tagName.toLowerCase();
|
|
305
|
+
const role = el.getAttribute("role") ?? null;
|
|
306
|
+
const name = accessibleName(el);
|
|
307
|
+
const parts = [];
|
|
308
|
+
if (ref != null) parts.push(`[${ref}]`);
|
|
309
|
+
parts.push(tag);
|
|
310
|
+
if (role && role !== IMPLICIT_ROLE[tag]) parts.push(`role=${role}`);
|
|
311
|
+
if (tag === "input") {
|
|
312
|
+
const input = el;
|
|
313
|
+
if (input.type && input.type !== "text") parts.push(`[${input.type}]`);
|
|
314
|
+
if (input.disabled) parts.push("(disabled)");
|
|
315
|
+
}
|
|
316
|
+
if (name) parts.push(JSON.stringify(name.length > 60 ? name.slice(0, 57) + "\u2026" : name));
|
|
317
|
+
if (tag === "a") {
|
|
318
|
+
const href = el.getAttribute("href");
|
|
319
|
+
if (href) parts.push(`\u2192 ${href.slice(0, 50)}`);
|
|
320
|
+
}
|
|
321
|
+
if (el.disabled) parts.push("(disabled)");
|
|
322
|
+
return parts.join(" ");
|
|
323
|
+
}
|
|
324
|
+
function buildStateTree() {
|
|
325
|
+
const lines = [];
|
|
326
|
+
const compounds = {};
|
|
327
|
+
const budget = { nodes: 0, chars: 0, truncated: false };
|
|
328
|
+
const MAX_NODES = 400;
|
|
329
|
+
const MAX_CHARS = 9e3;
|
|
330
|
+
let hiddenInteractive = 0;
|
|
331
|
+
function emit(depth, text) {
|
|
332
|
+
const line = " ".repeat(Math.min(depth, 12)) + text;
|
|
333
|
+
lines.push(line);
|
|
334
|
+
budget.chars += line.length;
|
|
335
|
+
}
|
|
336
|
+
function walk(el, depth) {
|
|
337
|
+
if (budget.nodes >= MAX_NODES || budget.chars >= MAX_CHARS) {
|
|
338
|
+
budget.truncated = true;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const tag = el.tagName.toLowerCase();
|
|
342
|
+
if (SKIP_TAGS.has(tag)) return;
|
|
343
|
+
budget.nodes++;
|
|
344
|
+
const visible = isVisible(el);
|
|
345
|
+
const interactive = isInteractive(el);
|
|
346
|
+
if (interactive) {
|
|
347
|
+
if (visible) {
|
|
348
|
+
const ref = allocRef(el);
|
|
349
|
+
emit(depth, describeElement(el, ref));
|
|
350
|
+
const compound = compoundOf(el);
|
|
351
|
+
if (compound) compounds[String(ref)] = compound;
|
|
352
|
+
} else {
|
|
353
|
+
hiddenInteractive++;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
} else if (visible) {
|
|
357
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
358
|
+
const t = accessibleName(el);
|
|
359
|
+
if (t) emit(depth, `${"#".repeat(Number(tag[1]))} ${t}`);
|
|
360
|
+
} else if (tag === "p" || tag === "li" || tag === "td" || tag === "th" || tag === "figcaption" || tag === "blockquote") {
|
|
361
|
+
const ownText = ownTextOf(el);
|
|
362
|
+
if (ownText) emit(depth, ownText.slice(0, 100));
|
|
363
|
+
} else if (tag === "img") {
|
|
364
|
+
const alt = el.getAttribute("alt") || "";
|
|
365
|
+
if (alt) emit(depth, `img ${JSON.stringify(alt.slice(0, 60))}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (depth < 14) {
|
|
369
|
+
for (const child of Array.from(el.children)) {
|
|
370
|
+
walk(child, depth + 1);
|
|
371
|
+
if (budget.truncated) return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function ownTextOf(el) {
|
|
376
|
+
let text = "";
|
|
377
|
+
for (const node of Array.from(el.childNodes)) {
|
|
378
|
+
if (node.nodeType === Node.TEXT_NODE) text += node.textContent || "";
|
|
379
|
+
}
|
|
380
|
+
return text.replace(/\s+/g, " ").trim();
|
|
381
|
+
}
|
|
382
|
+
const root = document.querySelector("main") || document.querySelector("#root") || document.body;
|
|
383
|
+
emit(0, `# ${document.title}`);
|
|
384
|
+
walk(root, 0);
|
|
385
|
+
const scrollable = document.documentElement.scrollHeight > window.innerHeight + 200;
|
|
386
|
+
if (scrollable) {
|
|
387
|
+
emit(0, "\u2195 page is scrollable \u2014 run cp.scroll down to reveal more");
|
|
388
|
+
}
|
|
389
|
+
if (hiddenInteractive > 0) {
|
|
390
|
+
emit(0, `\u2139 ${hiddenInteractive} hidden interactive element(s) not tagged`);
|
|
391
|
+
}
|
|
392
|
+
if (budget.truncated) {
|
|
393
|
+
emit(0, "\u2026 (snapshot truncated by budget \u2014 narrow with a selector or scroll)");
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
url: location.href,
|
|
397
|
+
title: document.title,
|
|
398
|
+
text: lines.join("\n"),
|
|
399
|
+
compounds,
|
|
400
|
+
hints: { scroll: scrollable, hidden_interactive: hiddenInteractive },
|
|
401
|
+
truncated: budget.truncated
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function runControl(command, args) {
|
|
405
|
+
const a = args || {};
|
|
406
|
+
function getVisibleText(el) {
|
|
407
|
+
return (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 120);
|
|
408
|
+
}
|
|
409
|
+
function getAttrs(el) {
|
|
410
|
+
const attrs = {};
|
|
411
|
+
for (const name of ["id", "class", "name", "type", "href", "src", "placeholder", "value", "role", "aria-label", "data-testid"]) {
|
|
412
|
+
attrs[name] = el.getAttribute(name);
|
|
413
|
+
}
|
|
414
|
+
return attrs;
|
|
415
|
+
}
|
|
416
|
+
function sel() {
|
|
417
|
+
return {
|
|
418
|
+
ref: a.ref,
|
|
419
|
+
css: a.css,
|
|
420
|
+
nth: a.nth,
|
|
421
|
+
role: a.role,
|
|
422
|
+
name: a.name,
|
|
423
|
+
label: a.label,
|
|
424
|
+
text: a.text,
|
|
425
|
+
testid: a.testid
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function findElements(findArgs) {
|
|
429
|
+
const limit = findArgs.limit ?? 20;
|
|
430
|
+
const textMax = findArgs["text-max"] ?? 120;
|
|
431
|
+
let elements = [];
|
|
432
|
+
if (findArgs.css) {
|
|
433
|
+
try {
|
|
434
|
+
elements = Array.from(document.querySelectorAll(findArgs.css));
|
|
435
|
+
} catch {
|
|
436
|
+
return err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: findArgs.css });
|
|
437
|
+
}
|
|
438
|
+
} else if (findArgs.testid) {
|
|
439
|
+
try {
|
|
440
|
+
elements = Array.from(document.querySelectorAll(testidSelector(findArgs.testid)));
|
|
441
|
+
} catch {
|
|
442
|
+
return err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.");
|
|
443
|
+
}
|
|
444
|
+
} else if (findArgs.role || findArgs.name || findArgs.label) {
|
|
445
|
+
const r = resolveTargetRich({ role: findArgs.role, name: findArgs.name, label: findArgs.label, text: findArgs.text });
|
|
446
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
447
|
+
elements = document.querySelectorAll("*").length ? semanticCandidates(findArgs) : [];
|
|
448
|
+
} else if (findArgs.text) {
|
|
449
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
450
|
+
const lower = findArgs.text.toLowerCase();
|
|
451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
452
|
+
let node;
|
|
453
|
+
while (node = walker.nextNode()) {
|
|
454
|
+
if (node.textContent?.toLowerCase().includes(lower)) {
|
|
455
|
+
const el = node.parentElement;
|
|
456
|
+
if (el && !seen.has(el)) {
|
|
457
|
+
seen.add(el);
|
|
458
|
+
elements.push(el);
|
|
459
|
+
if (elements.length >= limit) break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
elements = Array.from(document.querySelectorAll(INTERACTIVE_SELECTOR));
|
|
465
|
+
}
|
|
466
|
+
const out = elements.slice(0, limit).map((el, i) => {
|
|
467
|
+
const ref = allocRef(el);
|
|
468
|
+
return {
|
|
469
|
+
nth: i,
|
|
470
|
+
ref,
|
|
471
|
+
tag: el.tagName.toLowerCase(),
|
|
472
|
+
role: roleOf(el),
|
|
473
|
+
text: getVisibleText(el).slice(0, textMax),
|
|
474
|
+
attrs: getAttrs(el),
|
|
475
|
+
visible: isVisible(el),
|
|
476
|
+
compound: compoundOf(el) ?? void 0
|
|
477
|
+
};
|
|
478
|
+
});
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
function semanticCandidates(findArgs) {
|
|
482
|
+
const wantRole = findArgs.role ? findArgs.role.toLowerCase() : null;
|
|
483
|
+
const wantName = findArgs.name ? findArgs.name.toLowerCase() : null;
|
|
484
|
+
const wantText = findArgs.text ? findArgs.text.toLowerCase() : null;
|
|
485
|
+
const all = Array.from(document.querySelectorAll("*"));
|
|
486
|
+
return all.filter((el) => {
|
|
487
|
+
if (!isVisible(el)) return false;
|
|
488
|
+
if (wantRole) {
|
|
489
|
+
const role = roleOf(el);
|
|
490
|
+
if (!role || role.toLowerCase() !== wantRole) return false;
|
|
491
|
+
}
|
|
492
|
+
if (wantName && !accessibleName(el).toLowerCase().includes(wantName)) return false;
|
|
493
|
+
if (wantText && !(el.textContent || "").toLowerCase().includes(wantText)) return false;
|
|
494
|
+
return true;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function pageState() {
|
|
498
|
+
return {
|
|
499
|
+
url: location.href,
|
|
500
|
+
title: document.title,
|
|
501
|
+
width: window.innerWidth,
|
|
502
|
+
height: window.innerHeight,
|
|
503
|
+
devicePixelRatio: window.devicePixelRatio
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function goto(gotoArgs) {
|
|
507
|
+
if (!gotoArgs.url) return { error: "url_required" };
|
|
508
|
+
window.location.href = gotoArgs.url;
|
|
509
|
+
return { navigated: true, url: gotoArgs.url };
|
|
510
|
+
}
|
|
511
|
+
function evalScript(evalArgs) {
|
|
512
|
+
if (!evalArgs.script) return { error: "script_required" };
|
|
513
|
+
try {
|
|
514
|
+
const fn = new Function(evalArgs.script);
|
|
515
|
+
const result = fn();
|
|
516
|
+
return { result };
|
|
517
|
+
} catch (e) {
|
|
518
|
+
return { error: String(e) };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function fillElement(fArgs) {
|
|
522
|
+
const r = resolveTargetRich(sel());
|
|
523
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
524
|
+
const { el, matches_n, match_level, target } = r;
|
|
525
|
+
const input = el;
|
|
526
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable) {
|
|
527
|
+
const isContentEditable = el.isContentEditable;
|
|
528
|
+
if (isContentEditable) {
|
|
529
|
+
el.innerText = fArgs.text ?? "";
|
|
530
|
+
} else {
|
|
531
|
+
input.value = fArgs.text ?? "";
|
|
532
|
+
}
|
|
533
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
534
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
535
|
+
const actual = isContentEditable ? el.innerText : input.value;
|
|
536
|
+
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 });
|
|
537
|
+
}
|
|
538
|
+
return err("invalid_type", "fill only supports input, textarea, and contenteditable.");
|
|
539
|
+
}
|
|
540
|
+
function typeElement(tArgs) {
|
|
541
|
+
const r = resolveTargetRich(sel());
|
|
542
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
543
|
+
const { el, matches_n, match_level, target } = r;
|
|
544
|
+
const input = el;
|
|
545
|
+
el.focus({ preventScroll: true });
|
|
546
|
+
input.value = tArgs.text ?? "";
|
|
547
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
548
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
549
|
+
let autocomplete = false;
|
|
550
|
+
try {
|
|
551
|
+
autocomplete = !!document.querySelector("[role='listbox'], [role='combobox'] [role='listbox'], datalist, .ant-select-dropdown:not([style*='hidden']), [id^='listbox']");
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
return { typed: true, text: tArgs.text, target, matches_n, match_level, autocomplete };
|
|
555
|
+
}
|
|
556
|
+
function clickElement() {
|
|
557
|
+
const r = resolveTargetRich(sel());
|
|
558
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
559
|
+
const { el, matches_n, match_level, target } = r;
|
|
560
|
+
el.scrollIntoView({ block: "center" });
|
|
561
|
+
el.click();
|
|
562
|
+
return { clicked: true, target, matches_n, match_level, tag: el.tagName.toLowerCase(), text: getVisibleText(el) };
|
|
563
|
+
}
|
|
564
|
+
function selectElement(sArgs) {
|
|
565
|
+
const r = resolveTargetRich(sel());
|
|
566
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
567
|
+
const { el, matches_n, match_level, target } = r;
|
|
568
|
+
if (el.tagName !== "SELECT") {
|
|
569
|
+
return err(ERROR_CODES.NOT_A_SELECT, "select was called on a non-<select> element.", { tag: el.tagName.toLowerCase() });
|
|
570
|
+
}
|
|
571
|
+
const select = el;
|
|
572
|
+
const want = sArgs.label ?? sArgs.value ?? sArgs.option;
|
|
573
|
+
if (!want) return err("option_required", "Provide label or value.");
|
|
574
|
+
const byLabel = Array.from(select.options).find((o) => (o.textContent || "").trim() === want);
|
|
575
|
+
const byValue = Array.from(select.options).find((o) => o.value === want);
|
|
576
|
+
const opt = byLabel ?? byValue;
|
|
577
|
+
if (!opt) {
|
|
578
|
+
const available = Array.from(select.options).slice(0, 50).map((o) => (o.textContent || "").trim());
|
|
579
|
+
return err(ERROR_CODES.OPTION_NOT_FOUND, "Option not found.", { available });
|
|
580
|
+
}
|
|
581
|
+
select.value = opt.value;
|
|
582
|
+
select.dispatchEvent(new Event("input", { bubbles: true }));
|
|
583
|
+
select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
584
|
+
return { selected: true, value: select.value, target, matches_n, match_level };
|
|
585
|
+
}
|
|
586
|
+
function pressKeys(kArgs) {
|
|
587
|
+
const keyCombo = kArgs.keys ?? kArgs.key ?? "";
|
|
588
|
+
if (!keyCombo) return { error: "keys_required" };
|
|
589
|
+
const comboParts = keyCombo.split("+").map((s) => s.trim());
|
|
590
|
+
const main = comboParts[comboParts.length - 1];
|
|
591
|
+
const ctrl = comboParts.includes("Control") || comboParts.includes("Ctrl") || comboParts.includes("Meta") || comboParts.includes("Cmd");
|
|
592
|
+
const alt = comboParts.includes("Alt");
|
|
593
|
+
const shift = comboParts.includes("Shift");
|
|
594
|
+
const target = document.activeElement ?? document.body;
|
|
595
|
+
const opts = { key: main, bubbles: true, cancelable: true, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: comboParts.includes("Meta") || comboParts.includes("Cmd") };
|
|
596
|
+
target.dispatchEvent(new KeyboardEvent("keydown", opts));
|
|
597
|
+
target.dispatchEvent(new KeyboardEvent("keypress", opts));
|
|
598
|
+
target.dispatchEvent(new KeyboardEvent("keyup", opts));
|
|
599
|
+
return { pressed: true, key: keyCombo };
|
|
600
|
+
}
|
|
601
|
+
function hoverElement() {
|
|
602
|
+
const r = resolveTargetRich(sel());
|
|
603
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
604
|
+
const { el, matches_n, match_level, target } = r;
|
|
605
|
+
const rect = el.getBoundingClientRect();
|
|
606
|
+
const opts = { bubbles: true, cancelable: true, clientX: rect.x + rect.width / 2, clientY: rect.y + rect.height / 2 };
|
|
607
|
+
el.dispatchEvent(new MouseEvent("mouseover", opts));
|
|
608
|
+
el.dispatchEvent(new MouseEvent("mousemove", opts));
|
|
609
|
+
return { hovered: true, target, matches_n, match_level };
|
|
610
|
+
}
|
|
611
|
+
function focusElement() {
|
|
612
|
+
const r = resolveTargetRich(sel());
|
|
613
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
614
|
+
const { el, matches_n, match_level, target } = r;
|
|
615
|
+
el.focus({ preventScroll: true });
|
|
616
|
+
return { focused: true, target, matches_n, match_level };
|
|
617
|
+
}
|
|
618
|
+
function dblclickElement() {
|
|
619
|
+
const r = resolveTargetRich(sel());
|
|
620
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
621
|
+
const { el, matches_n, match_level, target } = r;
|
|
622
|
+
el.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));
|
|
623
|
+
return { dblclicked: true, target, matches_n, match_level };
|
|
624
|
+
}
|
|
625
|
+
function setChecked(want) {
|
|
626
|
+
const r = resolveTargetRich(sel());
|
|
627
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
628
|
+
const { el, matches_n, match_level, target } = r;
|
|
629
|
+
const input = el;
|
|
630
|
+
const isAria = input.type !== "checkbox" && input.type !== "radio" && el.getAttribute("aria-checked") != null;
|
|
631
|
+
if (!isAria && input.type !== "checkbox" && input.type !== "radio") {
|
|
632
|
+
return err("invalid_type", "Target is not a checkbox/radio/aria-checked control.", { tag: el.tagName.toLowerCase() });
|
|
633
|
+
}
|
|
634
|
+
if (isAria) {
|
|
635
|
+
el.setAttribute("aria-checked", String(want));
|
|
636
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
637
|
+
return { [want ? "checked" : "unchecked"]: true, changed: true, target, matches_n, match_level, kind: "aria" };
|
|
638
|
+
}
|
|
639
|
+
if (input.type === "radio" && !want) {
|
|
640
|
+
return err("invalid_type", "Radio buttons cannot be unchecked directly; select another radio in the group.");
|
|
641
|
+
}
|
|
642
|
+
const changed = input.checked !== want;
|
|
643
|
+
input.checked = want;
|
|
644
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
645
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
646
|
+
return { [want ? "checked" : "unchecked"]: true, changed, target, matches_n, match_level, kind: input.type };
|
|
647
|
+
}
|
|
648
|
+
function scrollElement(sArgs) {
|
|
649
|
+
const dir = sArgs.direction ?? "down";
|
|
650
|
+
const amount = sArgs.amount ?? 500;
|
|
651
|
+
if (dir !== "up" && dir !== "down") return { error: "invalid_direction" };
|
|
652
|
+
const dy = dir === "down" ? amount : -amount;
|
|
653
|
+
window.scrollBy({ top: dy, behavior: "instant" });
|
|
654
|
+
return {
|
|
655
|
+
scrolled: true,
|
|
656
|
+
direction: dir,
|
|
657
|
+
amount,
|
|
658
|
+
scrollY: Math.round(window.scrollY),
|
|
659
|
+
scrollHeight: document.documentElement.scrollHeight
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function backNav() {
|
|
663
|
+
history.back();
|
|
664
|
+
return { back: true };
|
|
665
|
+
}
|
|
666
|
+
function htmlJsonTree(el, depth, maxDepth, childrenMax, textMax, truncated) {
|
|
667
|
+
const children = Array.from(el.children);
|
|
668
|
+
const ownText = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
669
|
+
const node = {
|
|
670
|
+
tag: el.tagName.toLowerCase(),
|
|
671
|
+
attrs: getAttrs(el),
|
|
672
|
+
text: ownText.length > textMax ? (ownText.slice(0, textMax), truncated.text_truncated = (truncated.text_truncated || 0) + 1, ownText.slice(0, textMax)) : ownText,
|
|
673
|
+
children: [],
|
|
674
|
+
compound: compoundOf(el) ?? void 0
|
|
675
|
+
};
|
|
676
|
+
if (depth < maxDepth) {
|
|
677
|
+
let kids = children;
|
|
678
|
+
if (children.length > childrenMax) {
|
|
679
|
+
truncated.children_dropped = (truncated.children_dropped || 0) + (children.length - childrenMax);
|
|
680
|
+
kids = children.slice(0, childrenMax);
|
|
681
|
+
}
|
|
682
|
+
node.children = kids.map((c) => htmlJsonTree(c, depth + 1, maxDepth, childrenMax, textMax, truncated));
|
|
683
|
+
} else if (children.length) {
|
|
684
|
+
truncated.depth = (truncated.depth || 0) + 1;
|
|
685
|
+
}
|
|
686
|
+
return node;
|
|
687
|
+
}
|
|
688
|
+
function getElement(gArgs) {
|
|
689
|
+
const what = gArgs.what ?? "text";
|
|
690
|
+
if (what === "title") return { value: document.title, matches_n: 1, match_level: "exact" };
|
|
691
|
+
if (what === "url") return { value: location.href, matches_n: 1, match_level: "exact" };
|
|
692
|
+
if (what === "html") {
|
|
693
|
+
if (gArgs["as"] === "json") {
|
|
694
|
+
const root2 = gArgs.css ? document.querySelector(gArgs.css) : document.body;
|
|
695
|
+
if (!root2) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });
|
|
696
|
+
const truncated = {};
|
|
697
|
+
const tree = htmlJsonTree(root2, 0, gArgs.depth ?? 6, gArgs["children-max"] ?? 20, gArgs["text-max"] ?? 200, truncated);
|
|
698
|
+
return {
|
|
699
|
+
value: tree,
|
|
700
|
+
truncated: Object.keys(truncated).length ? truncated : void 0
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const root = gArgs.css ? document.querySelector(gArgs.css) : document.body;
|
|
704
|
+
if (!root) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });
|
|
705
|
+
let html = root.outerHTML;
|
|
706
|
+
if (gArgs.max && html.length > gArgs.max) html = html.slice(0, gArgs.max) + "\u2026";
|
|
707
|
+
return { value: html };
|
|
708
|
+
}
|
|
709
|
+
const r = resolveTargetRich(gArgs);
|
|
710
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
711
|
+
const { el, matches_n, match_level } = r;
|
|
712
|
+
switch (what) {
|
|
713
|
+
case "text": {
|
|
714
|
+
const t = getVisibleText(el);
|
|
715
|
+
return { value: t.length > 2e3 ? t.slice(0, 2e3) : t, matches_n, match_level };
|
|
716
|
+
}
|
|
717
|
+
case "value":
|
|
718
|
+
return { value: el.value ?? el.innerText ?? null, matches_n, match_level };
|
|
719
|
+
case "attributes":
|
|
720
|
+
return { value: getAttrs(el), matches_n, match_level };
|
|
721
|
+
default:
|
|
722
|
+
return { error: `unknown_what: ${what}` };
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function extractMarkdown(eArgs) {
|
|
726
|
+
const chunkSize = eArgs["chunk-size"] ?? 0;
|
|
727
|
+
const start = eArgs.start ?? 0;
|
|
728
|
+
const scope = eArgs.selector ? document.querySelector(eArgs.selector) : document.querySelector("main") || document.querySelector("article") || document.body;
|
|
729
|
+
if (!scope) return { error: "scope_not_found" };
|
|
730
|
+
const parts = [];
|
|
731
|
+
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);
|
|
732
|
+
let node;
|
|
733
|
+
while (node = walker.nextNode()) {
|
|
734
|
+
const el = node;
|
|
735
|
+
const tag = el.tagName.toLowerCase();
|
|
736
|
+
const text = getVisibleText(el);
|
|
737
|
+
if (!text) continue;
|
|
738
|
+
if (tag === "h1") parts.push(`# ${text}`);
|
|
739
|
+
else if (tag === "h2") parts.push(`## ${text}`);
|
|
740
|
+
else if (tag === "h3") parts.push(`### ${text}`);
|
|
741
|
+
else if (tag === "p" || tag === "li") parts.push(text);
|
|
742
|
+
else if (tag === "a") parts.push(`[${text}](${el.getAttribute("href") ?? ""})`);
|
|
743
|
+
}
|
|
744
|
+
const full = parts.join("\n\n");
|
|
745
|
+
const sliced = chunkSize > 0 ? full.slice(start, start + chunkSize) : full.slice(start);
|
|
746
|
+
return {
|
|
747
|
+
url: location.href,
|
|
748
|
+
title: document.title,
|
|
749
|
+
selector: eArgs.selector ?? "auto",
|
|
750
|
+
total_chars: full.length,
|
|
751
|
+
chunk_size: chunkSize,
|
|
752
|
+
start,
|
|
753
|
+
end: start + sliced.length,
|
|
754
|
+
next_start_char: start + sliced.length < full.length ? start + sliced.length : null,
|
|
755
|
+
content: sliced
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function listFrames() {
|
|
759
|
+
const frames = Array.from(document.querySelectorAll("iframe")).map((f, index) => {
|
|
760
|
+
const src = f.getAttribute("src") ?? "";
|
|
761
|
+
let crossOrigin = false;
|
|
762
|
+
try {
|
|
763
|
+
if (src && /^https?:/.test(src)) crossOrigin = new URL(src, location.href).origin !== location.origin;
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
766
|
+
return { index, url: src, name: f.getAttribute("name") ?? "", cross_origin: crossOrigin };
|
|
767
|
+
});
|
|
768
|
+
return { frames };
|
|
769
|
+
}
|
|
770
|
+
function screenshotMeta() {
|
|
771
|
+
return {
|
|
772
|
+
width: window.innerWidth,
|
|
773
|
+
height: window.innerHeight,
|
|
774
|
+
url: location.href,
|
|
775
|
+
title: document.title
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function startTitleMarquee2(tArgs) {
|
|
779
|
+
const g = globalThis;
|
|
780
|
+
if (g.__cpMarquee) {
|
|
781
|
+
clearInterval(g.__cpMarquee.interval);
|
|
782
|
+
} else {
|
|
783
|
+
g.__cpMarquee = { original: document.title, interval: 0 };
|
|
784
|
+
}
|
|
785
|
+
const prefix = tArgs.text ?? "\u{1F916}";
|
|
786
|
+
const base = g.__cpMarquee.original;
|
|
787
|
+
const full = `${prefix} ${base}`;
|
|
788
|
+
const speed = Math.max(100, Math.min(2e3, tArgs.speed ?? 300));
|
|
789
|
+
let pos = 0;
|
|
790
|
+
const tick = () => {
|
|
791
|
+
const display = full.slice(pos) + " " + full.slice(0, pos);
|
|
792
|
+
document.title = display.slice(0, 60);
|
|
793
|
+
pos = (pos + 1) % (full.length + 1 || 1);
|
|
794
|
+
};
|
|
795
|
+
tick();
|
|
796
|
+
g.__cpMarquee.interval = window.setInterval(tick, speed);
|
|
797
|
+
return { started: true, original: base, prefix, speed };
|
|
798
|
+
}
|
|
799
|
+
function stopTitleMarquee2() {
|
|
800
|
+
const g = globalThis;
|
|
801
|
+
const state2 = g.__cpMarquee;
|
|
802
|
+
if (!state2) return { restored: false, reason: "no_active_marquee" };
|
|
803
|
+
clearInterval(state2.interval);
|
|
804
|
+
document.title = state2.original;
|
|
805
|
+
delete g.__cpMarquee;
|
|
806
|
+
return { restored: true, original: state2.original };
|
|
807
|
+
}
|
|
808
|
+
function networkEntriesLegacy(nArgs) {
|
|
809
|
+
const all = performance.getEntriesByType("resource").map((e) => ({
|
|
810
|
+
url: e.name,
|
|
811
|
+
type: e.initiatorType ?? "unknown",
|
|
812
|
+
duration: Math.round(e.duration)
|
|
813
|
+
}));
|
|
814
|
+
let entries = all;
|
|
815
|
+
if (nArgs.filter) {
|
|
816
|
+
const keywords = nArgs.filter.split(",").map((s) => s.trim().toLowerCase());
|
|
817
|
+
entries = all.filter((e) => keywords.some((kw) => e.url.toLowerCase().includes(kw)));
|
|
818
|
+
}
|
|
819
|
+
return { entries, count: entries.length, source: "performance-api" };
|
|
820
|
+
}
|
|
821
|
+
async function waitForCondition(wArgs) {
|
|
822
|
+
const timeout = wArgs.timeout ?? 1e4;
|
|
823
|
+
if (wArgs.time != null) {
|
|
824
|
+
await new Promise((res) => setTimeout(res, Math.min(wArgs.time * 1e3, 3e4)));
|
|
825
|
+
return { waited: true, kind: "time", seconds: wArgs.time };
|
|
826
|
+
}
|
|
827
|
+
const deadline = Date.now() + timeout;
|
|
828
|
+
const pollInterval = 200;
|
|
829
|
+
if (wArgs.selector) {
|
|
830
|
+
while (Date.now() < deadline) {
|
|
831
|
+
if (document.querySelector(wArgs.selector)) return { waited: true, kind: "selector", selector: wArgs.selector, elapsedMs: timeout - (deadline - Date.now()) };
|
|
832
|
+
await new Promise((res) => setTimeout(res, pollInterval));
|
|
833
|
+
}
|
|
834
|
+
return err("wait_timeout", `Selector not found within ${timeout}ms.`, { selector: wArgs.selector });
|
|
835
|
+
}
|
|
836
|
+
if (wArgs.text) {
|
|
837
|
+
const lower = wArgs.text.toLowerCase();
|
|
838
|
+
while (Date.now() < deadline) {
|
|
839
|
+
if ((document.body.textContent || "").toLowerCase().includes(lower)) {
|
|
840
|
+
return { waited: true, kind: "text", text: wArgs.text, elapsedMs: timeout - (deadline - Date.now()) };
|
|
841
|
+
}
|
|
842
|
+
await new Promise((res) => setTimeout(res, pollInterval));
|
|
843
|
+
}
|
|
844
|
+
return err("wait_timeout", `Text not found within ${timeout}ms.`, { text: wArgs.text });
|
|
845
|
+
}
|
|
846
|
+
return { error: "no_condition" };
|
|
847
|
+
}
|
|
848
|
+
function uploadFiles(uArgs) {
|
|
849
|
+
const r = resolveTargetRich(sel());
|
|
850
|
+
if ("errorEnvelope" in r) return r.errorEnvelope;
|
|
851
|
+
const { el, matches_n, match_level, target } = r;
|
|
852
|
+
const input = el;
|
|
853
|
+
if (input.type !== "file") return err("invalid_type", "Target is not an input[type=file].", { tag: el.tagName.toLowerCase() });
|
|
854
|
+
try {
|
|
855
|
+
const dt = new DataTransfer();
|
|
856
|
+
for (const name of uArgs.files ?? []) {
|
|
857
|
+
dt.items.add(new File([""], name.split("/").pop() || name));
|
|
858
|
+
}
|
|
859
|
+
input.files = dt.files;
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
863
|
+
return {
|
|
864
|
+
uploaded: true,
|
|
865
|
+
files: uArgs.files ?? [],
|
|
866
|
+
file_names: (uArgs.files ?? []).map((f) => f.split("/").pop()),
|
|
867
|
+
target,
|
|
868
|
+
matches_n,
|
|
869
|
+
match_level,
|
|
870
|
+
multiple: input.multiple,
|
|
871
|
+
accept: input.accept || null
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function dragElements(dArgs) {
|
|
875
|
+
const r1 = resolveTargetRich(sel());
|
|
876
|
+
if ("errorEnvelope" in r1) return r1.errorEnvelope;
|
|
877
|
+
const r2 = resolveTargetRich({ ref: dArgs.ref2, css: dArgs.css2 });
|
|
878
|
+
if ("errorEnvelope" in r2) return r2.errorEnvelope;
|
|
879
|
+
const src = r1.el;
|
|
880
|
+
const tgt = r2.el;
|
|
881
|
+
const make = (type) => {
|
|
882
|
+
const r1rect = src.getBoundingClientRect();
|
|
883
|
+
return new DragEvent(type, { bubbles: true, cancelable: true, clientX: r1rect.x + r1rect.width / 2, clientY: r1rect.y + r1rect.height / 2 });
|
|
884
|
+
};
|
|
885
|
+
src.dispatchEvent(make("dragstart"));
|
|
886
|
+
tgt.dispatchEvent(new DragEvent("dragenter", { bubbles: true }));
|
|
887
|
+
tgt.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true }));
|
|
888
|
+
tgt.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true }));
|
|
889
|
+
src.dispatchEvent(new DragEvent("dragend", { bubbles: true }));
|
|
890
|
+
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 };
|
|
891
|
+
}
|
|
892
|
+
switch (command) {
|
|
893
|
+
case "cp.state":
|
|
894
|
+
return a.tree === false ? pageState() : buildStateTree();
|
|
895
|
+
case "cp.snapshot":
|
|
896
|
+
return buildStateTree();
|
|
897
|
+
case "cp.find":
|
|
898
|
+
return findElements({
|
|
899
|
+
css: a.css,
|
|
900
|
+
text: a.text,
|
|
901
|
+
limit: a.limit,
|
|
902
|
+
role: a.role,
|
|
903
|
+
name: a.name,
|
|
904
|
+
label: a.label,
|
|
905
|
+
testid: a.testid,
|
|
906
|
+
"text-max": a["text-max"]
|
|
907
|
+
});
|
|
908
|
+
case "cp.click":
|
|
909
|
+
return clickElement();
|
|
910
|
+
case "cp.type":
|
|
911
|
+
return typeElement({ text: a.text });
|
|
912
|
+
case "cp.goto":
|
|
913
|
+
return goto({ url: a.url });
|
|
914
|
+
case "cp.eval":
|
|
915
|
+
return evalScript({ script: a.script });
|
|
916
|
+
case "cp.fill":
|
|
917
|
+
return fillElement({ text: a.text });
|
|
918
|
+
case "cp.select":
|
|
919
|
+
return selectElement({ label: a.label, value: a.value, option: a.option });
|
|
920
|
+
case "cp.keys":
|
|
921
|
+
return pressKeys({ keys: a.keys, key: a.key });
|
|
922
|
+
case "cp.hover":
|
|
923
|
+
return hoverElement();
|
|
924
|
+
case "cp.focus":
|
|
925
|
+
return focusElement();
|
|
926
|
+
case "cp.dblclick":
|
|
927
|
+
return dblclickElement();
|
|
928
|
+
case "cp.check":
|
|
929
|
+
return setChecked(true);
|
|
930
|
+
case "cp.uncheck":
|
|
931
|
+
return setChecked(false);
|
|
932
|
+
case "cp.scroll":
|
|
933
|
+
return scrollElement({ direction: a.direction, amount: a.amount });
|
|
934
|
+
case "cp.back":
|
|
935
|
+
return backNav();
|
|
936
|
+
case "cp.get":
|
|
937
|
+
return getElement({
|
|
938
|
+
what: a.what,
|
|
939
|
+
css: a.css,
|
|
940
|
+
ref: a.ref,
|
|
941
|
+
nth: a.nth,
|
|
942
|
+
"as": a["as"],
|
|
943
|
+
depth: a.depth,
|
|
944
|
+
"children-max": a["children-max"],
|
|
945
|
+
"text-max": a["text-max"],
|
|
946
|
+
max: a.max,
|
|
947
|
+
role: a.role,
|
|
948
|
+
name: a.name,
|
|
949
|
+
label: a.label,
|
|
950
|
+
text: a.text,
|
|
951
|
+
testid: a.testid
|
|
952
|
+
});
|
|
953
|
+
case "cp.extract":
|
|
954
|
+
return extractMarkdown({
|
|
955
|
+
"chunk-size": a["chunk-size"],
|
|
956
|
+
start: a.start,
|
|
957
|
+
selector: a.selector
|
|
958
|
+
});
|
|
959
|
+
case "cp.frames":
|
|
960
|
+
return listFrames();
|
|
961
|
+
case "cp.screenshot":
|
|
962
|
+
return screenshotMeta();
|
|
963
|
+
case "cp.network":
|
|
964
|
+
return networkEntriesLegacy({ filter: a.filter });
|
|
965
|
+
case "cp.wait":
|
|
966
|
+
return waitForCondition({
|
|
967
|
+
selector: a.selector,
|
|
968
|
+
text: a.text,
|
|
969
|
+
time: a.time,
|
|
970
|
+
timeout: a.timeout
|
|
971
|
+
});
|
|
972
|
+
case "cp.upload":
|
|
973
|
+
return uploadFiles({ files: a.files });
|
|
974
|
+
case "cp.drag":
|
|
975
|
+
return dragElements({ ref2: a.ref2, css2: a.css2 });
|
|
976
|
+
case "cp.title.marquee":
|
|
977
|
+
return startTitleMarquee2({ text: a.text, speed: a.speed });
|
|
978
|
+
case "cp.title.restore":
|
|
979
|
+
return stopTitleMarquee2();
|
|
980
|
+
default:
|
|
981
|
+
return { error: `unknown_command: ${command}` };
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
var RUNTIME_VERSION = "0.3.0-opencli-parity";
|
|
985
|
+
globalThis.__cpRunControl = runControl;
|
|
986
|
+
globalThis.__cpRuntimeVersion = RUNTIME_VERSION;
|
|
987
|
+
|
|
988
|
+
// 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';
|
|
990
|
+
|
|
991
|
+
// src/background/cdp.ts
|
|
992
|
+
var attached = /* @__PURE__ */ new Set();
|
|
993
|
+
var CDP_COMMAND_TIMEOUT_MS = 15e3;
|
|
994
|
+
function send(target, method, params) {
|
|
995
|
+
return chrome.debugger.sendCommand(target, method, params ?? {});
|
|
996
|
+
}
|
|
997
|
+
function isAttachableUrl(url) {
|
|
998
|
+
if (!url) return false;
|
|
999
|
+
return /^(https?|file|chrome-extension):/i.test(url) || /^chrome:\/\/extensions/i.test(url);
|
|
1000
|
+
}
|
|
1001
|
+
var networkCaptures = /* @__PURE__ */ new Map();
|
|
1002
|
+
var networkListenerRegistered = false;
|
|
1003
|
+
var networkEnabledTabs = /* @__PURE__ */ new Set();
|
|
1004
|
+
function netKey(tabId, seq, url) {
|
|
1005
|
+
try {
|
|
1006
|
+
const u = new URL(url);
|
|
1007
|
+
const seg = u.pathname.split("/").filter(Boolean).pop() || u.hostname;
|
|
1008
|
+
return `${seg.slice(0, 24)}-${tabId}${seq}`;
|
|
1009
|
+
} catch {
|
|
1010
|
+
return `req-${tabId}${seq}`;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function registerNetworkListener() {
|
|
1014
|
+
if (networkListenerRegistered) return;
|
|
1015
|
+
networkListenerRegistered = true;
|
|
1016
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
1017
|
+
const tabId = source.tabId;
|
|
1018
|
+
if (tabId === void 0) return;
|
|
1019
|
+
const state2 = networkCaptures.get(tabId);
|
|
1020
|
+
if (method === "Network.requestWillBeSent" && state2) {
|
|
1021
|
+
const p = params;
|
|
1022
|
+
const key = netKey(tabId, ++state2.seq, p.request.url);
|
|
1023
|
+
const entry = {
|
|
1024
|
+
key,
|
|
1025
|
+
requestId: p.requestId,
|
|
1026
|
+
method: p.request.method,
|
|
1027
|
+
url: p.request.url,
|
|
1028
|
+
status: null,
|
|
1029
|
+
ct: "",
|
|
1030
|
+
size: null,
|
|
1031
|
+
finished: false,
|
|
1032
|
+
resourceType: p.type ?? "Other",
|
|
1033
|
+
timestamp: Date.now(),
|
|
1034
|
+
postData: p.request.postData?.slice(0, 4e3)
|
|
1035
|
+
};
|
|
1036
|
+
state2.entries.push(entry);
|
|
1037
|
+
state2.byRequestId.set(p.requestId, entry);
|
|
1038
|
+
}
|
|
1039
|
+
if (method === "Network.responseReceived" && state2) {
|
|
1040
|
+
const p = params;
|
|
1041
|
+
const entry = state2.byRequestId.get(p.requestId);
|
|
1042
|
+
if (entry) {
|
|
1043
|
+
entry.status = p.response?.status ?? null;
|
|
1044
|
+
entry.ct = p.response?.mimeType ?? "";
|
|
1045
|
+
entry.resourceType = p.type ?? entry.resourceType;
|
|
1046
|
+
const len = p.response?.headers?.["content-length"] ?? p.response?.headers?.["Content-Length"];
|
|
1047
|
+
if (len) entry.size = Number(len);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if ((method === "Network.loadingFinished" || method === "Network.loadingFailed") && state2) {
|
|
1051
|
+
const p = params;
|
|
1052
|
+
const entry = state2.byRequestId.get(p.requestId);
|
|
1053
|
+
if (entry) {
|
|
1054
|
+
entry.finished = true;
|
|
1055
|
+
if (p.encodedDataLength != null && entry.size == null) entry.size = p.encodedDataLength;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
chrome.tabs.onRemoved?.addListener((tabId) => {
|
|
1060
|
+
networkCaptures.delete(tabId);
|
|
1061
|
+
networkEnabledTabs.delete(tabId);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
async function ensureNetworkEnabled(tabId) {
|
|
1065
|
+
if (networkEnabledTabs.has(tabId)) return;
|
|
1066
|
+
await send({ tabId }, "Network.enable");
|
|
1067
|
+
networkEnabledTabs.add(tabId);
|
|
1068
|
+
}
|
|
1069
|
+
async function readNetworkEntries(tabId, opts) {
|
|
1070
|
+
registerNetworkListener();
|
|
1071
|
+
await ensureAttached(tabId);
|
|
1072
|
+
await ensureNetworkEnabled(tabId);
|
|
1073
|
+
if (!networkCaptures.has(tabId)) {
|
|
1074
|
+
networkCaptures.set(tabId, { seq: 0, entries: [], byRequestId: /* @__PURE__ */ new Map() });
|
|
1075
|
+
}
|
|
1076
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1077
|
+
const state2 = networkCaptures.get(tabId);
|
|
1078
|
+
let entries = state2.entries.slice();
|
|
1079
|
+
if (!opts.all) {
|
|
1080
|
+
entries = entries.filter(
|
|
1081
|
+
(e) => ["XHR", "Fetch"].includes(e.resourceType) || /\/api\/|json/i.test(e.ct + e.url)
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
if (opts.filter) {
|
|
1085
|
+
const lower = opts.filter.toLowerCase();
|
|
1086
|
+
entries = entries.filter((e) => e.url.toLowerCase().includes(lower));
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
source: "cdp",
|
|
1090
|
+
entries: entries.slice(-100).map(({ key, method, status, url, ct, size, resourceType }) => ({
|
|
1091
|
+
key,
|
|
1092
|
+
method,
|
|
1093
|
+
status,
|
|
1094
|
+
url,
|
|
1095
|
+
ct,
|
|
1096
|
+
size,
|
|
1097
|
+
type: resourceType
|
|
1098
|
+
})),
|
|
1099
|
+
count: entries.length,
|
|
1100
|
+
capture_running: true
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
async function readNetworkDetail(tabId, key) {
|
|
1104
|
+
registerNetworkListener();
|
|
1105
|
+
await ensureAttached(tabId);
|
|
1106
|
+
await ensureNetworkEnabled(tabId);
|
|
1107
|
+
const state2 = networkCaptures.get(tabId);
|
|
1108
|
+
const entry = state2?.entries.find((e) => e.key === key || e.requestId === key);
|
|
1109
|
+
if (!entry) return { error: "network_key_not_found", key, hint: "run cp.network first; keys expire on page navigation" };
|
|
1110
|
+
const out = {
|
|
1111
|
+
key: entry.key,
|
|
1112
|
+
method: entry.method,
|
|
1113
|
+
status: entry.status,
|
|
1114
|
+
url: entry.url,
|
|
1115
|
+
ct: entry.ct,
|
|
1116
|
+
size: entry.size,
|
|
1117
|
+
request_post_data: entry.postData
|
|
1118
|
+
};
|
|
1119
|
+
if (entry.status != null && entry.status !== 204) {
|
|
1120
|
+
try {
|
|
1121
|
+
const body = await send({ tabId }, "Network.getResponseBody", { requestId: entry.requestId });
|
|
1122
|
+
if (body?.body != null) {
|
|
1123
|
+
const limit = 200 * 1024;
|
|
1124
|
+
out.response_body_truncated = body.body.length > limit;
|
|
1125
|
+
out.response_body = body.body.slice(0, limit);
|
|
1126
|
+
out.base64_encoded = !!body.base64Encoded;
|
|
1127
|
+
}
|
|
1128
|
+
} catch {
|
|
1129
|
+
out.response_body = null;
|
|
1130
|
+
out.note = "body evicted (resource unloaded or navigated)";
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return out;
|
|
1134
|
+
}
|
|
1135
|
+
var tabFrameContexts = /* @__PURE__ */ new Map();
|
|
1136
|
+
var frameTrackingRegistered = false;
|
|
1137
|
+
function registerFrameTracking() {
|
|
1138
|
+
if (frameTrackingRegistered) return;
|
|
1139
|
+
frameTrackingRegistered = true;
|
|
1140
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
1141
|
+
const tabId = source.tabId;
|
|
1142
|
+
if (tabId === void 0) return;
|
|
1143
|
+
if (method === "Runtime.executionContextCreated") {
|
|
1144
|
+
const ctx = params.context;
|
|
1145
|
+
if (!ctx?.auxData?.frameId || ctx.auxData.isDefault !== true) return;
|
|
1146
|
+
if (!tabFrameContexts.has(tabId)) tabFrameContexts.set(tabId, /* @__PURE__ */ new Map());
|
|
1147
|
+
tabFrameContexts.get(tabId).set(ctx.auxData.frameId, ctx.id);
|
|
1148
|
+
}
|
|
1149
|
+
if (method === "Runtime.executionContextDestroyed") {
|
|
1150
|
+
const ctxId = params.executionContextId;
|
|
1151
|
+
const contexts = tabFrameContexts.get(tabId);
|
|
1152
|
+
if (contexts) {
|
|
1153
|
+
for (const [fid, cid] of contexts) {
|
|
1154
|
+
if (cid === ctxId) {
|
|
1155
|
+
contexts.delete(fid);
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
chrome.tabs.onRemoved?.addListener((tabId) => tabFrameContexts.delete(tabId));
|
|
1163
|
+
}
|
|
1164
|
+
async function getFrameTree(tabId) {
|
|
1165
|
+
await ensureAttached(tabId);
|
|
1166
|
+
return send({ tabId }, "Page.getFrameTree");
|
|
1167
|
+
}
|
|
1168
|
+
var frameTargets = /* @__PURE__ */ new Map();
|
|
1169
|
+
var frameTargetCleanup = false;
|
|
1170
|
+
function registerFrameTargetCleanup() {
|
|
1171
|
+
if (frameTargetCleanup) return;
|
|
1172
|
+
frameTargetCleanup = true;
|
|
1173
|
+
chrome.debugger.onEvent.addListener((_source, method, params) => {
|
|
1174
|
+
if (method === "Target.detachedFromTarget") {
|
|
1175
|
+
const targetId = String(params.targetId || "");
|
|
1176
|
+
if (targetId && frameTargets.get("__byTarget__" + targetId)) {
|
|
1177
|
+
frameTargets.delete("__byTarget__" + targetId);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
async function ensureFrameTarget(tabId, frameId, targetUrl) {
|
|
1183
|
+
registerFrameTargetCleanup();
|
|
1184
|
+
await ensureAttached(tabId);
|
|
1185
|
+
const key = `${tabId}:${frameId}`;
|
|
1186
|
+
const existing = frameTargets.get(key);
|
|
1187
|
+
if (existing) return existing;
|
|
1188
|
+
await send({ tabId }, "Target.setDiscoverTargets", { discover: true }).catch(() => void 0);
|
|
1189
|
+
await send({ tabId }, "Target.setAutoAttach", {
|
|
1190
|
+
autoAttach: true,
|
|
1191
|
+
waitForDebuggerOnStart: false,
|
|
1192
|
+
flatten: true,
|
|
1193
|
+
filter: [{ type: "iframe", exclude: false }]
|
|
1194
|
+
}).catch(() => void 0);
|
|
1195
|
+
const result = await send({ tabId }, "Target.getTargets").catch(() => null);
|
|
1196
|
+
const targets = result?.targetInfos ?? [];
|
|
1197
|
+
const frameTarget = targets.find((c) => {
|
|
1198
|
+
const cid = c.targetId || c.id;
|
|
1199
|
+
return c.type === "iframe" && (cid === frameId || !!targetUrl && c.url === targetUrl);
|
|
1200
|
+
});
|
|
1201
|
+
const targetId = frameTarget?.targetId || frameTarget?.id;
|
|
1202
|
+
if (!targetId) {
|
|
1203
|
+
throw new Error(`No iframe target found for frame ${frameId}${targetUrl ? ` (${targetUrl})` : ""}`);
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
await chrome.debugger.attach({ targetId }, "1.3");
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1209
|
+
if (!message.includes("Another debugger is already attached")) throw e;
|
|
1210
|
+
}
|
|
1211
|
+
frameTargets.set(key, targetId);
|
|
1212
|
+
frameTargets.set("__byTarget__" + targetId, key);
|
|
1213
|
+
return targetId;
|
|
1214
|
+
}
|
|
1215
|
+
async function evaluateInFrame(tabId, expression, frameId, targetUrl) {
|
|
1216
|
+
registerFrameTracking();
|
|
1217
|
+
await ensureAttached(tabId);
|
|
1218
|
+
await send({ tabId }, "Runtime.enable").catch(() => void 0);
|
|
1219
|
+
let contexts = tabFrameContexts.get(tabId);
|
|
1220
|
+
let contextId = contexts?.get(frameId);
|
|
1221
|
+
if (contextId === void 0) {
|
|
1222
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1223
|
+
contexts = tabFrameContexts.get(tabId);
|
|
1224
|
+
contextId = contexts?.get(frameId);
|
|
1225
|
+
}
|
|
1226
|
+
if (contextId !== void 0) {
|
|
1227
|
+
try {
|
|
1228
|
+
const result2 = await send({ tabId }, "Runtime.evaluate", {
|
|
1229
|
+
expression,
|
|
1230
|
+
contextId,
|
|
1231
|
+
returnByValue: true,
|
|
1232
|
+
awaitPromise: true
|
|
1233
|
+
});
|
|
1234
|
+
if (result2.exceptionDetails) {
|
|
1235
|
+
throw new Error(result2.exceptionDetails.exception?.description || result2.exceptionDetails.text || "Eval error");
|
|
1236
|
+
}
|
|
1237
|
+
return result2.result?.value;
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
const msg = String(e?.message || e);
|
|
1240
|
+
if (!/Cannot find context|context with specified id|Execution context was destroyed/i.test(msg)) {
|
|
1241
|
+
throw e;
|
|
1242
|
+
}
|
|
1243
|
+
contexts?.delete(frameId);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
await send({ tabId }, "Runtime.enable").catch(() => void 0);
|
|
1247
|
+
const targetId = await ensureFrameTarget(tabId, frameId, targetUrl);
|
|
1248
|
+
const result = await send({ targetId }, "Runtime.evaluate", {
|
|
1249
|
+
expression,
|
|
1250
|
+
returnByValue: true,
|
|
1251
|
+
awaitPromise: true
|
|
1252
|
+
});
|
|
1253
|
+
if (result.exceptionDetails) {
|
|
1254
|
+
throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error");
|
|
1255
|
+
}
|
|
1256
|
+
return result.result?.value;
|
|
1257
|
+
}
|
|
1258
|
+
async function waitForDownload(pattern, timeoutMs) {
|
|
1259
|
+
const startedAt = Date.now();
|
|
1260
|
+
const lower = pattern?.toLowerCase();
|
|
1261
|
+
return new Promise((resolve) => {
|
|
1262
|
+
const timer = setTimeout(() => {
|
|
1263
|
+
chrome.downloads.onChanged.removeListener(listener);
|
|
1264
|
+
resolve({ error: "wait_timeout", message: `No matching download within ${timeoutMs}ms.`, pattern: pattern ?? null });
|
|
1265
|
+
}, timeoutMs);
|
|
1266
|
+
const listener = (_delta) => {
|
|
1267
|
+
chrome.downloads.search({ limit: 5, orderBy: ["-startTime"] }, (items) => {
|
|
1268
|
+
const match = items.find((it) => {
|
|
1269
|
+
if (it.state !== "complete") return false;
|
|
1270
|
+
const hay = `${it.filename ?? ""} ${it.url ?? ""} ${it.mime ?? ""}`.toLowerCase();
|
|
1271
|
+
return !lower || hay.includes(lower);
|
|
1272
|
+
});
|
|
1273
|
+
if (match) {
|
|
1274
|
+
clearTimeout(timer);
|
|
1275
|
+
chrome.downloads.onChanged.removeListener(listener);
|
|
1276
|
+
resolve({
|
|
1277
|
+
downloaded: true,
|
|
1278
|
+
file: (match.filename ?? "").split("/").pop() ?? match.filename,
|
|
1279
|
+
path: match.filename,
|
|
1280
|
+
url: match.url,
|
|
1281
|
+
mime: match.mime,
|
|
1282
|
+
bytes: match.fileSize,
|
|
1283
|
+
elapsedMs: Date.now() - startedAt
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1288
|
+
chrome.downloads.onChanged.addListener(listener);
|
|
1289
|
+
listener({});
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
async function setFileInputFiles(tabId, cssPath, files) {
|
|
1293
|
+
await ensureAttached(tabId);
|
|
1294
|
+
const doc = await send({ tabId }, "DOM.getDocument", { depth: 0 });
|
|
1295
|
+
const rootNodeId = doc.root?.nodeId;
|
|
1296
|
+
if (!rootNodeId) return { error: "dom_get_document_failed" };
|
|
1297
|
+
const node = await send({ tabId }, "DOM.querySelector", {
|
|
1298
|
+
nodeId: rootNodeId,
|
|
1299
|
+
selector: cssPath
|
|
1300
|
+
});
|
|
1301
|
+
if (!node || node.nodeId === void 0 || node.nodeId === 0) {
|
|
1302
|
+
return { error: "selector_not_found", selector: cssPath };
|
|
1303
|
+
}
|
|
1304
|
+
await send({ tabId }, "DOM.setFileInputFiles", { files, nodeId: node.nodeId });
|
|
1305
|
+
return { uploaded: true, files, via: "cdp-dom" };
|
|
1306
|
+
}
|
|
1307
|
+
async function ensureAttached(tabId) {
|
|
1308
|
+
registerFrameTracking();
|
|
1309
|
+
registerNetworkListener();
|
|
1310
|
+
if (attached.has(tabId)) return;
|
|
1311
|
+
const target = { tabId };
|
|
1312
|
+
try {
|
|
1313
|
+
await chrome.debugger.attach(target, "1.3");
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
throw new Error(`attach failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
|
1316
|
+
}
|
|
1317
|
+
attached.add(tabId);
|
|
1318
|
+
try {
|
|
1319
|
+
await send(target, "Runtime.enable");
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
chrome.debugger.onDetach.addListener((source) => {
|
|
1324
|
+
if (source.tabId != null && source.tabId !== void 0) {
|
|
1325
|
+
attached.delete(source.tabId);
|
|
1326
|
+
networkEnabledTabs.delete(source.tabId);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
var CDP_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1330
|
+
"Accessibility.enable",
|
|
1331
|
+
"Accessibility.getFullAXTree",
|
|
1332
|
+
"DOM.enable",
|
|
1333
|
+
"DOM.getDocument",
|
|
1334
|
+
"DOM.getBoxModel",
|
|
1335
|
+
"DOM.getContentQuads",
|
|
1336
|
+
"DOM.focus",
|
|
1337
|
+
"DOM.querySelector",
|
|
1338
|
+
"DOM.querySelectorAll",
|
|
1339
|
+
"DOM.scrollIntoViewIfNeeded",
|
|
1340
|
+
"DOMSnapshot.captureSnapshot",
|
|
1341
|
+
"Input.dispatchMouseEvent",
|
|
1342
|
+
"Input.dispatchKeyEvent",
|
|
1343
|
+
"Input.insertText",
|
|
1344
|
+
"Page.getLayoutMetrics",
|
|
1345
|
+
"Page.captureScreenshot",
|
|
1346
|
+
"Page.getFrameTree",
|
|
1347
|
+
"Page.handleJavaScriptDialog",
|
|
1348
|
+
"Runtime.enable",
|
|
1349
|
+
"Emulation.setDeviceMetricsOverride",
|
|
1350
|
+
"Emulation.clearDeviceMetricsOverride"
|
|
1351
|
+
]);
|
|
1352
|
+
async function sendRawCommand(tabId, method, params) {
|
|
1353
|
+
if (!CDP_ALLOWLIST.has(method)) {
|
|
1354
|
+
throw new Error(`cdp_method_not_permitted: ${method}`);
|
|
1355
|
+
}
|
|
1356
|
+
await ensureAttached(tabId);
|
|
1357
|
+
return send({ tabId }, method, params);
|
|
1358
|
+
}
|
|
1359
|
+
async function insertText(tabId, text) {
|
|
1360
|
+
await ensureAttached(tabId);
|
|
1361
|
+
await send({ tabId }, "Input.insertText", { text });
|
|
1362
|
+
}
|
|
1363
|
+
async function cdpEvaluate(tabId, expression, timeoutMs = CDP_COMMAND_TIMEOUT_MS) {
|
|
1364
|
+
await ensureAttached(tabId);
|
|
1365
|
+
const result = await chrome.debugger.sendCommand(
|
|
1366
|
+
{ tabId },
|
|
1367
|
+
"Runtime.evaluate",
|
|
1368
|
+
{ expression, returnByValue: true, awaitPromise: true }
|
|
1369
|
+
);
|
|
1370
|
+
if (result.exceptionDetails) {
|
|
1371
|
+
const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
|
|
1372
|
+
throw new Error(errMsg);
|
|
1373
|
+
}
|
|
1374
|
+
return result.result?.value;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/background/capture-visible-tab.ts
|
|
1378
|
+
var DEFAULT_CAPTURE_TIMEOUT_MS = 4e3;
|
|
1379
|
+
var MAX_ATTEMPTS = 3;
|
|
1380
|
+
var BACKOFF_MS = 300;
|
|
1381
|
+
var defaultDependencies = {
|
|
1382
|
+
capture: (windowId) => chrome.tabs.captureVisibleTab(windowId, { format: "png" }),
|
|
1383
|
+
focusWindow: (windowId) => chrome.windows.update(windowId, { focused: true }),
|
|
1384
|
+
activateTab: (tabId) => chrome.tabs.update(tabId, { active: true }),
|
|
1385
|
+
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
1386
|
+
getWindow: (windowId) => chrome.windows.get(windowId),
|
|
1387
|
+
getTab: (tabId) => chrome.tabs.get(tabId)
|
|
1388
|
+
};
|
|
1389
|
+
async function captureWithTimeout(windowId, dependencies, timeoutMs) {
|
|
1390
|
+
let timer;
|
|
1391
|
+
try {
|
|
1392
|
+
return await Promise.race([
|
|
1393
|
+
dependencies.capture(windowId),
|
|
1394
|
+
new Promise((_, reject) => {
|
|
1395
|
+
timer = setTimeout(() => reject(new Error(`capture_timeout:${timeoutMs}ms`)), timeoutMs);
|
|
1396
|
+
})
|
|
1397
|
+
]);
|
|
1398
|
+
} finally {
|
|
1399
|
+
if (timer) clearTimeout(timer);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function prepareCompositor(tab, dependencies) {
|
|
1403
|
+
await dependencies.focusWindow(tab.windowId);
|
|
1404
|
+
await dependencies.activateTab(tab.id);
|
|
1405
|
+
await dependencies.wait(150);
|
|
1406
|
+
}
|
|
1407
|
+
function logCapture(opts, level, message, meta) {
|
|
1408
|
+
opts?.onLog?.(level, message, meta);
|
|
1409
|
+
}
|
|
1410
|
+
async function classifyCaptureError(err3, tab, dependencies) {
|
|
1411
|
+
const message = err3 instanceof Error ? err3.message : String(err3);
|
|
1412
|
+
const lower = message.toLowerCase();
|
|
1413
|
+
if (lower.includes("permission denied") || lower.includes("cannot access contents") || lower.includes("activetab") || lower.includes("extension has not been invoked") || lower.includes("'<all_urls>'") || lower.includes("<all_urls>")) {
|
|
1414
|
+
return {
|
|
1415
|
+
type: "permission_denied",
|
|
1416
|
+
recoverableWithoutFocus: false,
|
|
1417
|
+
recoverableWithFocus: false,
|
|
1418
|
+
message
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
if (lower.includes("rate") || lower.includes("maximum number of screenshots") || lower.includes("quota")) {
|
|
1422
|
+
return {
|
|
1423
|
+
type: "rate_limited",
|
|
1424
|
+
recoverableWithoutFocus: false,
|
|
1425
|
+
recoverableWithFocus: false,
|
|
1426
|
+
message
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
if (lower.includes("image readback failed")) {
|
|
1430
|
+
return {
|
|
1431
|
+
type: "image_readback_failed",
|
|
1432
|
+
recoverableWithoutFocus: true,
|
|
1433
|
+
recoverableWithFocus: true,
|
|
1434
|
+
message
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
if (lower.startsWith("capture_timeout:")) {
|
|
1438
|
+
return {
|
|
1439
|
+
type: "timeout",
|
|
1440
|
+
recoverableWithoutFocus: true,
|
|
1441
|
+
recoverableWithFocus: true,
|
|
1442
|
+
message
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
try {
|
|
1446
|
+
const [win, tabInfo] = await Promise.all([
|
|
1447
|
+
dependencies.getWindow?.(tab.windowId),
|
|
1448
|
+
dependencies.getTab?.(tab.id)
|
|
1449
|
+
]);
|
|
1450
|
+
if (win?.state === "minimized") {
|
|
1451
|
+
return {
|
|
1452
|
+
type: "window_minimized",
|
|
1453
|
+
recoverableWithoutFocus: false,
|
|
1454
|
+
recoverableWithFocus: true,
|
|
1455
|
+
message
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
if (tabInfo && !tabInfo.active) {
|
|
1459
|
+
return {
|
|
1460
|
+
type: "tab_not_visible",
|
|
1461
|
+
recoverableWithoutFocus: false,
|
|
1462
|
+
recoverableWithFocus: true,
|
|
1463
|
+
message
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
type: "unknown",
|
|
1470
|
+
recoverableWithoutFocus: false,
|
|
1471
|
+
recoverableWithFocus: false,
|
|
1472
|
+
message
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
async function captureActiveTabScreenshot2(tab, options = {}) {
|
|
1476
|
+
const { dependencies = defaultDependencies, timeoutMs = DEFAULT_CAPTURE_TIMEOUT_MS, stealFocus = false } = options;
|
|
1477
|
+
const attemptErrors = [];
|
|
1478
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
1479
|
+
const isLastAttempt = attempt === MAX_ATTEMPTS;
|
|
1480
|
+
const shouldPrepareCompositor = isLastAttempt && stealFocus;
|
|
1481
|
+
try {
|
|
1482
|
+
if (shouldPrepareCompositor) {
|
|
1483
|
+
logCapture(options, "info", "capture preparing compositor before attempt", {
|
|
1484
|
+
attempt,
|
|
1485
|
+
tabId: tab.id,
|
|
1486
|
+
windowId: tab.windowId
|
|
1487
|
+
});
|
|
1488
|
+
await prepareCompositor(tab, dependencies);
|
|
1489
|
+
}
|
|
1490
|
+
logCapture(options, "debug", "capture attempt", {
|
|
1491
|
+
attempt,
|
|
1492
|
+
maxAttempts: MAX_ATTEMPTS,
|
|
1493
|
+
tabId: tab.id,
|
|
1494
|
+
windowId: tab.windowId,
|
|
1495
|
+
stealFocus,
|
|
1496
|
+
focusUsed: shouldPrepareCompositor
|
|
1497
|
+
});
|
|
1498
|
+
const startedAt = Date.now();
|
|
1499
|
+
const result = await captureWithTimeout(tab.windowId, dependencies, timeoutMs);
|
|
1500
|
+
logCapture(options, "info", "capture succeeded", {
|
|
1501
|
+
attempt,
|
|
1502
|
+
tabId: tab.id,
|
|
1503
|
+
windowId: tab.windowId,
|
|
1504
|
+
durationMs: Date.now() - startedAt
|
|
1505
|
+
});
|
|
1506
|
+
return result;
|
|
1507
|
+
} catch (err3) {
|
|
1508
|
+
const info = await classifyCaptureError(err3, tab, dependencies);
|
|
1509
|
+
attemptErrors.push({ attempt, type: info.type, message: info.message });
|
|
1510
|
+
const canRetryWithoutFocus = !isLastAttempt && info.recoverableWithoutFocus;
|
|
1511
|
+
const canRetryWithFocus = !isLastAttempt && info.recoverableWithFocus && stealFocus;
|
|
1512
|
+
const shouldRetry = canRetryWithoutFocus || canRetryWithFocus;
|
|
1513
|
+
logCapture(options, "warn", "capture attempt failed", {
|
|
1514
|
+
attempt,
|
|
1515
|
+
maxAttempts: MAX_ATTEMPTS,
|
|
1516
|
+
tabId: tab.id,
|
|
1517
|
+
windowId: tab.windowId,
|
|
1518
|
+
errorType: info.type,
|
|
1519
|
+
error: info.message,
|
|
1520
|
+
recoverableWithoutFocus: info.recoverableWithoutFocus,
|
|
1521
|
+
recoverableWithFocus: info.recoverableWithFocus,
|
|
1522
|
+
stealFocus,
|
|
1523
|
+
willRetry: shouldRetry
|
|
1524
|
+
});
|
|
1525
|
+
if (!shouldRetry) {
|
|
1526
|
+
const finalMessage = `capture_failed_after_${attempt}_attempts: ${info.type}: ${info.message}; attempts=${JSON.stringify(attemptErrors)}`;
|
|
1527
|
+
const finalErr = new Error(finalMessage);
|
|
1528
|
+
finalErr.errorType = info.type;
|
|
1529
|
+
throw finalErr;
|
|
1530
|
+
}
|
|
1531
|
+
await dependencies.wait(BACKOFF_MS);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
throw new Error("capture_failed_unreachable");
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/background/session-groups.ts
|
|
1538
|
+
var STORAGE_KEY = "hmSessionGroups";
|
|
1539
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1540
|
+
var hydrated = false;
|
|
1541
|
+
async function hydrate() {
|
|
1542
|
+
if (hydrated) return;
|
|
1543
|
+
try {
|
|
1544
|
+
const stored = await chrome.storage.session.get(STORAGE_KEY);
|
|
1545
|
+
const state2 = stored[STORAGE_KEY] ?? {};
|
|
1546
|
+
cache.clear();
|
|
1547
|
+
for (const [sessionId, entries] of Object.entries(state2)) {
|
|
1548
|
+
cache.set(sessionId, entries.map((e) => ({ ...e })));
|
|
1549
|
+
}
|
|
1550
|
+
} catch (err3) {
|
|
1551
|
+
console.warn("[HunterMate] failed to hydrate session groups", err3);
|
|
1552
|
+
} finally {
|
|
1553
|
+
hydrated = true;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async function persist() {
|
|
1557
|
+
const state2 = {};
|
|
1558
|
+
for (const [sessionId, entries] of cache.entries()) {
|
|
1559
|
+
state2[sessionId] = entries.map((e) => ({ ...e }));
|
|
1560
|
+
}
|
|
1561
|
+
try {
|
|
1562
|
+
await chrome.storage.session.set({ [STORAGE_KEY]: state2 });
|
|
1563
|
+
} catch (err3) {
|
|
1564
|
+
console.warn("[HunterMate] failed to persist session groups", err3);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function getSessionEntries(sessionId) {
|
|
1568
|
+
return cache.get(sessionId) ?? [];
|
|
1569
|
+
}
|
|
1570
|
+
async function setSessionEntries(sessionId, entries) {
|
|
1571
|
+
if (entries.length === 0) {
|
|
1572
|
+
cache.delete(sessionId);
|
|
1573
|
+
} else {
|
|
1574
|
+
cache.set(sessionId, entries);
|
|
1575
|
+
}
|
|
1576
|
+
await persist();
|
|
1577
|
+
}
|
|
1578
|
+
function hasExplicitTabTarget(args) {
|
|
1579
|
+
return ["tab", "tabId", "page", "targetId"].some((k) => args[k] !== void 0);
|
|
1580
|
+
}
|
|
1581
|
+
function buildGroupTitle(args, sessionId) {
|
|
1582
|
+
const raw = String(args.title ?? args.summary ?? args.taskTitle ?? args.taskName ?? "").trim();
|
|
1583
|
+
const compact = raw ? raw.replace(/\s+/g, " ").slice(0, 12) : sessionId.slice(0, 8);
|
|
1584
|
+
return `\u{1F916}${compact}`;
|
|
1585
|
+
}
|
|
1586
|
+
async function ensureSessionGroup(sessionId, title, tab) {
|
|
1587
|
+
await hydrate();
|
|
1588
|
+
if (!tab.id || tab.windowId == null) {
|
|
1589
|
+
throw new Error("ensureSessionGroup_requires_tab_id_and_window_id");
|
|
1590
|
+
}
|
|
1591
|
+
const entries = getSessionEntries(sessionId);
|
|
1592
|
+
const existing = entries.find((e) => e.windowId === tab.windowId);
|
|
1593
|
+
if (existing) {
|
|
1594
|
+
try {
|
|
1595
|
+
await chrome.tabGroups.get(existing.groupId);
|
|
1596
|
+
} catch {
|
|
1597
|
+
const cleaned = entries.filter((e) => e.groupId !== existing.groupId);
|
|
1598
|
+
await setSessionEntries(sessionId, cleaned);
|
|
1599
|
+
return ensureSessionGroup(sessionId, title, tab);
|
|
1600
|
+
}
|
|
1601
|
+
if (tab.groupId !== existing.groupId) {
|
|
1602
|
+
try {
|
|
1603
|
+
await chrome.tabs.group({ groupId: existing.groupId, tabIds: [tab.id] });
|
|
1604
|
+
} catch (err3) {
|
|
1605
|
+
console.warn("[HunterMate] failed to add tab to existing session group", { sessionId, groupId: existing.groupId, tabId: tab.id, err: err3 });
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
if (title && title !== existing.title) {
|
|
1609
|
+
try {
|
|
1610
|
+
await chrome.tabGroups.update(existing.groupId, { title });
|
|
1611
|
+
existing.title = title;
|
|
1612
|
+
await persist();
|
|
1613
|
+
} catch (err3) {
|
|
1614
|
+
console.warn("[HunterMate] failed to update session group title", { sessionId, groupId: existing.groupId, err: err3 });
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (!existing.tabIds.includes(tab.id)) {
|
|
1618
|
+
existing.tabIds.push(tab.id);
|
|
1619
|
+
await persist();
|
|
1620
|
+
}
|
|
1621
|
+
return { groupId: existing.groupId, created: false };
|
|
1622
|
+
}
|
|
1623
|
+
let groupId;
|
|
1624
|
+
try {
|
|
1625
|
+
groupId = await chrome.tabs.group({ tabIds: [tab.id] });
|
|
1626
|
+
} catch (err3) {
|
|
1627
|
+
console.warn("[HunterMate] failed to create session group", { sessionId, tabId: tab.id, err: err3 });
|
|
1628
|
+
throw err3;
|
|
1629
|
+
}
|
|
1630
|
+
try {
|
|
1631
|
+
await chrome.tabGroups.update(groupId, {
|
|
1632
|
+
title,
|
|
1633
|
+
color: "grey",
|
|
1634
|
+
collapsed: false
|
|
1635
|
+
});
|
|
1636
|
+
} catch (err3) {
|
|
1637
|
+
console.warn("[HunterMate] failed to update newly created session group", { sessionId, groupId, err: err3 });
|
|
1638
|
+
}
|
|
1639
|
+
const entry = {
|
|
1640
|
+
groupId,
|
|
1641
|
+
windowId: tab.windowId,
|
|
1642
|
+
title,
|
|
1643
|
+
tabIds: [tab.id],
|
|
1644
|
+
createdAt: Date.now()
|
|
1645
|
+
};
|
|
1646
|
+
entries.push(entry);
|
|
1647
|
+
await setSessionEntries(sessionId, entries);
|
|
1648
|
+
return { groupId, created: true };
|
|
1649
|
+
}
|
|
1650
|
+
async function addTabToSessionGroup(sessionId, title, tab) {
|
|
1651
|
+
if (!sessionId || !tab.id || tab.windowId == null) {
|
|
1652
|
+
return { created: false };
|
|
1653
|
+
}
|
|
1654
|
+
try {
|
|
1655
|
+
const result = await ensureSessionGroup(sessionId, title, tab);
|
|
1656
|
+
return result;
|
|
1657
|
+
} catch (err3) {
|
|
1658
|
+
console.warn("[HunterMate] addTabToSessionGroup failed", { sessionId, tabId: tab.id, err: err3 });
|
|
1659
|
+
return { created: false };
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async function evictClosedTab(tabId) {
|
|
1663
|
+
await hydrate();
|
|
1664
|
+
let dirty = false;
|
|
1665
|
+
for (const [sessionId, entries] of cache.entries()) {
|
|
1666
|
+
for (const entry of entries) {
|
|
1667
|
+
const before = entry.tabIds.length;
|
|
1668
|
+
entry.tabIds = entry.tabIds.filter((id) => id !== tabId);
|
|
1669
|
+
if (entry.tabIds.length !== before) {
|
|
1670
|
+
dirty = true;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const cleaned = entries.filter((e) => e.tabIds.length > 0);
|
|
1674
|
+
if (cleaned.length !== entries.length) {
|
|
1675
|
+
dirty = true;
|
|
1676
|
+
}
|
|
1677
|
+
cache.set(sessionId, cleaned);
|
|
1678
|
+
}
|
|
1679
|
+
if (dirty) {
|
|
1680
|
+
await persist();
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1684
|
+
evictClosedTab(tabId).catch((err3) => console.warn("[HunterMate] evictClosedTab failed", err3));
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
// src/background/control.ts
|
|
1688
|
+
var boundTabId = null;
|
|
1689
|
+
async function getActiveTab() {
|
|
1690
|
+
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1691
|
+
return tabs[0] || null;
|
|
1692
|
+
}
|
|
1693
|
+
async function getControlTab(tabOverride) {
|
|
1694
|
+
if (typeof tabOverride === "string" || typeof tabOverride === "number") {
|
|
1695
|
+
const tabId = Number(tabOverride);
|
|
1696
|
+
if (Number.isFinite(tabId)) {
|
|
1697
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
1698
|
+
if (tab) return tab;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (boundTabId != null) {
|
|
1702
|
+
const tab = await chrome.tabs.get(boundTabId).catch(() => null);
|
|
1703
|
+
if (tab) return tab;
|
|
1704
|
+
boundTabId = null;
|
|
1705
|
+
}
|
|
1706
|
+
return getActiveTab();
|
|
1707
|
+
}
|
|
1708
|
+
async function frameListCdp(tabId) {
|
|
1709
|
+
const tree = await getFrameTree(tabId);
|
|
1710
|
+
const mainUrl = tree?.frameTree?.frame?.url ?? "";
|
|
1711
|
+
const out = [];
|
|
1712
|
+
const walk = (node) => {
|
|
1713
|
+
const f = node?.frame;
|
|
1714
|
+
if (!f) return;
|
|
1715
|
+
let cross = false;
|
|
1716
|
+
try {
|
|
1717
|
+
cross = new URL(f.url ?? "about:blank").origin !== new URL(mainUrl || "about:blank").origin;
|
|
1718
|
+
} catch {
|
|
1719
|
+
}
|
|
1720
|
+
out.push({ index: out.length, frame_id: f.id ?? "", url: f.url ?? "", name: f.name ?? "", cross_origin: cross });
|
|
1721
|
+
for (const child of node?.childFrames ?? []) {
|
|
1722
|
+
walk(child);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
walk(tree?.frameTree);
|
|
1726
|
+
return { frames: out };
|
|
1727
|
+
}
|
|
1728
|
+
function buildRuntimeExpression(command, args) {
|
|
1729
|
+
return '(() => { try { if (typeof globalThis.__cpRunControl !== "function" || globalThis.__cpRuntimeVersion !== ' + JSON.stringify(RUNTIME_VERSION) + ") {\n" + control_runtime_source_default + "\n} const r = globalThis.__cpRunControl(" + JSON.stringify(command) + ", " + JSON.stringify(args) + '); if (r && typeof r.then === "function") { return r; } return r; } catch (e) { return { __cpError: String((e && e.message) || e) }; } })()';
|
|
1730
|
+
}
|
|
1731
|
+
var marqueeCounters = /* @__PURE__ */ new Map();
|
|
1732
|
+
async function startTitleMarquee(tabId, sessionId, text) {
|
|
1733
|
+
if (!isAttachableUrl((await chrome.tabs.get(tabId).catch(() => null))?.url)) {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
const entry = marqueeCounters.get(tabId);
|
|
1737
|
+
if (entry) {
|
|
1738
|
+
entry.count++;
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
marqueeCounters.set(tabId, { count: 1, sessionId });
|
|
1742
|
+
try {
|
|
1743
|
+
const expression = buildRuntimeExpression("cp.title.marquee", { text: text ?? "\u{1F916}" });
|
|
1744
|
+
await cdpEvaluate(tabId, expression);
|
|
1745
|
+
} catch (err3) {
|
|
1746
|
+
console.warn("[HunterMate] startTitleMarquee failed", { tabId, error: err3.message });
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async function stopTitleMarquee(tabId, _sessionId) {
|
|
1750
|
+
const entry = marqueeCounters.get(tabId);
|
|
1751
|
+
if (!entry) return;
|
|
1752
|
+
entry.count--;
|
|
1753
|
+
if (entry.count > 0) return;
|
|
1754
|
+
marqueeCounters.delete(tabId);
|
|
1755
|
+
try {
|
|
1756
|
+
const expression = buildRuntimeExpression("cp.title.restore", {});
|
|
1757
|
+
await cdpEvaluate(tabId, expression);
|
|
1758
|
+
} catch (err3) {
|
|
1759
|
+
console.warn("[HunterMate] stopTitleMarquee failed", { tabId, error: err3.message });
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
async function withSessionBrackets(sessionId, tab, explicitTab, groupTitle, fn) {
|
|
1763
|
+
if (sessionId && !explicitTab && tab.id && tab.windowId != null) {
|
|
1764
|
+
ensureSessionGroup(sessionId, groupTitle, tab).catch(
|
|
1765
|
+
(err3) => console.warn("[HunterMate] ensureSessionGroup failed", { sessionId, tabId: tab.id, error: err3.message })
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
if (sessionId && tab.id) {
|
|
1769
|
+
startTitleMarquee(tab.id, sessionId).catch(() => {
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
try {
|
|
1773
|
+
return await fn();
|
|
1774
|
+
} finally {
|
|
1775
|
+
if (sessionId && tab.id) {
|
|
1776
|
+
stopTitleMarquee(tab.id, sessionId).catch(() => {
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
async function executeControlCommand(rawCommand, rawArgs, options = {}) {
|
|
1782
|
+
const { onLog } = options;
|
|
1783
|
+
function log(level, message, meta) {
|
|
1784
|
+
onLog?.(level, message, meta);
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
const controlArgs = rawArgs && typeof rawArgs === "object" ? { ...rawArgs } : {};
|
|
1788
|
+
let command = rawCommand;
|
|
1789
|
+
if (command === "cp.waitFor") {
|
|
1790
|
+
command = "cp.wait";
|
|
1791
|
+
if (typeof controlArgs.expression === "string" && controlArgs.script === void 0) {
|
|
1792
|
+
controlArgs.script = controlArgs.expression;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
if (controlArgs.selector !== void 0 && controlArgs.css === void 0) {
|
|
1796
|
+
controlArgs.css = controlArgs.selector;
|
|
1797
|
+
}
|
|
1798
|
+
if (command === "cp.fill" && controlArgs.value !== void 0 && controlArgs.text === void 0) {
|
|
1799
|
+
controlArgs.text = controlArgs.value;
|
|
1800
|
+
}
|
|
1801
|
+
if (command === "cp.eval" && typeof controlArgs.expression === "string" && controlArgs.script === void 0) {
|
|
1802
|
+
controlArgs.script = controlArgs.expression;
|
|
1803
|
+
}
|
|
1804
|
+
const tabOverride = controlArgs.__tab;
|
|
1805
|
+
delete controlArgs.__tab;
|
|
1806
|
+
const sessionId = String(controlArgs.taskId ?? options.sessionId ?? "");
|
|
1807
|
+
const explicitTab = hasExplicitTabTarget(controlArgs);
|
|
1808
|
+
const groupTitle = sessionId ? buildGroupTitle(controlArgs, sessionId) : "";
|
|
1809
|
+
if (command === "cp.screenshot") {
|
|
1810
|
+
const tab2 = await getControlTab(tabOverride);
|
|
1811
|
+
if (!tab2?.id) return err2("no_active_tab");
|
|
1812
|
+
const stealFocus = controlArgs.stealFocus === true;
|
|
1813
|
+
const result = await withSessionBrackets(sessionId, tab2, explicitTab, groupTitle, async () => {
|
|
1814
|
+
log("debug", "cp.screenshot capturing", { tabId: tab2.id, windowId: tab2.windowId, stealFocus });
|
|
1815
|
+
const dataUrl = await captureActiveTabScreenshot2(
|
|
1816
|
+
{ id: tab2.id, windowId: tab2.windowId },
|
|
1817
|
+
{
|
|
1818
|
+
stealFocus,
|
|
1819
|
+
onLog: (level, message, meta) => log(level, `[capture] ${message}`, meta)
|
|
1820
|
+
}
|
|
1821
|
+
);
|
|
1822
|
+
log("info", "cp.screenshot captured", { tabId: tab2.id, windowId: tab2.windowId, stealFocus });
|
|
1823
|
+
return { screenshot: dataUrl };
|
|
1824
|
+
});
|
|
1825
|
+
return ok(result);
|
|
1826
|
+
}
|
|
1827
|
+
if (command.startsWith("cp.tab.")) {
|
|
1828
|
+
if (command === "cp.tab.new") {
|
|
1829
|
+
const result2 = await handleTabCommand(command, controlArgs, log);
|
|
1830
|
+
const page = typeof result2 === "object" && result2 ? result2.page : void 0;
|
|
1831
|
+
if (sessionId && page && !explicitTab) {
|
|
1832
|
+
const newTabId = Number(page);
|
|
1833
|
+
if (Number.isFinite(newTabId)) {
|
|
1834
|
+
const newTab = await chrome.tabs.get(newTabId).catch(() => null);
|
|
1835
|
+
if (newTab) {
|
|
1836
|
+
addTabToSessionGroup(sessionId, groupTitle, newTab).catch(
|
|
1837
|
+
(err3) => log("warn", "cp.tab.new addTabToSessionGroup failed", { sessionId, tabId: newTabId, error: err3.message })
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return ok(result2);
|
|
1843
|
+
}
|
|
1844
|
+
const tab2 = await getControlTab(tabOverride);
|
|
1845
|
+
if (!tab2?.id) return err2("no_active_tab");
|
|
1846
|
+
const result = await withSessionBrackets(
|
|
1847
|
+
sessionId,
|
|
1848
|
+
tab2,
|
|
1849
|
+
explicitTab,
|
|
1850
|
+
groupTitle,
|
|
1851
|
+
async () => handleTabCommand(command, controlArgs, log)
|
|
1852
|
+
);
|
|
1853
|
+
return ok(result);
|
|
1854
|
+
}
|
|
1855
|
+
if (command === "cp.group.ensure") {
|
|
1856
|
+
const tab2 = await getControlTab(tabOverride);
|
|
1857
|
+
if (!tab2?.id || tab2.windowId == null) return err2("no_active_tab");
|
|
1858
|
+
const title = typeof controlArgs.title === "string" ? controlArgs.title : groupTitle;
|
|
1859
|
+
const { groupId, created } = await ensureSessionGroup(sessionId || `manual-${Date.now()}`, title || buildGroupTitle(controlArgs, sessionId || "manual"), tab2);
|
|
1860
|
+
return ok({ groupId, created, title });
|
|
1861
|
+
}
|
|
1862
|
+
if (command === "cp.cookies") {
|
|
1863
|
+
return ok(await handleCookiesCommand(controlArgs));
|
|
1864
|
+
}
|
|
1865
|
+
if (command === "cp.window.close") {
|
|
1866
|
+
const tab2 = await getControlTab(tabOverride);
|
|
1867
|
+
if (!tab2?.windowId) return err2("no_active_tab");
|
|
1868
|
+
const windowId = typeof controlArgs.windowId === "number" ? controlArgs.windowId : tab2.windowId;
|
|
1869
|
+
await chrome.windows.remove(windowId);
|
|
1870
|
+
return ok({ closed: true, windowId });
|
|
1871
|
+
}
|
|
1872
|
+
const tab = await getControlTab(tabOverride);
|
|
1873
|
+
if (!tab?.id) return err2("no_active_tab");
|
|
1874
|
+
if (sessionId && !explicitTab) {
|
|
1875
|
+
ensureSessionGroup(sessionId, groupTitle, tab).catch(
|
|
1876
|
+
(err3) => log("warn", "ensureSessionGroup failed", { sessionId, tabId: tab.id, error: err3.message })
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
if (sessionId) {
|
|
1880
|
+
startTitleMarquee(tab.id, sessionId).catch(() => {
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
try {
|
|
1884
|
+
if (command === "cp.network" && isAttachableUrl(tab.url)) {
|
|
1885
|
+
try {
|
|
1886
|
+
const detail = controlArgs.detail;
|
|
1887
|
+
const result = typeof detail === "string" ? await readNetworkDetail(tab.id, detail) : await readNetworkEntries(tab.id, {
|
|
1888
|
+
filter: typeof controlArgs.filter === "string" ? controlArgs.filter : void 0,
|
|
1889
|
+
all: controlArgs.all === true
|
|
1890
|
+
});
|
|
1891
|
+
return ok(result);
|
|
1892
|
+
} catch (e) {
|
|
1893
|
+
console.warn("[HunterMate] CDP network failed, falling back:", e);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (command === "cp.frames" && isAttachableUrl(tab.url)) {
|
|
1897
|
+
try {
|
|
1898
|
+
return ok(await frameListCdp(tab.id));
|
|
1899
|
+
} catch (e) {
|
|
1900
|
+
console.warn("[HunterMate] CDP frames failed, falling back:", e);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
if (command === "cp.wait" && (controlArgs.download === true || controlArgs.type === "download")) {
|
|
1904
|
+
const timeout = typeof controlArgs.timeout === "number" ? controlArgs.timeout : 3e4;
|
|
1905
|
+
const result = await waitForDownload(
|
|
1906
|
+
typeof controlArgs.pattern === "string" ? controlArgs.pattern : void 0,
|
|
1907
|
+
timeout
|
|
1908
|
+
);
|
|
1909
|
+
return ok(result);
|
|
1910
|
+
}
|
|
1911
|
+
if (command === "cp.upload" && Array.isArray(controlArgs.files) && typeof controlArgs.css === "string" && controlArgs.css && isAttachableUrl(tab.url)) {
|
|
1912
|
+
try {
|
|
1913
|
+
const result = await setFileInputFiles(tab.id, controlArgs.css, controlArgs.files);
|
|
1914
|
+
return ok(result);
|
|
1915
|
+
} catch (e) {
|
|
1916
|
+
console.warn("[HunterMate] CDP upload failed, falling back:", e);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (command === "cp.cdp" && typeof controlArgs.method === "string") {
|
|
1920
|
+
if (!isAttachableUrl(tab.url)) return err2("cdp_requires_http_page");
|
|
1921
|
+
try {
|
|
1922
|
+
const params = controlArgs.params && typeof controlArgs.params === "object" ? { ...controlArgs.params } : {};
|
|
1923
|
+
const result = await sendRawCommand(tab.id, controlArgs.method, params);
|
|
1924
|
+
return ok({ method: controlArgs.method, result });
|
|
1925
|
+
} catch (e) {
|
|
1926
|
+
return err2(e instanceof Error ? e.message : String(e));
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (command === "cp.insert" && typeof controlArgs.text === "string") {
|
|
1930
|
+
if (!isAttachableUrl(tab.url)) return err2("insert_requires_http_page");
|
|
1931
|
+
try {
|
|
1932
|
+
if (controlArgs.ref !== void 0 || typeof controlArgs.css === "string") {
|
|
1933
|
+
log("debug", "cp.insert focusing target before insert", { ref: controlArgs.ref, css: controlArgs.css });
|
|
1934
|
+
try {
|
|
1935
|
+
await cdpEvaluate(tab.id, buildInjectionExpression("cp.focus", { ref: controlArgs.ref, css: controlArgs.css }));
|
|
1936
|
+
} catch {
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
await insertText(tab.id, controlArgs.text);
|
|
1940
|
+
return ok({ inserted: true, length: controlArgs.text.length });
|
|
1941
|
+
} catch (e) {
|
|
1942
|
+
return err2(e instanceof Error ? e.message : String(e));
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
if (command === "cp.eval" && typeof controlArgs.script === "string") {
|
|
1946
|
+
const script = controlArgs.script;
|
|
1947
|
+
const frame = controlArgs.frame;
|
|
1948
|
+
if (frame !== void 0 && isAttachableUrl(tab.url)) {
|
|
1949
|
+
let frameId = typeof frame === "string" ? frame : "";
|
|
1950
|
+
let frameUrl;
|
|
1951
|
+
if (typeof frame === "number" || /^\d+$/.test(String(frame))) {
|
|
1952
|
+
const list = await frameListCdp(tab.id);
|
|
1953
|
+
const target = list.frames?.find((f) => f.index === Number(frame));
|
|
1954
|
+
if (!target) return err2(`frame_not_found: ${frame}`);
|
|
1955
|
+
frameId = target.frame_id;
|
|
1956
|
+
frameUrl = target.url;
|
|
1957
|
+
}
|
|
1958
|
+
const result2 = await evaluateInFrame(tab.id, `(async () => { ${script} })()`, frameId, frameUrl);
|
|
1959
|
+
return ok(result2);
|
|
1960
|
+
}
|
|
1961
|
+
const result = await cdpEvaluate(tab.id, `(async () => { ${script} })()`);
|
|
1962
|
+
return ok(result);
|
|
1963
|
+
}
|
|
1964
|
+
if (command === "hm.linkedin.extract") {
|
|
1965
|
+
if (!isAttachableUrl(tab.url)) {
|
|
1966
|
+
return err2("linkedin_extract_requires_http_page");
|
|
1967
|
+
}
|
|
1968
|
+
try {
|
|
1969
|
+
const result = await cdpEvaluate(tab.id, LINKEDIN_EXTRACT_SOURCE);
|
|
1970
|
+
return ok(result);
|
|
1971
|
+
} catch (e) {
|
|
1972
|
+
return err2(e instanceof Error ? e.message : String(e));
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (command === "hm.hiredchina.extract") {
|
|
1976
|
+
if (!isAttachableUrl(tab.url)) {
|
|
1977
|
+
return err2("hiredchina_extract_requires_http_page");
|
|
1978
|
+
}
|
|
1979
|
+
try {
|
|
1980
|
+
const result = await cdpEvaluate(tab.id, HIREDCHINA_EXTRACT_SOURCE);
|
|
1981
|
+
return ok(result);
|
|
1982
|
+
} catch (e) {
|
|
1983
|
+
return err2(e instanceof Error ? e.message : String(e));
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (command.startsWith("hm.")) {
|
|
1987
|
+
return { status: "error", code: "not_implemented", message: `${command} is not implemented yet` };
|
|
1988
|
+
}
|
|
1989
|
+
if (isAttachableUrl(tab.url)) {
|
|
1990
|
+
log("debug", "control running via CDP runtime", { command, tabId: tab.id });
|
|
1991
|
+
try {
|
|
1992
|
+
const result = await cdpEvaluate(tab.id, buildInjectionExpression(command, controlArgs));
|
|
1993
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "__cpError" in result) {
|
|
1994
|
+
return err2(String(result.__cpError));
|
|
1995
|
+
}
|
|
1996
|
+
return ok(result);
|
|
1997
|
+
} catch (e) {
|
|
1998
|
+
console.warn("[HunterMate] CDP control failed, falling back to executeScript:", e);
|
|
1999
|
+
}
|
|
2000
|
+
} else if (tab.url?.startsWith("chrome://") || tab.url?.startsWith("chrome-extension://") || tab.url?.startsWith("about:")) {
|
|
2001
|
+
return err2(
|
|
2002
|
+
`Cannot access a ${tab.url.split("/")[0]}// page for control commands. Open the target page in a normal tab first.`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
const [injection] = await chrome.scripting.executeScript({
|
|
2006
|
+
target: { tabId: tab.id },
|
|
2007
|
+
func: runControl,
|
|
2008
|
+
args: [command, controlArgs]
|
|
2009
|
+
});
|
|
2010
|
+
const inj = injection;
|
|
2011
|
+
if (inj?.error) return err2(String(inj.error));
|
|
2012
|
+
return ok(inj?.result);
|
|
2013
|
+
} finally {
|
|
2014
|
+
if (sessionId && tab?.id) {
|
|
2015
|
+
stopTitleMarquee(tab.id, sessionId).catch(() => {
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
} catch (e) {
|
|
2020
|
+
console.error("[HunterMate] executeControlCommand error", e);
|
|
2021
|
+
return err2(e instanceof Error ? e.message : String(e));
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
function buildInjectionExpression(command, controlArgs) {
|
|
2025
|
+
return '(() => { try { if (typeof globalThis.__cpRunControl !== "function" || globalThis.__cpRuntimeVersion !== ' + JSON.stringify(RUNTIME_VERSION) + ") {\n" + control_runtime_source_default + "\n} const r = globalThis.__cpRunControl(" + JSON.stringify(command) + ", " + JSON.stringify(controlArgs) + '); if (r && typeof r.then === "function") { return r; } return r; } catch (e) { return { __cpError: String((e && e.message) || e) }; } })()';
|
|
2026
|
+
}
|
|
2027
|
+
function ok(result) {
|
|
2028
|
+
return { status: "ok", result };
|
|
2029
|
+
}
|
|
2030
|
+
var LINKEDIN_EXTRACT_SOURCE = String.raw`(async () => {
|
|
2031
|
+
const waitFor = (fn, timeout, interval) => new Promise((resolve) => {
|
|
2032
|
+
const t0 = Date.now();
|
|
2033
|
+
const tick = () => {
|
|
2034
|
+
let v = null;
|
|
2035
|
+
try { v = fn(); } catch (e) {}
|
|
2036
|
+
if (v) return resolve(v);
|
|
2037
|
+
if (Date.now() - t0 > (timeout || 8000)) return resolve(null);
|
|
2038
|
+
setTimeout(tick, interval || 250);
|
|
2039
|
+
};
|
|
2040
|
+
tick();
|
|
2041
|
+
});
|
|
2042
|
+
if (document.readyState !== 'complete') {
|
|
2043
|
+
await waitFor(() => document.readyState === 'complete', 12000, 200);
|
|
2044
|
+
}
|
|
2045
|
+
const path = location.pathname;
|
|
2046
|
+
const jsonLdAll = () => Array.from(document.querySelectorAll('script[type="application/ld+json"]'))
|
|
2047
|
+
.map((s) => { try { return JSON.parse(s.textContent); } catch (e) { return null; } })
|
|
2048
|
+
.filter(Boolean)
|
|
2049
|
+
.flatMap((x) => (Array.isArray(x) ? x : [x]));
|
|
2050
|
+
const meta = (sel) => {
|
|
2051
|
+
const el = document.querySelector(sel);
|
|
2052
|
+
return (el && el.content ? String(el.content).trim() : null) || null;
|
|
2053
|
+
};
|
|
2054
|
+
const text = (sel) => {
|
|
2055
|
+
const el = document.querySelector(sel);
|
|
2056
|
+
return el ? el.textContent.trim().replace(/\s+/g, ' ') : null;
|
|
2057
|
+
};
|
|
2058
|
+
const strip = (html) => String(html || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
2059
|
+
|
|
2060
|
+
let pageClass = 'unknown';
|
|
2061
|
+
if (/^\/in\/[^/]+/.test(path)) pageClass = 'talent';
|
|
2062
|
+
else if (/^\/company\/[^/]+/.test(path)) pageClass = 'company';
|
|
2063
|
+
else if (/^\/jobs\/view\//.test(path)) pageClass = 'job';
|
|
2064
|
+
else if (/^\/(jobs\/(search|collections|job)|search\/results\/(people|companies|content))/.test(path)) pageClass = 'list';
|
|
2065
|
+
|
|
2066
|
+
const canonical = (document.querySelector('link[rel="canonical"]') || {}).href || location.href.split('?')[0];
|
|
2067
|
+
const entities = [];
|
|
2068
|
+
const links = { talent: [], company: [], job: [] };
|
|
2069
|
+
|
|
2070
|
+
if (pageClass === 'talent') {
|
|
2071
|
+
await waitFor(() => document.querySelector('main h1, main h2, h1, h2'), 9000);
|
|
2072
|
+
const person = jsonLdAll().find((x) => x['@type'] === 'Person');
|
|
2073
|
+
// 现代 LinkedIn profile 无 h1/JSON-LD(混淆 class 不稳定),用 main innerText 行结构解析:
|
|
2074
|
+
// [0]=展示名 [· 3rd]=人脉度 [2]=headline [3]=location
|
|
2075
|
+
const lines = ((document.querySelector('main') || document.body).innerText || '')
|
|
2076
|
+
.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
2077
|
+
const titleName = (document.title.split('|')[0] || '').trim() || null;
|
|
2078
|
+
let ni = 0;
|
|
2079
|
+
while (ni < lines.length && lines[ni].startsWith('·')) ni++;
|
|
2080
|
+
const displayName = ni < lines.length ? lines[ni] : null;
|
|
2081
|
+
let hi = ni + 1;
|
|
2082
|
+
while (hi < lines.length && lines[hi].startsWith('·')) hi++;
|
|
2083
|
+
const lineHeadline = hi < lines.length && lines[hi].length > 2 ? lines[hi] : null;
|
|
2084
|
+
let li = hi + 1;
|
|
2085
|
+
while (li < lines.length && lines[li].startsWith('·')) li++;
|
|
2086
|
+
const lineLoc = li < lines.length && !/contact info/i.test(lines[li]) && lines[li].length < 80 ? lines[li] : null;
|
|
2087
|
+
entities.push({
|
|
2088
|
+
entityType: 'talent',
|
|
2089
|
+
name: (person && person.name) || titleName || displayName,
|
|
2090
|
+
displayName: displayName || null,
|
|
2091
|
+
headline: (person && person.jobTitle) || lineHeadline || null,
|
|
2092
|
+
location: (person && person.address && person.address.addressLocality) || lineLoc || null,
|
|
2093
|
+
company: (person && person.worksFor && person.worksFor.name) || null,
|
|
2094
|
+
url: canonical, source: 'linkedin',
|
|
2095
|
+
});
|
|
2096
|
+
} else if (pageClass === 'company') {
|
|
2097
|
+
await waitFor(() => document.querySelector('main h1, main h2, h1, h2'), 9000);
|
|
2098
|
+
const org = jsonLdAll().find((x) => /Organization|Corporation|LocalBusiness/i.test(String(x['@type'])));
|
|
2099
|
+
const mainLines = ((document.querySelector('main') || document.body).innerText || '')
|
|
2100
|
+
.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
2101
|
+
const rawTitle = (document.title.split('|')[0] || '').trim();
|
|
2102
|
+
const titleName = rawTitle.replace(/^\(\d+\)\s*/, '').replace(/:\s*Overview\s*$/, '').trim() || null;
|
|
2103
|
+
// 概览区:industry 是 meta 行的相邻前置 DIV;meta 行内 hq/followers/employees 为分离兄弟元素
|
|
2104
|
+
let industry = (org && org.industry) || null;
|
|
2105
|
+
let headquarters = null;
|
|
2106
|
+
let companySize = null;
|
|
2107
|
+
let followers = null;
|
|
2108
|
+
const folEl = Array.from(document.querySelectorAll('main *'))
|
|
2109
|
+
.find((x) => /followers/i.test(x.textContent || '') && x.children.length === 0);
|
|
2110
|
+
if (folEl) {
|
|
2111
|
+
const row = folEl.parentElement;
|
|
2112
|
+
const cells = Array.from(row ? row.children : []).map((c) => (c.textContent || '').trim()).filter(Boolean);
|
|
2113
|
+
headquarters = cells.find((c) => /,/.test(c) && !/followers|employees/i.test(c)) || null;
|
|
2114
|
+
const sizeC = cells.find((c) => /employees/i.test(c));
|
|
2115
|
+
if (sizeC) companySize = sizeC.replace(/\s*employees.*/i, '').trim();
|
|
2116
|
+
followers = (folEl.textContent || '').replace(/[\s,]*followers.*/i, '').trim();
|
|
2117
|
+
const outer = row ? row.parentElement : null;
|
|
2118
|
+
const prev = outer ? outer.previousElementSibling : null;
|
|
2119
|
+
const ind = prev ? (prev.textContent || '').trim() : '';
|
|
2120
|
+
if (ind && ind.length < 60 && !/follow|learn|sign|connect|notifications/i.test(ind)) industry = ind;
|
|
2121
|
+
}
|
|
2122
|
+
// 兜底:innerText 概览行(游客态/结构变化) "Software Development Redmond, Washington 29M followers 10K+ employees"
|
|
2123
|
+
if (!headquarters && !industry) {
|
|
2124
|
+
const overviewLine = mainLines.slice(0, 4).find((l) => /followers/i.test(l) || /employees/i.test(l));
|
|
2125
|
+
if (overviewLine) {
|
|
2126
|
+
const sizeM = overviewLine.match(/([\d.,]+[KMB]?\+?)\s*employees/i);
|
|
2127
|
+
const folM = overviewLine.match(/([\d.,]+[KMB]?)\s*followers/i);
|
|
2128
|
+
if (sizeM) companySize = companySize || sizeM[1];
|
|
2129
|
+
if (folM) followers = followers || folM[1];
|
|
2130
|
+
let rest = overviewLine;
|
|
2131
|
+
if (sizeM) rest = rest.replace(sizeM[0], ' ');
|
|
2132
|
+
if (folM) rest = rest.replace(folM[0], ' ');
|
|
2133
|
+
rest = rest.replace(/\s+/g, ' ').trim();
|
|
2134
|
+
const lastComma = rest.lastIndexOf(',');
|
|
2135
|
+
if (lastComma > 0) {
|
|
2136
|
+
const state = rest.slice(lastComma + 1).trim();
|
|
2137
|
+
const bw = rest.slice(0, lastComma).trim().split(/\s+/);
|
|
2138
|
+
const city = bw.slice(-2).join(' '); // 常见双词城市("Mountain View");DOM 主路径不受此限制
|
|
2139
|
+
headquarters = city + ', ' + state;
|
|
2140
|
+
industry = bw.slice(0, -2).join(' ') || null;
|
|
2141
|
+
} else if (rest) {
|
|
2142
|
+
industry = rest;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const name = text('h1') || (org && org.name) || mainLines[0] || titleName || meta('meta[property="og:title"]');
|
|
2147
|
+
const description = meta('meta[name="description"]') || meta('meta[property="og:description"]') || (org && org.description) || null;
|
|
2148
|
+
entities.push({
|
|
2149
|
+
entityType: 'company', name: name || null,
|
|
2150
|
+
description: description ? description.slice(0, 500) : null,
|
|
2151
|
+
industry: industry || null,
|
|
2152
|
+
companySize: companySize || null,
|
|
2153
|
+
headquarters: headquarters || null,
|
|
2154
|
+
followers: followers || null,
|
|
2155
|
+
url: canonical, source: 'linkedin',
|
|
2156
|
+
});
|
|
2157
|
+
} else if (pageClass === 'job') {
|
|
2158
|
+
await waitFor(() => document.querySelector('h1, .top-card-layout__title, [class*="unified-top-card__job-title"]'), 10000);
|
|
2159
|
+
const job = jsonLdAll().find((x) => x['@type'] === 'JobPosting');
|
|
2160
|
+
const title = (job && job.title) || text('.top-card-layout__title') || text('h1');
|
|
2161
|
+
const company = (job && job.hiringOrganization && job.hiringOrganization.name)
|
|
2162
|
+
|| text('.job-details-jobs-unified-top-card__company-name')
|
|
2163
|
+
|| text('a[data-test-app-aware-link]');
|
|
2164
|
+
let location = null;
|
|
2165
|
+
if (job && job.jobLocation) {
|
|
2166
|
+
const loc = Array.isArray(job.jobLocation) ? job.jobLocation[0] : job.jobLocation;
|
|
2167
|
+
location = loc && loc.address && loc.address.addressLocality;
|
|
2168
|
+
}
|
|
2169
|
+
if (!location) location = text('.job-details-jobs-unified-top-card__bullet') || text('.top-card-layout__bullet');
|
|
2170
|
+
const description = (job && job.description)
|
|
2171
|
+
? strip(job.description).slice(0, 2000)
|
|
2172
|
+
: (text('.show-more-less-html__markup') || '').slice(0, 2000) || null;
|
|
2173
|
+
entities.push({
|
|
2174
|
+
entityType: 'job', title: title || null, company: company || null,
|
|
2175
|
+
location: location || null, description: description || null,
|
|
2176
|
+
datePosted: (job && job.datePosted) || null,
|
|
2177
|
+
employmentType: (job && job.employmentType) || null,
|
|
2178
|
+
url: canonical, source: 'linkedin',
|
|
2179
|
+
});
|
|
2180
|
+
} else if (pageClass === 'list') {
|
|
2181
|
+
const seen = new Set();
|
|
2182
|
+
for (const a of Array.from(document.querySelectorAll('a[href]'))) {
|
|
2183
|
+
const href = a.href || '';
|
|
2184
|
+
let m = href.match(/linkedin\.com\/in\/([^/?#]+)/);
|
|
2185
|
+
if (m && !seen.has('t:' + m[1])) {
|
|
2186
|
+
seen.add('t:' + m[1]);
|
|
2187
|
+
links.talent.push({ url: 'https://www.linkedin.com/in/' + m[1] + '/', name: (a.textContent || '').trim().split('\n')[0] || m[1] });
|
|
2188
|
+
}
|
|
2189
|
+
m = href.match(/linkedin\.com\/company\/([^/?#]+)/);
|
|
2190
|
+
if (m && !seen.has('c:' + m[1])) {
|
|
2191
|
+
seen.add('c:' + m[1]);
|
|
2192
|
+
links.company.push({ url: 'https://www.linkedin.com/company/' + m[1] + '/', name: (a.textContent || '').trim().split('\n')[0] || m[1] });
|
|
2193
|
+
}
|
|
2194
|
+
m = href.match(/linkedin\.com\/jobs\/view\/(\d+)/);
|
|
2195
|
+
if (m && !seen.has('j:' + m[1])) {
|
|
2196
|
+
seen.add('j:' + m[1]);
|
|
2197
|
+
links.job.push({ url: 'https://www.linkedin.com/jobs/view/' + m[1] + '/', title: (a.textContent || '').trim().split('\n')[0] || m[1] });
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
links.talent = links.talent.slice(0, 10);
|
|
2201
|
+
links.company = links.company.slice(0, 10);
|
|
2202
|
+
links.job = links.job.slice(0, 10);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
return {
|
|
2206
|
+
pageClass: pageClass,
|
|
2207
|
+
url: location.href,
|
|
2208
|
+
canonical: canonical,
|
|
2209
|
+
title: document.title,
|
|
2210
|
+
entities: entities,
|
|
2211
|
+
links: links,
|
|
2212
|
+
source: 'linkedin',
|
|
2213
|
+
extractedAt: new Date().toISOString(),
|
|
2214
|
+
};
|
|
2215
|
+
})()`;
|
|
2216
|
+
var HIREDCHINA_EXTRACT_SOURCE = String.raw`(async () => {
|
|
2217
|
+
const waitFor = (fn, timeout, interval) => new Promise((resolve) => {
|
|
2218
|
+
const t0 = Date.now();
|
|
2219
|
+
const tick = () => {
|
|
2220
|
+
let v = null;
|
|
2221
|
+
try { v = fn(); } catch (e) {}
|
|
2222
|
+
if (v) return resolve(v);
|
|
2223
|
+
if (Date.now() - t0 > (timeout || 8000)) return resolve(null);
|
|
2224
|
+
setTimeout(tick, interval || 250);
|
|
2225
|
+
};
|
|
2226
|
+
tick();
|
|
2227
|
+
});
|
|
2228
|
+
if (document.readyState !== 'complete') {
|
|
2229
|
+
await waitFor(() => document.readyState === 'complete', 12000, 200);
|
|
2230
|
+
}
|
|
2231
|
+
const path = location.pathname;
|
|
2232
|
+
const jsonLdAll = () => Array.from(document.querySelectorAll('script[type="application/ld+json"]'))
|
|
2233
|
+
.map((s) => { try { return JSON.parse(s.textContent); } catch (e) { return null; } })
|
|
2234
|
+
.filter(Boolean)
|
|
2235
|
+
.flatMap((x) => (Array.isArray(x) ? x : [x]));
|
|
2236
|
+
|
|
2237
|
+
// i18n key 归一化:support.nationalitie.united.states.of.america → United States Of America
|
|
2238
|
+
const normalizeLocality = (s) => {
|
|
2239
|
+
if (!s || typeof s !== 'string') return null;
|
|
2240
|
+
if (/\s/.test(s)) return s; // 已是可读地名(含空格)
|
|
2241
|
+
if (!/^[a-z0-9._-]+$/i.test(s)) return s;
|
|
2242
|
+
// 剥离 i18n 命名空间前缀(如 support.nationalitie.),保留完整国名段
|
|
2243
|
+
const stripped = s.replace(/^support\.nationalitie\./i, '');
|
|
2244
|
+
return stripped.replace(/[._-]+/g, ' ')
|
|
2245
|
+
.split(' ')
|
|
2246
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
2247
|
+
.join(' ');
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
let pageClass = 'unknown';
|
|
2251
|
+
if (/^\/[a-z]{2}(-[A-Z]{2})?\/job\/[^/]+/.test(path)) pageClass = 'job';
|
|
2252
|
+
else if (/^\/([a-z]{2}(-[A-Z]{2})?\/)?talents\/?$/.test(path)) pageClass = 'talents';
|
|
2253
|
+
else pageClass = 'list';
|
|
2254
|
+
|
|
2255
|
+
const canonical = (document.querySelector('link[rel="canonical"]') || {}).href || location.href.split('?')[0];
|
|
2256
|
+
const entities = [];
|
|
2257
|
+
const links = { talent: [], company: [], job: [] };
|
|
2258
|
+
|
|
2259
|
+
if (pageClass === 'job') {
|
|
2260
|
+
await waitFor(() => document.querySelector('script[type="application/ld+json"], h1'), 9000);
|
|
2261
|
+
const job = jsonLdAll().find((x) => x['@type'] === 'JobPosting');
|
|
2262
|
+
const org = job && job.hiringOrganization ? job.hiringOrganization : null;
|
|
2263
|
+
const salary = job && job.baseSalary ? job.baseSalary : null;
|
|
2264
|
+
const salaryValue = salary && salary.value ? salary.value : {};
|
|
2265
|
+
const hasSalary = salary && (salaryValue.minValue > 0 || salaryValue.maxValue > 0);
|
|
2266
|
+
let location = null;
|
|
2267
|
+
if (job && job.jobLocation) {
|
|
2268
|
+
const loc = Array.isArray(job.jobLocation) ? job.jobLocation[0] : job.jobLocation;
|
|
2269
|
+
location = loc && loc.address ? normalizeLocality(loc.address.addressLocality) : null;
|
|
2270
|
+
}
|
|
2271
|
+
entities.push({
|
|
2272
|
+
entityType: 'job',
|
|
2273
|
+
title: (job && job.title) || (document.querySelector('h1') || {}).textContent || null,
|
|
2274
|
+
company: (org && org.name) || null,
|
|
2275
|
+
location: location,
|
|
2276
|
+
description: job && job.description ? String(job.description).slice(0, 2000) : null,
|
|
2277
|
+
employmentType: (job && job.employmentType) || null,
|
|
2278
|
+
datePosted: (job && job.datePosted) || null,
|
|
2279
|
+
salaryCurrency: hasSalary ? (salary.currency || null) : null,
|
|
2280
|
+
salaryMin: hasSalary ? (salaryValue.minValue || null) : null,
|
|
2281
|
+
salaryMax: hasSalary ? (salaryValue.maxValue || null) : null,
|
|
2282
|
+
salaryUnit: hasSalary ? (salaryValue.unitText || null) : null,
|
|
2283
|
+
url: canonical, source: 'hiredchina',
|
|
2284
|
+
});
|
|
2285
|
+
if (org && org.name) {
|
|
2286
|
+
entities.push({
|
|
2287
|
+
entityType: 'company',
|
|
2288
|
+
name: org.name,
|
|
2289
|
+
logo: org.logo || null,
|
|
2290
|
+
url: canonical, source: 'hiredchina',
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
} else if (pageClass === 'talents') {
|
|
2294
|
+
// 卡片右列锚点:class 含 col-span-5 且文本含"工作经历";父级即卡片根(左右两列 grid)
|
|
2295
|
+
const rightCols = Array.from(document.querySelectorAll('div[class*="col-span-5"]'))
|
|
2296
|
+
.filter((el) => /工作经历|Work Experience/i.test(el.textContent || ''));
|
|
2297
|
+
const fallbackCols = rightCols.length
|
|
2298
|
+
? rightCols
|
|
2299
|
+
: Array.from(document.querySelectorAll('div'))
|
|
2300
|
+
.filter((el) => {
|
|
2301
|
+
const t = el.textContent || '';
|
|
2302
|
+
return /工作经历/.test(t) && t.length < 3000
|
|
2303
|
+
&& Array.from(el.querySelectorAll('span')).some((s) => (s.textContent || '').trim() === '工作经历')
|
|
2304
|
+
&& el.querySelectorAll('div[class*="mb-1"]').length >= 2;
|
|
2305
|
+
});
|
|
2306
|
+
const cards = (fallbackCols.length ? fallbackCols : rightCols)
|
|
2307
|
+
.map((rc) => rc.parentElement)
|
|
2308
|
+
.filter(Boolean);
|
|
2309
|
+
for (const card of cards) {
|
|
2310
|
+
const lines = (card.innerText || '').split('\n').map((s) => s.trim()).filter(Boolean);
|
|
2311
|
+
const name = lines[0] || null;
|
|
2312
|
+
if (!name || name.length > 60) continue;
|
|
2313
|
+
const gaIdx = lines.findIndex((l) => /^[男女] · \d+$/.test(l));
|
|
2314
|
+
const ga = gaIdx >= 0 ? lines[gaIdx].split(' · ') : [];
|
|
2315
|
+
const at = (i) => (gaIdx >= 0 && gaIdx + i < lines.length ? lines[gaIdx + i] : null);
|
|
2316
|
+
const introIdx = lines.findIndex((l) => l === '💬');
|
|
2317
|
+
const intro = introIdx >= 0 && lines[introIdx + 1] && lines[introIdx + 1].length < 300 ? lines[introIdx + 1] : null;
|
|
2318
|
+
const cnIdx = lines.findIndex((l) => l.startsWith('中文水平'));
|
|
2319
|
+
const chineseLevel = cnIdx >= 0 ? lines[cnIdx].replace('中文水平: ', '').replace('中文水平:', '') : null;
|
|
2320
|
+
const workIdx = lines.indexOf('工作经历');
|
|
2321
|
+
const eduIdx = lines.indexOf('教育经历');
|
|
2322
|
+
const workLines = workIdx >= 0 ? lines.slice(workIdx + 1, eduIdx > workIdx ? eduIdx : lines.length) : [];
|
|
2323
|
+
const works = [];
|
|
2324
|
+
for (let i = 0; i + 1 < workLines.length; i += 2) {
|
|
2325
|
+
if (/^\d{4}\.\d{2}/.test(workLines[i + 1] || '')) {
|
|
2326
|
+
works.push({ title: workLines[i], period: workLines[i + 1], company: workLines[i + 2] || null });
|
|
2327
|
+
i += 1;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
const eduLines = eduIdx >= 0 ? lines.slice(eduIdx + 1) : [];
|
|
2331
|
+
const educations = [];
|
|
2332
|
+
for (let i = 0; i + 1 < eduLines.length; i += 2) {
|
|
2333
|
+
if (/^\d{4}$/.test(eduLines[i + 1] || '')) {
|
|
2334
|
+
educations.push({ school: eduLines[i], year: eduLines[i + 1], degree: eduLines[i + 2] || null });
|
|
2335
|
+
i += 1;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
entities.push({
|
|
2339
|
+
entityType: 'talent',
|
|
2340
|
+
name: name,
|
|
2341
|
+
gender: ga[0] || null,
|
|
2342
|
+
age: ga[1] || null,
|
|
2343
|
+
nationality: at(1),
|
|
2344
|
+
location: at(2),
|
|
2345
|
+
degree: at(3),
|
|
2346
|
+
headline: at(4),
|
|
2347
|
+
intro: intro,
|
|
2348
|
+
chineseLevel: chineseLevel,
|
|
2349
|
+
works: works.slice(0, 5),
|
|
2350
|
+
educations: educations.slice(0, 3),
|
|
2351
|
+
url: canonical + '#talent=' + encodeURIComponent(name) + '|' + encodeURIComponent(at(1) || ''),
|
|
2352
|
+
source: 'hiredchina',
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
} else {
|
|
2356
|
+
// list:提取 /<lang>/job/ 链接
|
|
2357
|
+
const seen = new Set();
|
|
2358
|
+
for (const a of Array.from(document.querySelectorAll('a[href]'))) {
|
|
2359
|
+
const href = a.href || '';
|
|
2360
|
+
const m = href.match(/hiredchina\.com\/[a-z]{2}(-[A-Z]{2})?\/job\/([^/?#]+)/);
|
|
2361
|
+
if (m && !seen.has(m[2])) {
|
|
2362
|
+
seen.add(m[2]);
|
|
2363
|
+
links.job.push({ url: href.split('?')[0], title: (a.textContent || '').trim().split('\n')[0] || m[2] });
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
links.job = links.job.slice(0, 10);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
return {
|
|
2370
|
+
pageClass: pageClass,
|
|
2371
|
+
url: location.href,
|
|
2372
|
+
canonical: canonical,
|
|
2373
|
+
title: document.title,
|
|
2374
|
+
entities: entities,
|
|
2375
|
+
links: links,
|
|
2376
|
+
source: 'hiredchina',
|
|
2377
|
+
extractedAt: new Date().toISOString(),
|
|
2378
|
+
};
|
|
2379
|
+
})()`;
|
|
2380
|
+
function err2(message, code) {
|
|
2381
|
+
return { status: "error", code, message };
|
|
2382
|
+
}
|
|
2383
|
+
async function handleCookiesCommand(args) {
|
|
2384
|
+
const action = typeof args.action === "string" ? args.action : "get";
|
|
2385
|
+
const domain = typeof args.domain === "string" ? args.domain : void 0;
|
|
2386
|
+
const url = typeof args.url === "string" ? args.url : void 0;
|
|
2387
|
+
const name = typeof args.name === "string" ? args.name : void 0;
|
|
2388
|
+
if (action === "get") {
|
|
2389
|
+
if (!domain && !url) {
|
|
2390
|
+
throw new Error("Cookie scope required: provide domain or url to avoid dumping all cookies");
|
|
2391
|
+
}
|
|
2392
|
+
const details = {};
|
|
2393
|
+
if (domain) details.domain = domain;
|
|
2394
|
+
if (url) details.url = url;
|
|
2395
|
+
if (name) details.name = name;
|
|
2396
|
+
const cookies = await chrome.cookies.getAll(details);
|
|
2397
|
+
return {
|
|
2398
|
+
action: "get",
|
|
2399
|
+
count: cookies.length,
|
|
2400
|
+
cookies: cookies.map((c) => ({
|
|
2401
|
+
name: c.name,
|
|
2402
|
+
value: c.value,
|
|
2403
|
+
domain: c.domain,
|
|
2404
|
+
path: c.path,
|
|
2405
|
+
secure: c.secure,
|
|
2406
|
+
httpOnly: c.httpOnly,
|
|
2407
|
+
sameSite: c.sameSite,
|
|
2408
|
+
expirationDate: c.expirationDate
|
|
2409
|
+
}))
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
if (action === "set") {
|
|
2413
|
+
if (!name || typeof args.value !== "string" || !domain && !url) {
|
|
2414
|
+
throw new Error("cp.cookies set requires name, value and (domain or url)");
|
|
2415
|
+
}
|
|
2416
|
+
const details = {
|
|
2417
|
+
name,
|
|
2418
|
+
value: args.value,
|
|
2419
|
+
url: url ?? `https://${(domain ?? "").replace(/^\./, "")}/`
|
|
2420
|
+
};
|
|
2421
|
+
if (domain) details.domain = domain;
|
|
2422
|
+
if (typeof args.path === "string") details.path = args.path;
|
|
2423
|
+
if (args.secure === true) details.secure = true;
|
|
2424
|
+
if (args.httpOnly === true) details.httpOnly = true;
|
|
2425
|
+
if (typeof args.expirationDate === "number") details.expirationDate = args.expirationDate;
|
|
2426
|
+
const cookie = await chrome.cookies.set(details);
|
|
2427
|
+
return { action: "set", cookie };
|
|
2428
|
+
}
|
|
2429
|
+
if (action === "delete") {
|
|
2430
|
+
if (!name || !domain && !url) {
|
|
2431
|
+
throw new Error("cp.cookies delete requires name and (domain or url)");
|
|
2432
|
+
}
|
|
2433
|
+
const targetUrl = url ?? `https://${(domain ?? "").replace(/^\./, "")}/`;
|
|
2434
|
+
const deleted = await chrome.cookies.remove({ name, url: targetUrl });
|
|
2435
|
+
return { action: "delete", deleted };
|
|
2436
|
+
}
|
|
2437
|
+
throw new Error(`cookies_unknown_action: ${action}`);
|
|
2438
|
+
}
|
|
2439
|
+
async function handleTabCommand(command, args, log) {
|
|
2440
|
+
const tabArg = args.tab ?? args.targetId ?? args.page;
|
|
2441
|
+
switch (command) {
|
|
2442
|
+
case "cp.tab.list": {
|
|
2443
|
+
const tabs = await chrome.tabs.query({});
|
|
2444
|
+
return tabs.map((t, index) => ({
|
|
2445
|
+
index,
|
|
2446
|
+
page: String(t.id),
|
|
2447
|
+
url: t.url ?? "",
|
|
2448
|
+
title: t.title ?? "",
|
|
2449
|
+
active: t.active
|
|
2450
|
+
}));
|
|
2451
|
+
}
|
|
2452
|
+
case "cp.tab.new": {
|
|
2453
|
+
const url = typeof args.url === "string" && args.url ? args.url : "about:blank";
|
|
2454
|
+
const active = args.active === true;
|
|
2455
|
+
log("debug", "cp.tab.new creating tab", { url, active });
|
|
2456
|
+
const tab = await chrome.tabs.create({ url, active });
|
|
2457
|
+
log("info", "cp.tab.new created", { page: String(tab.id), url: tab.url ?? url, active });
|
|
2458
|
+
return { page: String(tab.id), url: tab.url ?? url };
|
|
2459
|
+
}
|
|
2460
|
+
case "cp.tab.select": {
|
|
2461
|
+
if (!tabArg) throw new Error("tab_select_requires_tab_id");
|
|
2462
|
+
const tabId = Number(tabArg);
|
|
2463
|
+
log("debug", "cp.tab.select activating tab", { tabId });
|
|
2464
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
2465
|
+
log("info", "cp.tab.select activated", { tabId });
|
|
2466
|
+
return { selected: true, page: String(tabArg) };
|
|
2467
|
+
}
|
|
2468
|
+
case "cp.tab.close": {
|
|
2469
|
+
if (!tabArg) throw new Error("tab_close_requires_tab_id");
|
|
2470
|
+
await chrome.tabs.remove(Number(tabArg));
|
|
2471
|
+
return { closed: true, page: String(tabArg) };
|
|
2472
|
+
}
|
|
2473
|
+
case "cp.tab.bind": {
|
|
2474
|
+
const t = tabArg ? await getControlTab(tabArg) : await getActiveTab();
|
|
2475
|
+
if (!t?.id) throw new Error("no_tab_to_bind");
|
|
2476
|
+
boundTabId = t.id;
|
|
2477
|
+
return { bound: true, page: String(t.id), url: t.url, title: t.title };
|
|
2478
|
+
}
|
|
2479
|
+
case "cp.tab.unbind": {
|
|
2480
|
+
const had = boundTabId;
|
|
2481
|
+
boundTabId = null;
|
|
2482
|
+
return { unbound: true, had: had != null ? String(had) : null };
|
|
2483
|
+
}
|
|
2484
|
+
default:
|
|
2485
|
+
throw new Error(`unknown_command: ${command}`);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// src/shared/etld.ts
|
|
2490
|
+
var MULTI_PART_SUFFIXES = /* @__PURE__ */ new Set([
|
|
2491
|
+
"com.cn",
|
|
2492
|
+
"net.cn",
|
|
2493
|
+
"org.cn",
|
|
2494
|
+
"gov.cn",
|
|
2495
|
+
"edu.cn",
|
|
2496
|
+
"com.hk",
|
|
2497
|
+
"com.tw",
|
|
2498
|
+
"com.sg",
|
|
2499
|
+
"com.my",
|
|
2500
|
+
"com.au",
|
|
2501
|
+
"co.nz",
|
|
2502
|
+
"co.jp",
|
|
2503
|
+
"co.kr",
|
|
2504
|
+
"co.in",
|
|
2505
|
+
"co.uk"
|
|
2506
|
+
]);
|
|
2507
|
+
function etldPlusOne(hostname) {
|
|
2508
|
+
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
2509
|
+
const parts = host.split(".");
|
|
2510
|
+
if (parts.length <= 2) return host;
|
|
2511
|
+
const lastTwo = parts.slice(-2).join(".");
|
|
2512
|
+
if (MULTI_PART_SUFFIXES.has(lastTwo)) return parts.slice(-3).join(".");
|
|
2513
|
+
return lastTwo;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// src/background/sites.ts
|
|
2517
|
+
var DYNAMIC_SCRIPT_ID = "hm-auto-capture";
|
|
2518
|
+
var RECRUITING_HINTS = [
|
|
2519
|
+
"jobs",
|
|
2520
|
+
"careers",
|
|
2521
|
+
"hiring",
|
|
2522
|
+
"recruit",
|
|
2523
|
+
"vacancy",
|
|
2524
|
+
"vacancies",
|
|
2525
|
+
"openings",
|
|
2526
|
+
"\u804C\u4F4D",
|
|
2527
|
+
"\u62DB\u8058",
|
|
2528
|
+
"\u4EBA\u624D",
|
|
2529
|
+
"\u730E\u5934",
|
|
2530
|
+
"\u7B80\u5386",
|
|
2531
|
+
"\u6C42\u804C",
|
|
2532
|
+
"talent",
|
|
2533
|
+
"headhunter",
|
|
2534
|
+
"linkedin",
|
|
2535
|
+
"zhipin",
|
|
2536
|
+
"liepin",
|
|
2537
|
+
"51job",
|
|
2538
|
+
"glassdoor",
|
|
2539
|
+
"indeed",
|
|
2540
|
+
"xing",
|
|
2541
|
+
"stepstone"
|
|
2542
|
+
];
|
|
2543
|
+
var PATH_CLASS_HINTS = [
|
|
2544
|
+
{ re: /\/(in|profile|pub|resume|cv)\b/i, cls: "talent" },
|
|
2545
|
+
{ re: /\/(jobs|job|vacancies|positions|careers)\b/i, cls: "job" },
|
|
2546
|
+
{ re: /\/(company|companies|employer)\b/i, cls: "company" },
|
|
2547
|
+
{ re: /\/(search|talent|people|directory)\b/i, cls: "list" }
|
|
2548
|
+
];
|
|
2549
|
+
var enabledDomains = /* @__PURE__ */ new Set();
|
|
2550
|
+
async function syncSites() {
|
|
2551
|
+
let entries = [];
|
|
2552
|
+
try {
|
|
2553
|
+
const res = await fetch(`${SERVER_HTTP_URL}/sites`);
|
|
2554
|
+
if (res.ok) {
|
|
2555
|
+
const data = await res.json();
|
|
2556
|
+
entries = data.sites || data || [];
|
|
2557
|
+
}
|
|
2558
|
+
} catch {
|
|
2559
|
+
return [];
|
|
2560
|
+
}
|
|
2561
|
+
const next = new Set(entries.filter((e) => e.enabled).map((e) => e.domain));
|
|
2562
|
+
const changed = symmetricDiffers(enabledDomains, next);
|
|
2563
|
+
enabledDomains = next;
|
|
2564
|
+
if (changed) await registerDynamicScripts([...next]);
|
|
2565
|
+
return entries;
|
|
2566
|
+
}
|
|
2567
|
+
async function registerDynamicScripts(domains) {
|
|
2568
|
+
try {
|
|
2569
|
+
await chrome.scripting.unregisterContentScripts({ ids: [DYNAMIC_SCRIPT_ID] });
|
|
2570
|
+
} catch {
|
|
2571
|
+
}
|
|
2572
|
+
if (!domains.length) return;
|
|
2573
|
+
const matches = domains.map((d) => `https://*.${d}/*`);
|
|
2574
|
+
await chrome.scripting.registerContentScripts([
|
|
2575
|
+
{
|
|
2576
|
+
id: DYNAMIC_SCRIPT_ID,
|
|
2577
|
+
matches,
|
|
2578
|
+
js: ["content/index.js"],
|
|
2579
|
+
runAt: "document_idle",
|
|
2580
|
+
persistAcrossSessions: true
|
|
2581
|
+
}
|
|
2582
|
+
]);
|
|
2583
|
+
console.log(`[HunterMate] dynamic content scripts registered for ${domains.length} domains`);
|
|
2584
|
+
}
|
|
2585
|
+
function symmetricDiffers(a, b) {
|
|
2586
|
+
if (a.size !== b.size) return true;
|
|
2587
|
+
for (const v of a) if (!b.has(v)) return true;
|
|
2588
|
+
return false;
|
|
2589
|
+
}
|
|
2590
|
+
function isProductionDomain(hostname) {
|
|
2591
|
+
const h = hostname.toLowerCase().trim();
|
|
2592
|
+
if (!h) return false;
|
|
2593
|
+
if (h === "localhost" || h === "127.0.0.1" || h === "[::1]" || h === "::1") return false;
|
|
2594
|
+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return false;
|
|
2595
|
+
if (/^\[?[0-9a-f:]+\]?$/.test(h)) return false;
|
|
2596
|
+
if (!h.includes(".") || h.startsWith(".") || h.endsWith(".")) return false;
|
|
2597
|
+
return true;
|
|
2598
|
+
}
|
|
2599
|
+
var discovered = /* @__PURE__ */ new Set();
|
|
2600
|
+
function watchForDiscovery() {
|
|
2601
|
+
chrome.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
|
|
2602
|
+
if (changeInfo.status !== "complete" || !tab.url) return;
|
|
2603
|
+
maybeDiscover(tab.url, changeInfo.title ?? tab.title ?? "");
|
|
2604
|
+
});
|
|
2605
|
+
chrome.tabs.onActivated.addListener(async (activeInfo) => {
|
|
2606
|
+
try {
|
|
2607
|
+
const tab = await chrome.tabs.get(activeInfo.tabId);
|
|
2608
|
+
if (tab.url) maybeDiscover(tab.url, tab.title ?? "");
|
|
2609
|
+
} catch {
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
function maybeDiscover(url, title) {
|
|
2614
|
+
let hostname;
|
|
2615
|
+
try {
|
|
2616
|
+
const u = new URL(url);
|
|
2617
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return;
|
|
2618
|
+
hostname = u.hostname;
|
|
2619
|
+
} catch {
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
if (!isProductionDomain(hostname)) return;
|
|
2623
|
+
const domain = etldPlusOne(hostname);
|
|
2624
|
+
if (discovered.has(domain) || enabledDomains.has(domain)) return;
|
|
2625
|
+
const haystack = `${title} ${url}`.toLowerCase();
|
|
2626
|
+
const hit = RECRUITING_HINTS.find((k) => haystack.includes(k));
|
|
2627
|
+
if (!hit) return;
|
|
2628
|
+
const pathHint = PATH_CLASS_HINTS.find(({ re }) => re.test(new URL(url).pathname));
|
|
2629
|
+
const payload = {
|
|
2630
|
+
domain,
|
|
2631
|
+
url,
|
|
2632
|
+
title,
|
|
2633
|
+
evidence: `keyword:${hit}`,
|
|
2634
|
+
pageClass: pathHint?.cls ?? "unknown"
|
|
2635
|
+
};
|
|
2636
|
+
discovered.add(domain);
|
|
2637
|
+
fetch(`${SERVER_HTTP_URL}/sites/discover`, {
|
|
2638
|
+
method: "POST",
|
|
2639
|
+
headers: { "Content-Type": "application/json" },
|
|
2640
|
+
body: JSON.stringify(payload)
|
|
2641
|
+
}).catch(() => {
|
|
2642
|
+
discovered.delete(domain);
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// ../../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
|
|
2647
|
+
function stringifyArg(arg) {
|
|
2648
|
+
if (typeof arg === "string")
|
|
2649
|
+
return arg;
|
|
2650
|
+
try {
|
|
2651
|
+
return JSON.stringify(arg);
|
|
2652
|
+
} catch {
|
|
2653
|
+
return String(arg);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
var Logger = class {
|
|
2657
|
+
opts;
|
|
2658
|
+
buffer = [];
|
|
2659
|
+
timer = null;
|
|
2660
|
+
originalConsole = {};
|
|
2661
|
+
maxBufferSize;
|
|
2662
|
+
flushIntervalMs;
|
|
2663
|
+
constructor(opts) {
|
|
2664
|
+
this.opts = opts;
|
|
2665
|
+
this.maxBufferSize = opts.maxBufferSize ?? 50;
|
|
2666
|
+
this.flushIntervalMs = opts.flushIntervalMs ?? 1e3;
|
|
2667
|
+
}
|
|
2668
|
+
resolveSessionId() {
|
|
2669
|
+
const sid = typeof this.opts.sessionId === "function" ? this.opts.sessionId() : this.opts.sessionId;
|
|
2670
|
+
return sid || "none";
|
|
2671
|
+
}
|
|
2672
|
+
async resolveTabInfo() {
|
|
2673
|
+
try {
|
|
2674
|
+
return await this.opts.getTabInfo?.() ?? {};
|
|
2675
|
+
} catch {
|
|
2676
|
+
return {};
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
enqueue(level, message, meta) {
|
|
2680
|
+
this.resolveTabInfo().then((info) => {
|
|
2681
|
+
this.buffer.push({
|
|
2682
|
+
sessionId: this.resolveSessionId(),
|
|
2683
|
+
context: this.opts.context,
|
|
2684
|
+
level,
|
|
2685
|
+
message,
|
|
2686
|
+
meta: meta ?? null,
|
|
2687
|
+
tabId: info.tabId ?? null,
|
|
2688
|
+
url: info.url ?? null,
|
|
2689
|
+
timestamp: Date.now()
|
|
2690
|
+
});
|
|
2691
|
+
if (this.buffer.length >= this.maxBufferSize) {
|
|
2692
|
+
this.flush().catch(() => {
|
|
2693
|
+
});
|
|
2694
|
+
} else {
|
|
2695
|
+
this.scheduleFlush();
|
|
2696
|
+
}
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
scheduleFlush() {
|
|
2700
|
+
if (this.timer)
|
|
2701
|
+
return;
|
|
2702
|
+
this.timer = setTimeout(() => {
|
|
2703
|
+
this.timer = null;
|
|
2704
|
+
this.flush().catch(() => {
|
|
2705
|
+
});
|
|
2706
|
+
}, this.flushIntervalMs);
|
|
2707
|
+
}
|
|
2708
|
+
async flush() {
|
|
2709
|
+
if (this.timer) {
|
|
2710
|
+
clearTimeout(this.timer);
|
|
2711
|
+
this.timer = null;
|
|
2712
|
+
}
|
|
2713
|
+
if (!this.buffer.length)
|
|
2714
|
+
return;
|
|
2715
|
+
const batch = this.buffer.splice(0, this.buffer.length);
|
|
2716
|
+
try {
|
|
2717
|
+
await this.opts.send(batch);
|
|
2718
|
+
} catch {
|
|
2719
|
+
const queue = this.opts.persistentQueue;
|
|
2720
|
+
if (queue) {
|
|
2721
|
+
for (const entry of batch) {
|
|
2722
|
+
await queue.enqueue(entry).catch(() => {
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
} else {
|
|
2726
|
+
this.buffer.unshift(...batch);
|
|
2727
|
+
if (this.buffer.length > this.maxBufferSize * 2) {
|
|
2728
|
+
this.buffer.splice(0, this.buffer.length - this.maxBufferSize * 2);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* Drain persistent queue by calling send. Returns counts of sent/remaining.
|
|
2735
|
+
* Caller should invoke this when server is reachable again (e.g. on retry tick).
|
|
2736
|
+
*/
|
|
2737
|
+
async drainPersistent() {
|
|
2738
|
+
if (!this.opts.persistentQueue)
|
|
2739
|
+
return null;
|
|
2740
|
+
return this.opts.persistentQueue.drain(this.opts.send);
|
|
2741
|
+
}
|
|
2742
|
+
debug(message, meta) {
|
|
2743
|
+
this.enqueue("debug", message, meta);
|
|
2744
|
+
}
|
|
2745
|
+
info(message, meta) {
|
|
2746
|
+
this.enqueue("info", message, meta);
|
|
2747
|
+
}
|
|
2748
|
+
warn(message, meta) {
|
|
2749
|
+
this.enqueue("warn", message, meta);
|
|
2750
|
+
}
|
|
2751
|
+
error(message, meta) {
|
|
2752
|
+
this.enqueue("error", message, meta);
|
|
2753
|
+
}
|
|
2754
|
+
log(message, meta) {
|
|
2755
|
+
this.info(message, meta);
|
|
2756
|
+
}
|
|
2757
|
+
patchConsole() {
|
|
2758
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
2759
|
+
for (const level of levels) {
|
|
2760
|
+
const original = console[level];
|
|
2761
|
+
this.originalConsole[level] = original;
|
|
2762
|
+
console[level] = (...args) => {
|
|
2763
|
+
original.apply(console, args);
|
|
2764
|
+
try {
|
|
2765
|
+
const message = args.map(stringifyArg).join(" ");
|
|
2766
|
+
this[level](message);
|
|
2767
|
+
} catch {
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
const originalLog = console.log;
|
|
2772
|
+
this.originalConsole.log = originalLog;
|
|
2773
|
+
console.log = (...args) => {
|
|
2774
|
+
originalLog.apply(console, args);
|
|
2775
|
+
try {
|
|
2776
|
+
const message = args.map(stringifyArg).join(" ");
|
|
2777
|
+
this.info(message);
|
|
2778
|
+
} catch {
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
patchProcessStreams() {
|
|
2783
|
+
if (typeof process === "undefined")
|
|
2784
|
+
return;
|
|
2785
|
+
const wrap = (stream, label) => {
|
|
2786
|
+
const original = stream.write.bind(stream);
|
|
2787
|
+
stream.write = (chunk, encoding, cb) => {
|
|
2788
|
+
const str = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
|
|
2789
|
+
this.info(str.replace(/\n$/, ""), { stream: label });
|
|
2790
|
+
return original(chunk, encoding, cb);
|
|
2791
|
+
};
|
|
2792
|
+
};
|
|
2793
|
+
wrap(process.stdout, "stdout");
|
|
2794
|
+
wrap(process.stderr, "stderr");
|
|
2795
|
+
}
|
|
2796
|
+
};
|
|
2797
|
+
|
|
2798
|
+
// ../../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
|
|
2799
|
+
async function createHttpTransport(opts) {
|
|
2800
|
+
const serverUrl = opts.serverUrl.replace(/\/$/, "");
|
|
2801
|
+
return async (logs) => {
|
|
2802
|
+
if (!logs.length)
|
|
2803
|
+
return;
|
|
2804
|
+
const res = await fetch(`${serverUrl}/logs`, {
|
|
2805
|
+
method: "POST",
|
|
2806
|
+
headers: { "Content-Type": "application/json", ...opts.headers || {} },
|
|
2807
|
+
body: JSON.stringify({ logs })
|
|
2808
|
+
});
|
|
2809
|
+
if (!res.ok) {
|
|
2810
|
+
throw new Error(`HTTP ${res.status}: ${await res.text().catch(() => "")}`);
|
|
2811
|
+
}
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// src/shared/queue.ts
|
|
2816
|
+
var STORAGE_KEY2 = "hunter-mate:logs-pending";
|
|
2817
|
+
var ChromeStorageLogQueue = class {
|
|
2818
|
+
buffer = [];
|
|
2819
|
+
nextId = 1;
|
|
2820
|
+
maxBytes = 2 * 1024 * 1024;
|
|
2821
|
+
async load() {
|
|
2822
|
+
const stored = await chromeStorageLocalGet(STORAGE_KEY2);
|
|
2823
|
+
if (!stored) return;
|
|
2824
|
+
try {
|
|
2825
|
+
const parsed = JSON.parse(stored);
|
|
2826
|
+
this.buffer = Array.isArray(parsed) ? parsed : [];
|
|
2827
|
+
for (const line of this.buffer) {
|
|
2828
|
+
if (line.id >= this.nextId) this.nextId = line.id + 1;
|
|
2829
|
+
}
|
|
2830
|
+
} catch {
|
|
2831
|
+
this.buffer = [];
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
async enqueue(log) {
|
|
2835
|
+
this.buffer.push({ id: this.nextId++, log });
|
|
2836
|
+
await this.enforceCap();
|
|
2837
|
+
await this.persist();
|
|
2838
|
+
}
|
|
2839
|
+
async enforceCap() {
|
|
2840
|
+
const size = JSON.stringify(this.buffer).length;
|
|
2841
|
+
if (size <= this.maxBytes) return;
|
|
2842
|
+
const drop = Math.ceil(this.buffer.length * 0.1);
|
|
2843
|
+
this.buffer.splice(0, drop);
|
|
2844
|
+
}
|
|
2845
|
+
async persist() {
|
|
2846
|
+
try {
|
|
2847
|
+
await chromeStorageLocalSet(STORAGE_KEY2, JSON.stringify(this.buffer));
|
|
2848
|
+
} catch {
|
|
2849
|
+
this.buffer.splice(0, Math.ceil(this.buffer.length * 0.5));
|
|
2850
|
+
await chromeStorageLocalSet(STORAGE_KEY2, JSON.stringify(this.buffer));
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
size() {
|
|
2854
|
+
return this.buffer.length;
|
|
2855
|
+
}
|
|
2856
|
+
async drain(sendFn, batchSize = 100) {
|
|
2857
|
+
if (this.buffer.length === 0) return { sent: 0, remaining: 0 };
|
|
2858
|
+
let sent = 0;
|
|
2859
|
+
while (this.buffer.length > 0) {
|
|
2860
|
+
const batch = this.buffer.slice(0, batchSize);
|
|
2861
|
+
try {
|
|
2862
|
+
await sendFn(batch.map((q) => q.log));
|
|
2863
|
+
this.buffer.splice(0, batch.length);
|
|
2864
|
+
sent += batch.length;
|
|
2865
|
+
} catch {
|
|
2866
|
+
break;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
if (sent > 0) await this.persist();
|
|
2870
|
+
return { sent, remaining: this.buffer.length };
|
|
2871
|
+
}
|
|
2872
|
+
async clear() {
|
|
2873
|
+
this.buffer = [];
|
|
2874
|
+
this.nextId = 1;
|
|
2875
|
+
await chromeStorageLocalRemove(STORAGE_KEY2);
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
function chromeStorageLocalGet(key) {
|
|
2879
|
+
return new Promise((resolve) => {
|
|
2880
|
+
chrome.storage.local.get(key, (result) => {
|
|
2881
|
+
resolve(typeof result?.[key] === "string" ? result[key] : null);
|
|
2882
|
+
});
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
function chromeStorageLocalSet(key, value) {
|
|
2886
|
+
return new Promise((resolve, reject) => {
|
|
2887
|
+
chrome.storage.local.set({ [key]: value }, () => {
|
|
2888
|
+
if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message));
|
|
2889
|
+
else resolve();
|
|
2890
|
+
});
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
function chromeStorageLocalRemove(key) {
|
|
2894
|
+
return new Promise((resolve) => {
|
|
2895
|
+
chrome.storage.local.remove(key, () => resolve());
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// src/shared/logger.ts
|
|
2900
|
+
function createExtensionLogger(options) {
|
|
2901
|
+
const send2 = async (logs) => {
|
|
2902
|
+
const transport = await createHttpTransport({ serverUrl: SERVER_HTTP_URL });
|
|
2903
|
+
await transport(logs);
|
|
2904
|
+
};
|
|
2905
|
+
return new Logger({
|
|
2906
|
+
context: options.context,
|
|
2907
|
+
sessionId: options.sessionId,
|
|
2908
|
+
send: send2,
|
|
2909
|
+
flushIntervalMs: 1500,
|
|
2910
|
+
maxBufferSize: 30
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
async function createBackgroundLogForwarder(options) {
|
|
2914
|
+
const transport = options?.transport;
|
|
2915
|
+
const maxPending = options?.maxPending ?? 200;
|
|
2916
|
+
const intervalMs = options?.intervalMs ?? 3e3;
|
|
2917
|
+
const queue = options?.persistentQueue ?? new ChromeStorageLogQueue();
|
|
2918
|
+
await queue.load();
|
|
2919
|
+
const pending = [];
|
|
2920
|
+
async function flush() {
|
|
2921
|
+
if (!transport) return;
|
|
2922
|
+
while (pending.length > 0) {
|
|
2923
|
+
const batch = pending.splice(0, pending.length);
|
|
2924
|
+
try {
|
|
2925
|
+
await transport(batch);
|
|
2926
|
+
} catch {
|
|
2927
|
+
for (const entry of batch) {
|
|
2928
|
+
await queue.enqueue(entry).catch(() => {
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
break;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
async function drainPersistent() {
|
|
2936
|
+
if (!transport) return null;
|
|
2937
|
+
return queue.drain(transport);
|
|
2938
|
+
}
|
|
2939
|
+
if (transport) {
|
|
2940
|
+
setInterval(() => {
|
|
2941
|
+
flush().catch(() => {
|
|
2942
|
+
});
|
|
2943
|
+
drainPersistent().catch(() => {
|
|
2944
|
+
});
|
|
2945
|
+
}, intervalMs);
|
|
2946
|
+
}
|
|
2947
|
+
function onLogsMessage(logs) {
|
|
2948
|
+
if (!transport) return;
|
|
2949
|
+
pending.push(...logs);
|
|
2950
|
+
if (pending.length > maxPending) {
|
|
2951
|
+
pending.splice(0, pending.length - maxPending);
|
|
2952
|
+
}
|
|
2953
|
+
flush().catch(() => {
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
return {
|
|
2957
|
+
flush,
|
|
2958
|
+
onLogsMessage,
|
|
2959
|
+
pendingCount: () => pending.length,
|
|
2960
|
+
drainPersistent
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
// src/background/service-worker.ts
|
|
2965
|
+
var state = {
|
|
2966
|
+
serverConnected: false,
|
|
2967
|
+
wsConnected: false,
|
|
2968
|
+
recordingSessionId: null,
|
|
2969
|
+
eventCount: 0,
|
|
2970
|
+
ruleVersion: null,
|
|
2971
|
+
classification: null,
|
|
2972
|
+
sidePanel: "pending",
|
|
2973
|
+
serverVersion: null,
|
|
2974
|
+
serverBuildTime: null
|
|
2975
|
+
};
|
|
2976
|
+
var backgroundLogger = createExtensionLogger({ context: "background", sessionId: () => `bg-${Date.now()}` });
|
|
2977
|
+
backgroundLogger.patchConsole();
|
|
2978
|
+
var logForwarder = null;
|
|
2979
|
+
createBackgroundLogForwarder({
|
|
2980
|
+
transport: async (logs) => {
|
|
2981
|
+
const transport = await createHttpTransport({ serverUrl: SERVER_HTTP_URL });
|
|
2982
|
+
await transport(logs);
|
|
2983
|
+
}
|
|
2984
|
+
}).then((f) => {
|
|
2985
|
+
logForwarder = f;
|
|
2986
|
+
});
|
|
2987
|
+
var socket = null;
|
|
2988
|
+
var SEND_QUEUE_LIMIT = 500;
|
|
2989
|
+
var sendQueue = [];
|
|
2990
|
+
var queuedDropped = 0;
|
|
2991
|
+
function isFullSnapshot(payload) {
|
|
2992
|
+
return payload?.type === "rrweb" && payload?.payload?.type === 2;
|
|
2993
|
+
}
|
|
2994
|
+
function enqueueRecordingEvent(payload) {
|
|
2995
|
+
sendQueue.push({ raw: JSON.stringify(payload), isSnapshot: isFullSnapshot(payload) });
|
|
2996
|
+
while (sendQueue.length > SEND_QUEUE_LIMIT) {
|
|
2997
|
+
const idx = sendQueue.findIndex((item) => !item.isSnapshot);
|
|
2998
|
+
if (idx === -1) break;
|
|
2999
|
+
sendQueue.splice(idx, 1);
|
|
3000
|
+
queuedDropped++;
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
function flushSendQueue() {
|
|
3004
|
+
if (!socket || socket.readyState !== WebSocket.OPEN || !sendQueue.length) return;
|
|
3005
|
+
while (sendQueue.length) {
|
|
3006
|
+
const item = sendQueue.shift();
|
|
3007
|
+
try {
|
|
3008
|
+
socket.send(item.raw);
|
|
3009
|
+
} catch (e) {
|
|
3010
|
+
console.error("[HunterMate] flush send failed", e);
|
|
3011
|
+
break;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (queuedDropped > 0) {
|
|
3015
|
+
console.warn(`[HunterMate] send queue overflow, dropped ${queuedDropped} incremental events (snapshots kept)`);
|
|
3016
|
+
queuedDropped = 0;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
function sendLog(level, message, meta) {
|
|
3020
|
+
try {
|
|
3021
|
+
backgroundLogger[level === "debug" ? "debug" : level]("extension:sendLog", { message, meta });
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
const ws = socket;
|
|
3025
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
3026
|
+
try {
|
|
3027
|
+
ws.send(JSON.stringify({ type: "log", level, message, meta }));
|
|
3028
|
+
} catch (e) {
|
|
3029
|
+
console.error("[HunterMate] sendLog failed", e);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
const prefix = `[HunterMate] ${message}`;
|
|
3033
|
+
if (level === "debug") {
|
|
3034
|
+
console.debug(prefix, meta ?? "");
|
|
3035
|
+
} else if (level === "warn") {
|
|
3036
|
+
console.warn(prefix, meta ?? "");
|
|
3037
|
+
} else if (level === "error") {
|
|
3038
|
+
console.error(prefix, meta ?? "");
|
|
3039
|
+
} else {
|
|
3040
|
+
console.log(prefix, meta ?? "");
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
async function checkHttpHealth() {
|
|
3044
|
+
const wasConnected = state.serverConnected;
|
|
3045
|
+
try {
|
|
3046
|
+
const res = await fetch(`${SERVER_HTTP_URL}/health`);
|
|
3047
|
+
state.serverConnected = res.ok;
|
|
3048
|
+
if (res.ok) {
|
|
3049
|
+
const data = await res.json();
|
|
3050
|
+
state.serverVersion = data.version ?? null;
|
|
3051
|
+
state.serverBuildTime = data.buildTime ?? null;
|
|
3052
|
+
} else {
|
|
3053
|
+
state.serverVersion = null;
|
|
3054
|
+
state.serverBuildTime = null;
|
|
3055
|
+
}
|
|
3056
|
+
} catch {
|
|
3057
|
+
state.serverConnected = false;
|
|
3058
|
+
state.serverVersion = null;
|
|
3059
|
+
state.serverBuildTime = null;
|
|
3060
|
+
}
|
|
3061
|
+
if (!wasConnected && state.serverConnected) {
|
|
3062
|
+
broadcastState();
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
async function checkRuleVersion() {
|
|
3066
|
+
if (!state.serverConnected) return;
|
|
3067
|
+
try {
|
|
3068
|
+
const res = await fetch(`${SERVER_HTTP_URL}/rules/latest`);
|
|
3069
|
+
if (res.ok) {
|
|
3070
|
+
const data = await res.json();
|
|
3071
|
+
state.ruleVersion = data.version;
|
|
3072
|
+
}
|
|
3073
|
+
} catch {
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
function broadcastState() {
|
|
3077
|
+
chrome.runtime.sendMessage({ type: "status", payload: state }).catch(() => {
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
function connect() {
|
|
3081
|
+
if (socket) return;
|
|
3082
|
+
socket = new WebSocket(WS_URL);
|
|
3083
|
+
socket.onopen = () => {
|
|
3084
|
+
state.wsConnected = true;
|
|
3085
|
+
console.log("[HunterMate] connected to local server");
|
|
3086
|
+
socket?.send(
|
|
3087
|
+
JSON.stringify({
|
|
3088
|
+
type: "hello",
|
|
3089
|
+
extensionId: chrome.runtime.id,
|
|
3090
|
+
version: chrome.runtime.getManifest().version,
|
|
3091
|
+
capabilities: ["cp.*", "hm.*"]
|
|
3092
|
+
})
|
|
3093
|
+
);
|
|
3094
|
+
flushSendQueue();
|
|
3095
|
+
broadcastState();
|
|
3096
|
+
backgroundLogger.info("ws:connected", { extensionId: chrome.runtime.id });
|
|
3097
|
+
};
|
|
3098
|
+
socket.onclose = () => {
|
|
3099
|
+
socket = null;
|
|
3100
|
+
state.wsConnected = false;
|
|
3101
|
+
broadcastState();
|
|
3102
|
+
backgroundLogger.warn("ws:closed", { reconnectDelayMs: 5e3 });
|
|
3103
|
+
setTimeout(connect, 5e3);
|
|
3104
|
+
};
|
|
3105
|
+
socket.onerror = (err3) => {
|
|
3106
|
+
backgroundLogger.error("ws:error", { message: String(err3) });
|
|
3107
|
+
};
|
|
3108
|
+
socket.onmessage = async (event) => {
|
|
3109
|
+
let data;
|
|
3110
|
+
try {
|
|
3111
|
+
data = JSON.parse(event.data);
|
|
3112
|
+
} catch {
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
if (data.type === "pong" || data.type === "status" || data.type === "ack") return;
|
|
3116
|
+
if (data.type === "reload") {
|
|
3117
|
+
console.log("[HunterMate] received reload broadcast, reloading extension in 200ms...");
|
|
3118
|
+
setTimeout(() => chrome.runtime.reload(), 200);
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
if (data.type === "cmd" && data.requestId) {
|
|
3122
|
+
const startedAt = Date.now();
|
|
3123
|
+
sendLog("info", "extension handling control command", { command: data.command, requestId: data.requestId, params: data.params });
|
|
3124
|
+
let outcome;
|
|
3125
|
+
if (data.command === "hm.status") {
|
|
3126
|
+
const manifest = chrome.runtime.getManifest();
|
|
3127
|
+
outcome = {
|
|
3128
|
+
status: "ok",
|
|
3129
|
+
result: {
|
|
3130
|
+
...state,
|
|
3131
|
+
extBasePath: chrome.runtime.getURL(""),
|
|
3132
|
+
manifestPermissions: manifest.permissions,
|
|
3133
|
+
hasSidePanelInManifest: manifest.permissions?.includes("sidePanel"),
|
|
3134
|
+
chromeSidePanelType: typeof chrome.sidePanel
|
|
3135
|
+
}
|
|
3136
|
+
};
|
|
3137
|
+
} else {
|
|
3138
|
+
try {
|
|
3139
|
+
outcome = await executeControlCommand(String(data.command ?? ""), data.params ?? {}, {
|
|
3140
|
+
onLog: (level, message, meta) => sendLog(level, message, meta),
|
|
3141
|
+
sessionId: typeof data.sessionId === "string" ? data.sessionId : void 0
|
|
3142
|
+
});
|
|
3143
|
+
} catch (e) {
|
|
3144
|
+
outcome = { status: "error", message: e instanceof Error ? e.message : String(e) };
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
const resp = outcome.status === "ok" ? {
|
|
3148
|
+
type: "cmd_response",
|
|
3149
|
+
requestId: data.requestId,
|
|
3150
|
+
status: "ok",
|
|
3151
|
+
result: outcome.result,
|
|
3152
|
+
durationMs: Date.now() - startedAt
|
|
3153
|
+
} : {
|
|
3154
|
+
type: "cmd_response",
|
|
3155
|
+
requestId: data.requestId,
|
|
3156
|
+
status: "error",
|
|
3157
|
+
error: { code: outcome.code, message: outcome.message },
|
|
3158
|
+
durationMs: Date.now() - startedAt
|
|
3159
|
+
};
|
|
3160
|
+
try {
|
|
3161
|
+
socket?.send(JSON.stringify(resp));
|
|
3162
|
+
sendLog("debug", "cmd_response sent", { requestId: data.requestId, status: resp.status, durationMs: resp.durationMs });
|
|
3163
|
+
} catch (e) {
|
|
3164
|
+
sendLog("error", "cmd_response send failed", { requestId: data.requestId, error: e instanceof Error ? e.message : String(e) });
|
|
3165
|
+
}
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
console.log("[HunterMate] server message", data);
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
connect();
|
|
3172
|
+
var RECONNECT_ALARM_NAME = "hm:reconnect";
|
|
3173
|
+
function ensureReconnectAlarm() {
|
|
3174
|
+
chrome.alarms.create(RECONNECT_ALARM_NAME, { periodInMinutes: 0.5 }).catch((err3) => console.error("[HunterMate] create reconnect alarm failed", err3));
|
|
3175
|
+
}
|
|
3176
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
3177
|
+
if (alarm.name !== RECONNECT_ALARM_NAME) return;
|
|
3178
|
+
checkHttpHealth();
|
|
3179
|
+
if (socket && socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) {
|
|
3180
|
+
socket = null;
|
|
3181
|
+
}
|
|
3182
|
+
if (!socket) {
|
|
3183
|
+
connect();
|
|
3184
|
+
}
|
|
3185
|
+
});
|
|
3186
|
+
ensureReconnectAlarm();
|
|
3187
|
+
setInterval(() => {
|
|
3188
|
+
checkHttpHealth();
|
|
3189
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
3190
|
+
socket.send(JSON.stringify({ type: "ping" }));
|
|
3191
|
+
} else {
|
|
3192
|
+
connect();
|
|
3193
|
+
}
|
|
3194
|
+
broadcastState();
|
|
3195
|
+
}, 2e4);
|
|
3196
|
+
setInterval(() => {
|
|
3197
|
+
checkRuleVersion();
|
|
3198
|
+
}, 6e4);
|
|
3199
|
+
checkHttpHealth();
|
|
3200
|
+
checkRuleVersion();
|
|
3201
|
+
syncSites();
|
|
3202
|
+
setInterval(() => {
|
|
3203
|
+
syncSites();
|
|
3204
|
+
}, 10 * 60 * 1e3);
|
|
3205
|
+
watchForDiscovery();
|
|
3206
|
+
if (typeof chrome !== "undefined" && chrome.sidePanel?.setPanelBehavior) {
|
|
3207
|
+
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).then(() => {
|
|
3208
|
+
state.sidePanel = "ok";
|
|
3209
|
+
broadcastState();
|
|
3210
|
+
}).catch((err3) => {
|
|
3211
|
+
state.sidePanel = `error: ${err3?.message ?? err3}`;
|
|
3212
|
+
console.error("[HunterMate] setPanelBehavior failed", err3);
|
|
3213
|
+
broadcastState();
|
|
3214
|
+
});
|
|
3215
|
+
} else {
|
|
3216
|
+
state.sidePanel = "error: chrome.sidePanel API not available";
|
|
3217
|
+
}
|
|
3218
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
3219
|
+
if (message.type === "get-status") {
|
|
3220
|
+
sendResponse(state);
|
|
3221
|
+
return true;
|
|
3222
|
+
}
|
|
3223
|
+
if (message.type === "recording-started" && message.payload?.sessionId) {
|
|
3224
|
+
state.recordingSessionId = message.payload.sessionId;
|
|
3225
|
+
state.eventCount = 0;
|
|
3226
|
+
state.classification = message.payload.classification ?? null;
|
|
3227
|
+
broadcastState();
|
|
3228
|
+
sendResponse({ ok: true });
|
|
3229
|
+
return true;
|
|
3230
|
+
}
|
|
3231
|
+
if (message.type === "recording-stopped") {
|
|
3232
|
+
state.recordingSessionId = null;
|
|
3233
|
+
state.eventCount = 0;
|
|
3234
|
+
broadcastState();
|
|
3235
|
+
sendResponse({ ok: true });
|
|
3236
|
+
return true;
|
|
3237
|
+
}
|
|
3238
|
+
if (message.type === "recording-event") {
|
|
3239
|
+
const payload = { ...message.payload, tabId: sender.tab?.id };
|
|
3240
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
3241
|
+
socket.send(JSON.stringify(payload));
|
|
3242
|
+
} else {
|
|
3243
|
+
enqueueRecordingEvent(payload);
|
|
3244
|
+
}
|
|
3245
|
+
if (message.payload?.sessionId === state.recordingSessionId) {
|
|
3246
|
+
state.eventCount++;
|
|
3247
|
+
}
|
|
3248
|
+
} else if (message.type === "hm:logs" && Array.isArray(message.logs)) {
|
|
3249
|
+
logForwarder?.onLogsMessage(message.logs);
|
|
3250
|
+
}
|
|
3251
|
+
sendResponse({ ok: true });
|
|
3252
|
+
return true;
|
|
3253
|
+
});
|