hunter-mate 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/extension/background/service-worker.js +911 -159
- package/dist/assets/extension/build-info.json +1 -0
- package/dist/assets/extension/content/index.js +22 -4
- package/dist/assets/extension/manifest.json +5 -4
- package/dist/assets/extension/offscreen/offscreen.html +8 -0
- package/dist/assets/extension/offscreen/offscreen.js +149 -0
- package/dist/assets/extension/package.json +12 -4
- package/dist/assets/extension/sidepanel/index.css +2 -0
- package/dist/assets/extension/sidepanel/index.html +7 -394
- package/dist/assets/extension/sidepanel/index.js +53 -923
- package/dist/assets/server/app.module.js +12 -1
- package/dist/assets/server/app.module.js.map +1 -1
- package/dist/assets/server/auth/auth.controller.d.ts +7 -0
- package/dist/assets/server/auth/auth.controller.js +20 -0
- package/dist/assets/server/auth/auth.controller.js.map +1 -1
- package/dist/assets/server/auth/auth.middleware.js +9 -1
- package/dist/assets/server/auth/auth.middleware.js.map +1 -1
- package/dist/assets/server/auth/auth.service.d.ts +11 -0
- package/dist/assets/server/auth/auth.service.js +58 -1
- package/dist/assets/server/auth/auth.service.js.map +1 -1
- package/dist/assets/server/build-info.json +1 -1
- package/dist/assets/server/customer/customer.controller.d.ts +18 -0
- package/dist/assets/server/customer/customer.controller.js +81 -0
- package/dist/assets/server/customer/customer.controller.js.map +1 -0
- package/dist/assets/server/customer/customer.entity.d.ts +17 -0
- package/dist/assets/server/customer/customer.entity.js +82 -0
- package/dist/assets/server/customer/customer.entity.js.map +1 -0
- package/dist/assets/server/customer/customer.module.d.ts +2 -0
- package/dist/assets/server/customer/customer.module.js +28 -0
- package/dist/assets/server/customer/customer.module.js.map +1 -0
- package/dist/assets/server/customer/customer.service.d.ts +36 -0
- package/dist/assets/server/customer/customer.service.js +200 -0
- package/dist/assets/server/customer/customer.service.js.map +1 -0
- package/dist/assets/server/db/schema-validator.service.d.ts +2 -0
- package/dist/assets/server/db/schema-validator.service.js +32 -1
- package/dist/assets/server/db/schema-validator.service.js.map +1 -1
- package/dist/assets/server/db/sqlite.module.d.ts +1 -0
- package/dist/assets/server/db/sqlite.module.js +12 -2
- package/dist/assets/server/db/sqlite.module.js.map +1 -1
- package/dist/assets/server/devops/devops-sink-init.d.ts +1 -14
- package/dist/assets/server/devops/devops-sink-init.js +3 -8
- package/dist/assets/server/devops/devops-sink-init.js.map +1 -1
- package/dist/assets/server/extension/extension-control.controller.d.ts +15 -0
- package/dist/assets/server/extension/extension-control.controller.js +42 -2
- package/dist/assets/server/extension/extension-control.controller.js.map +1 -1
- package/dist/assets/server/extension/extension-control.service.d.ts +14 -1
- package/dist/assets/server/extension/extension-control.service.js +65 -2
- package/dist/assets/server/extension/extension-control.service.js.map +1 -1
- package/dist/assets/server/extraction/adapter.service.d.ts +3 -0
- package/dist/assets/server/extraction/adapter.service.js +127 -6
- package/dist/assets/server/extraction/adapter.service.js.map +1 -1
- package/dist/assets/server/extraction/anomaly-report.service.js +1 -1
- package/dist/assets/server/extraction/anomaly-report.service.js.map +1 -1
- package/dist/assets/server/extraction/entities/adapter-rule.entity.d.ts +2 -0
- package/dist/assets/server/extraction/entities/adapter-rule.entity.js +10 -0
- package/dist/assets/server/extraction/entities/adapter-rule.entity.js.map +1 -1
- package/dist/assets/server/extraction/extraction-entity.controller.d.ts +21 -3
- package/dist/assets/server/extraction/extraction-entity.controller.js +44 -9
- package/dist/assets/server/extraction/extraction-entity.controller.js.map +1 -1
- package/dist/assets/server/extraction/extraction.module.js +13 -3
- package/dist/assets/server/extraction/extraction.module.js.map +1 -1
- package/dist/assets/server/extraction/extraction.service.d.ts +4 -1
- package/dist/assets/server/extraction/extraction.service.js +59 -4
- package/dist/assets/server/extraction/extraction.service.js.map +1 -1
- package/dist/assets/server/extraction/harness-overlay.service.d.ts +41 -0
- package/dist/assets/server/extraction/harness-overlay.service.js +319 -0
- package/dist/assets/server/extraction/harness-overlay.service.js.map +1 -0
- package/dist/assets/server/extraction/liepin-extractor.d.ts +1 -0
- package/dist/assets/server/extraction/liepin-extractor.js +172 -0
- package/dist/assets/server/extraction/liepin-extractor.js.map +1 -0
- package/dist/assets/server/extraction/script-rule.js +4 -0
- package/dist/assets/server/extraction/script-rule.js.map +1 -1
- package/dist/assets/server/governance/action-audit.entity.d.ts +12 -0
- package/dist/assets/server/governance/action-audit.entity.js +66 -0
- package/dist/assets/server/governance/action-audit.entity.js.map +1 -0
- package/dist/assets/server/governance/governance.controller.d.ts +14 -0
- package/dist/assets/server/governance/governance.controller.js +77 -0
- package/dist/assets/server/governance/governance.controller.js.map +1 -0
- package/dist/assets/server/governance/governance.ingester.d.ts +20 -0
- package/dist/assets/server/governance/governance.ingester.js +114 -0
- package/dist/assets/server/governance/governance.ingester.js.map +1 -0
- package/dist/assets/server/governance/governance.module.d.ts +2 -0
- package/dist/assets/server/governance/governance.module.js +26 -0
- package/dist/assets/server/governance/governance.module.js.map +1 -0
- package/dist/assets/server/governance/governance.service.d.ts +114 -0
- package/dist/assets/server/governance/governance.service.js +356 -0
- package/dist/assets/server/governance/governance.service.js.map +1 -0
- package/dist/assets/server/jobcase/jobcase.controller.d.ts +1 -0
- package/dist/assets/server/jobcase/jobcase.entity.d.ts +1 -0
- package/dist/assets/server/jobcase/jobcase.entity.js +6 -0
- package/dist/assets/server/jobcase/jobcase.entity.js.map +1 -1
- package/dist/assets/server/jobcase/jobcase.service.d.ts +2 -0
- package/dist/assets/server/jobcase/jobcase.service.js +2 -1
- package/dist/assets/server/jobcase/jobcase.service.js.map +1 -1
- package/dist/assets/server/main.js +1 -0
- package/dist/assets/server/main.js.map +1 -1
- package/dist/assets/server/package.json +5 -4
- package/dist/assets/server/preference/preference.controller.d.ts +14 -0
- package/dist/assets/server/preference/preference.controller.js +66 -0
- package/dist/assets/server/preference/preference.controller.js.map +1 -0
- package/dist/assets/server/preference/preference.entity.d.ts +13 -0
- package/dist/assets/server/preference/preference.entity.js +77 -0
- package/dist/assets/server/preference/preference.entity.js.map +1 -0
- package/dist/assets/server/preference/preference.module.d.ts +2 -0
- package/dist/assets/server/preference/preference.module.js +26 -0
- package/dist/assets/server/preference/preference.module.js.map +1 -0
- package/dist/assets/server/preference/preference.service.d.ts +33 -0
- package/dist/assets/server/preference/preference.service.js +122 -0
- package/dist/assets/server/preference/preference.service.js.map +1 -0
- package/dist/assets/server/recording/recording-event-queue.service.d.ts +26 -0
- package/dist/assets/server/recording/recording-event-queue.service.js +115 -0
- package/dist/assets/server/recording/recording-event-queue.service.js.map +1 -0
- package/dist/assets/server/recording/recording-retention.service.d.ts +38 -0
- package/dist/assets/server/recording/recording-retention.service.js +174 -0
- package/dist/assets/server/recording/recording-retention.service.js.map +1 -0
- package/dist/assets/server/recording/recording-snapshot.service.d.ts +1 -0
- package/dist/assets/server/recording/recording-snapshot.service.js +4 -0
- package/dist/assets/server/recording/recording-snapshot.service.js.map +1 -1
- package/dist/assets/server/recording/recording.controller.d.ts +21 -1
- package/dist/assets/server/recording/recording.controller.js +37 -2
- package/dist/assets/server/recording/recording.controller.js.map +1 -1
- package/dist/assets/server/recording/recording.gateway.js +11 -0
- package/dist/assets/server/recording/recording.gateway.js.map +1 -1
- package/dist/assets/server/recording/recording.module.js +16 -2
- package/dist/assets/server/recording/recording.module.js.map +1 -1
- package/dist/assets/server/recording/recording.service.d.ts +10 -1
- package/dist/assets/server/recording/recording.service.js +31 -6
- package/dist/assets/server/recording/recording.service.js.map +1 -1
- package/dist/assets/server/remark/remark.controller.d.ts +1 -1
- package/dist/assets/server/remark/remark.controller.js +1 -10
- package/dist/assets/server/remark/remark.controller.js.map +1 -1
- package/dist/assets/server/remark/remark.entity.d.ts +1 -1
- package/dist/assets/server/remark/remark.entity.js.map +1 -1
- package/dist/assets/server/site/entities/site-registry.entity.d.ts +1 -1
- package/dist/assets/server/site/entities/site-registry.entity.js +1 -1
- package/dist/assets/server/site/entities/site-registry.entity.js.map +1 -1
- package/dist/assets/server/site/site.controller.d.ts +1 -1
- package/dist/assets/server/site/site.service.d.ts +5 -1
- package/dist/assets/server/site/site.service.js +13 -0
- package/dist/assets/server/site/site.service.js.map +1 -1
- package/dist/assets/server/system/system.controller.d.ts +1 -0
- package/dist/assets/server/system/system.controller.js +1 -1
- package/dist/assets/server/system/system.controller.js.map +1 -1
- package/dist/assets/server/system/system.service.d.ts +1 -0
- package/dist/assets/server/system/system.service.js +3 -2
- package/dist/assets/server/system/system.service.js.map +1 -1
- package/dist/cache/db.js +6 -6
- package/dist/commands/addons.js +2 -1
- package/dist/commands/cache.js +3 -4
- package/dist/commands/case.d.ts +67 -0
- package/dist/commands/case.js +101 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +8 -1
- package/dist/commands/customer.d.ts +25 -0
- package/dist/commands/customer.js +137 -0
- package/dist/commands/doctor.js +119 -13
- package/dist/commands/extension.js +26 -32
- package/dist/commands/extras.d.ts +22 -3
- package/dist/commands/extras.js +116 -7
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +3 -4
- package/dist/commands/issue-env.d.ts +23 -0
- package/dist/commands/issue-env.js +101 -0
- package/dist/commands/job.d.ts +6 -2
- package/dist/commands/job.js +75 -20
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +85 -13
- package/dist/commands/migrate.d.ts +32 -0
- package/dist/commands/migrate.js +116 -0
- package/dist/commands/note.d.ts +3 -0
- package/dist/commands/note.js +75 -0
- package/dist/commands/ops.js +82 -15
- package/dist/commands/preference.d.ts +6 -0
- package/dist/commands/preference.js +80 -0
- package/dist/commands/record.js +2 -4
- package/dist/commands/report.js +2 -4
- package/dist/commands/search.js +9 -2
- package/dist/commands/server.d.ts +9 -2
- package/dist/commands/server.js +118 -19
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.js +16 -6
- package/dist/commands/update.js +8 -9
- package/dist/config.d.ts +21 -0
- package/dist/config.js +27 -0
- package/dist/i18n/locales/en-US.json +19 -1
- package/dist/i18n/locales/zh-CN.json +19 -1
- package/dist/index.js +52 -2
- package/dist/keychain.d.ts +16 -1
- package/dist/keychain.js +43 -16
- package/dist/logger.js +2 -2
- package/dist/updater.d.ts +1 -1
- package/dist/updater.js +24 -7
- package/dist/utils/command-error.d.ts +11 -0
- package/dist/utils/command-error.js +47 -0
- package/dist/utils/link.d.ts +18 -0
- package/dist/utils/link.js +83 -0
- package/dist/utils/proxy-detect.d.ts +20 -0
- package/dist/utils/proxy-detect.js +96 -0
- package/dist/utils/script-path.d.ts +5 -0
- package/dist/utils/script-path.js +27 -0
- package/dist/version.js +34 -9
- package/dist/watchdog.d.ts +23 -1
- package/dist/watchdog.js +97 -6
- package/dist/web/dist/404.html +1 -0
- package/dist/web/dist/_next/static/WdJr3NhHjlu5xN0qOap1k/_buildManifest.js +1 -0
- package/dist/web/dist/_next/static/WdJr3NhHjlu5xN0qOap1k/_ssgManifest.js +1 -0
- package/dist/web/dist/_next/static/chunks/166.f30be223f694f8ee.js +1 -0
- package/dist/web/dist/_next/static/chunks/1e6d5a0c.0488932e17ecdf24.js +15 -0
- package/dist/web/dist/_next/static/chunks/539-5d0e8c5c238c56f8.js +1 -0
- package/dist/web/dist/_next/static/chunks/589-d632233641f35681.js +1 -0
- package/dist/web/dist/_next/static/chunks/671.3b8e9cc96b25b6ea.js +1 -0
- package/dist/web/dist/_next/static/chunks/935-99f57b7d4151d9e7.js +5 -0
- package/dist/web/dist/_next/static/chunks/a7067448-96dd4b8457c447e8.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/_not-found/page-fdf1a13a7656886a.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/auth/verify/page-8e5cff53d6f7d8e1.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/companies/page-e38a69e1929d0953.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/entity/page-250f8a9b6d91ab19.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/jobs/page-6a281776fec065f6.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/layout-83e623e973c1693f.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/logs/page-d478031d49f7ba00.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/matching/jobs/[id]/talents/page-f57f4b7529823601.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/matching/portraits/[id]/jobs/page-614d1e62d7b9d215.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/page-e3392de08ae81fba.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/parselog/page-58d3acece95b4089.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/recordings/page-e2e2e3c3a64083fb.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/snapshots/[id]/agent-prompt/page-fd3b7d7373b8f8b7.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/talents/page-30748338da20d7e0.js +1 -0
- package/dist/web/dist/_next/static/chunks/framework-b3a558b17a070965.js +1 -0
- package/dist/web/dist/_next/static/chunks/main-5a4e0cd75dd7f04b.js +1 -0
- package/dist/web/dist/_next/static/chunks/main-app-80b038a312917d90.js +1 -0
- package/dist/web/dist/_next/static/chunks/pages/_app-da7d628f49271ece.js +4 -0
- package/dist/web/dist/_next/static/chunks/pages/_error-17756f67e0a11350.js +1 -0
- package/dist/web/dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/web/dist/_next/static/chunks/webpack-1784baecc6fdcf0b.js +1 -0
- package/dist/web/dist/_next/static/css/db881c5b3072b88a.css +3 -0
- package/dist/web/dist/_next/static/css/f60d210841fab76b.css +1 -0
- package/dist/web/dist/auth/verify.html +1 -0
- package/dist/web/dist/auth/verify.txt +8 -0
- package/dist/web/dist/companies.html +1 -0
- package/dist/web/dist/companies.txt +7 -0
- package/dist/web/dist/entity.html +1 -0
- package/dist/web/dist/entity.txt +8 -0
- package/dist/web/dist/index.html +1 -0
- package/dist/web/dist/index.txt +8 -0
- package/dist/web/dist/jobs.html +1 -0
- package/dist/web/dist/jobs.txt +7 -0
- package/dist/web/dist/logs.html +1 -0
- package/dist/web/dist/logs.txt +8 -0
- package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -0
- package/dist/web/dist/matching/jobs/placeholder/talents.txt +8 -0
- package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -0
- package/dist/web/dist/matching/portraits/placeholder/jobs.txt +8 -0
- package/dist/web/dist/parselog.html +1 -0
- package/dist/web/dist/parselog.txt +8 -0
- package/dist/web/dist/recordings.html +1 -0
- package/dist/web/dist/recordings.txt +8 -0
- package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -0
- package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +8 -0
- package/dist/web/dist/talents.html +1 -0
- package/dist/web/dist/talents.txt +7 -0
- package/package.json +26 -6
|
@@ -991,8 +991,27 @@ var control_runtime_source_default = '"use strict";\n(() => {\n // src/shared/c
|
|
|
991
991
|
// src/background/cdp.ts
|
|
992
992
|
var attached = /* @__PURE__ */ new Set();
|
|
993
993
|
var CDP_COMMAND_TIMEOUT_MS = 15e3;
|
|
994
|
-
function send(target, method, params) {
|
|
995
|
-
return
|
|
994
|
+
function send(target, method, params, timeoutMs = CDP_COMMAND_TIMEOUT_MS) {
|
|
995
|
+
return new Promise((resolve, reject) => {
|
|
996
|
+
const timer = setTimeout(() => {
|
|
997
|
+
if (target.tabId !== void 0) {
|
|
998
|
+
chrome.debugger.detach({ tabId: target.tabId }).catch(() => void 0).then(() => {
|
|
999
|
+
attached.delete(target.tabId);
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
reject(new Error(`cdp_timeout: ${method} after ${timeoutMs}ms (tab frozen or unresponsive)`));
|
|
1003
|
+
}, timeoutMs);
|
|
1004
|
+
chrome.debugger.sendCommand(target, method, params ?? {}).then(
|
|
1005
|
+
(v) => {
|
|
1006
|
+
clearTimeout(timer);
|
|
1007
|
+
resolve(v);
|
|
1008
|
+
},
|
|
1009
|
+
(err3) => {
|
|
1010
|
+
clearTimeout(timer);
|
|
1011
|
+
reject(err3);
|
|
1012
|
+
}
|
|
1013
|
+
);
|
|
1014
|
+
});
|
|
996
1015
|
}
|
|
997
1016
|
function isAttachableUrl(url) {
|
|
998
1017
|
if (!url) return false;
|
|
@@ -1326,6 +1345,15 @@ chrome.debugger.onDetach.addListener((source) => {
|
|
|
1326
1345
|
networkEnabledTabs.delete(source.tabId);
|
|
1327
1346
|
}
|
|
1328
1347
|
});
|
|
1348
|
+
async function detachIfAttached(tabId) {
|
|
1349
|
+
if (!attached.has(tabId)) return;
|
|
1350
|
+
try {
|
|
1351
|
+
await chrome.debugger.detach({ tabId });
|
|
1352
|
+
} catch {
|
|
1353
|
+
}
|
|
1354
|
+
attached.delete(tabId);
|
|
1355
|
+
networkEnabledTabs.delete(tabId);
|
|
1356
|
+
}
|
|
1329
1357
|
var CDP_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1330
1358
|
"Accessibility.enable",
|
|
1331
1359
|
"Accessibility.getFullAXTree",
|
|
@@ -1362,10 +1390,11 @@ async function insertText(tabId, text) {
|
|
|
1362
1390
|
}
|
|
1363
1391
|
async function cdpEvaluate(tabId, expression, timeoutMs = CDP_COMMAND_TIMEOUT_MS) {
|
|
1364
1392
|
await ensureAttached(tabId);
|
|
1365
|
-
const result = await
|
|
1393
|
+
const result = await send(
|
|
1366
1394
|
{ tabId },
|
|
1367
1395
|
"Runtime.evaluate",
|
|
1368
|
-
{ expression, returnByValue: true, awaitPromise: true }
|
|
1396
|
+
{ expression, returnByValue: true, awaitPromise: true },
|
|
1397
|
+
timeoutMs
|
|
1369
1398
|
);
|
|
1370
1399
|
if (result.exceptionDetails) {
|
|
1371
1400
|
const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
|
|
@@ -1534,6 +1563,21 @@ async function captureActiveTabScreenshot2(tab, options = {}) {
|
|
|
1534
1563
|
throw new Error("capture_failed_unreachable");
|
|
1535
1564
|
}
|
|
1536
1565
|
|
|
1566
|
+
// src/shared/eval-wrap.ts
|
|
1567
|
+
function buildEvalSyntaxCheckExpression(script) {
|
|
1568
|
+
return "(() => { try { new Function(" + JSON.stringify("return (async () => (" + script + "\n))();") + "); return true; } catch (e) { return false; } })()";
|
|
1569
|
+
}
|
|
1570
|
+
function buildEvalExpressionWrapper(script) {
|
|
1571
|
+
return `(async () => { return (
|
|
1572
|
+
${script}
|
|
1573
|
+
); })()`;
|
|
1574
|
+
}
|
|
1575
|
+
function buildEvalStatementWrapper(script) {
|
|
1576
|
+
return `(async () => {
|
|
1577
|
+
${script}
|
|
1578
|
+
})()`;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1537
1581
|
// src/background/session-groups.ts
|
|
1538
1582
|
var STORAGE_KEY = "hmSessionGroups";
|
|
1539
1583
|
var cache = /* @__PURE__ */ new Map();
|
|
@@ -1869,8 +1913,48 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
|
|
|
1869
1913
|
await chrome.windows.remove(windowId);
|
|
1870
1914
|
return ok({ closed: true, windowId });
|
|
1871
1915
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1916
|
+
if (command === "hm.linkedin.search") {
|
|
1917
|
+
try {
|
|
1918
|
+
const result = await runLinkedInSearch(controlArgs, log, {
|
|
1919
|
+
sessionId,
|
|
1920
|
+
groupTitle,
|
|
1921
|
+
explicitTab
|
|
1922
|
+
});
|
|
1923
|
+
return ok(result);
|
|
1924
|
+
} catch (e) {
|
|
1925
|
+
const code = e instanceof ControlBlockedError ? e.code : void 0;
|
|
1926
|
+
return err2(e instanceof Error ? e.message : String(e), code);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (command === "hm.liepin.search") {
|
|
1930
|
+
try {
|
|
1931
|
+
const result = await runLiepinSearch(controlArgs, log, {
|
|
1932
|
+
sessionId,
|
|
1933
|
+
groupTitle,
|
|
1934
|
+
explicitTab
|
|
1935
|
+
});
|
|
1936
|
+
return ok(result);
|
|
1937
|
+
} catch (e) {
|
|
1938
|
+
const code = e instanceof ControlBlockedError ? e.code : void 0;
|
|
1939
|
+
return err2(e instanceof Error ? e.message : String(e), code);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
let tab = await getControlTab(tabOverride);
|
|
1943
|
+
if (!tab?.id) {
|
|
1944
|
+
if (command === "cp.goto" && typeof controlArgs.url === "string" && controlArgs.url) {
|
|
1945
|
+
const created = await chrome.tabs.create({ url: controlArgs.url, active: false });
|
|
1946
|
+
log("info", "auto-created tab for cp.goto (no active tab)", { tabId: created.id, url: controlArgs.url });
|
|
1947
|
+
if (sessionId && !explicitTab) {
|
|
1948
|
+
addTabToSessionGroup(sessionId, groupTitle, created).catch(
|
|
1949
|
+
(err3) => log("warn", "cp.goto auto-tab addTabToSessionGroup failed", { sessionId, error: err3.message })
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
tab = created;
|
|
1953
|
+
} else {
|
|
1954
|
+
return err2("no_active_tab");
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
if (!tab.id) return err2("no_active_tab");
|
|
1874
1958
|
if (sessionId && !explicitTab) {
|
|
1875
1959
|
ensureSessionGroup(sessionId, groupTitle, tab).catch(
|
|
1876
1960
|
(err3) => log("warn", "ensureSessionGroup failed", { sessionId, tabId: tab.id, error: err3.message })
|
|
@@ -1945,20 +2029,24 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
|
|
|
1945
2029
|
if (command === "cp.eval" && typeof controlArgs.script === "string") {
|
|
1946
2030
|
const script = controlArgs.script;
|
|
1947
2031
|
const frame = controlArgs.frame;
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2032
|
+
const tabId = tab.id;
|
|
2033
|
+
const evalWith = async (expr) => {
|
|
2034
|
+
if (frame !== void 0 && isAttachableUrl(tab.url)) {
|
|
2035
|
+
let frameId = typeof frame === "string" ? frame : "";
|
|
2036
|
+
let frameUrl;
|
|
2037
|
+
if (typeof frame === "number" || /^\d+$/.test(String(frame))) {
|
|
2038
|
+
const list = await frameListCdp(tabId);
|
|
2039
|
+
const target = list.frames?.find((f) => f.index === Number(frame));
|
|
2040
|
+
if (!target) throw new Error(`frame_not_found: ${frame}`);
|
|
2041
|
+
frameId = target.frame_id;
|
|
2042
|
+
frameUrl = target.url;
|
|
2043
|
+
}
|
|
2044
|
+
return evaluateInFrame(tabId, expr, frameId, frameUrl);
|
|
1957
2045
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
const result = await
|
|
2046
|
+
return cdpEvaluate(tabId, expr);
|
|
2047
|
+
};
|
|
2048
|
+
const useExpression = await evalWith(buildEvalSyntaxCheckExpression(script)).catch(() => false) === true;
|
|
2049
|
+
const result = await evalWith(useExpression ? buildEvalExpressionWrapper(script) : buildEvalStatementWrapper(script));
|
|
1962
2050
|
return ok(result);
|
|
1963
2051
|
}
|
|
1964
2052
|
if (command === "hm.linkedin.extract") {
|
|
@@ -1983,6 +2071,17 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
|
|
|
1983
2071
|
return err2(e instanceof Error ? e.message : String(e));
|
|
1984
2072
|
}
|
|
1985
2073
|
}
|
|
2074
|
+
if (command === "hm.liepin.extract") {
|
|
2075
|
+
if (!isAttachableUrl(tab.url)) {
|
|
2076
|
+
return err2("liepin_extract_requires_http_page");
|
|
2077
|
+
}
|
|
2078
|
+
try {
|
|
2079
|
+
const result = await cdpEvaluate(tab.id, LIEPIN_EXTRACT_SOURCE);
|
|
2080
|
+
return ok(result);
|
|
2081
|
+
} catch (e) {
|
|
2082
|
+
return err2(e instanceof Error ? e.message : String(e));
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
1986
2085
|
if (command.startsWith("hm.")) {
|
|
1987
2086
|
return { status: "error", code: "not_implemented", message: `${command} is not implemented yet` };
|
|
1988
2087
|
}
|
|
@@ -2021,6 +2120,13 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
|
|
|
2021
2120
|
return err2(e instanceof Error ? e.message : String(e));
|
|
2022
2121
|
}
|
|
2023
2122
|
}
|
|
2123
|
+
var ControlBlockedError = class extends Error {
|
|
2124
|
+
constructor(code, message) {
|
|
2125
|
+
super(message);
|
|
2126
|
+
this.code = code;
|
|
2127
|
+
this.name = "ControlBlockedError";
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2024
2130
|
function buildInjectionExpression(command, controlArgs) {
|
|
2025
2131
|
return '(() => { try { if (typeof globalThis.__cpRunControl !== "function" || globalThis.__cpRuntimeVersion !== ' + JSON.stringify(RUNTIME_VERSION) + ") {\n" + control_runtime_source_default + "\n} const r = globalThis.__cpRunControl(" + JSON.stringify(command) + ", " + JSON.stringify(controlArgs) + '); if (r && typeof r.then === "function") { return r; } return r; } catch (e) { return { __cpError: String((e && e.message) || e) }; } })()';
|
|
2026
2132
|
}
|
|
@@ -2213,6 +2319,657 @@ var LINKEDIN_EXTRACT_SOURCE = String.raw`(async () => {
|
|
|
2213
2319
|
extractedAt: new Date().toISOString(),
|
|
2214
2320
|
};
|
|
2215
2321
|
})()`;
|
|
2322
|
+
var LINKEDIN_WALL_CHECK_SOURCE = String.raw`(() => {
|
|
2323
|
+
try {
|
|
2324
|
+
if (document.querySelector('iframe[src*="captcha"], iframe[src*="challenge-platform"]')) return 'CAPTCHA';
|
|
2325
|
+
const bodyText = (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 3000);
|
|
2326
|
+
if (/verify you('| a)re human|captcha|security (check|verification)/i.test(bodyText)) return 'CAPTCHA';
|
|
2327
|
+
const url = location.href;
|
|
2328
|
+
if (/linkedin\.com\/(authwall|uas\/login|login)/i.test(url)) return 'LOGIN_WALL';
|
|
2329
|
+
if (/linkedin\.com\/checkpoint/i.test(url)) return 'CAPTCHA';
|
|
2330
|
+
} catch (e) { /* 检测失败不阻断搜索 */ }
|
|
2331
|
+
return null;
|
|
2332
|
+
})()`;
|
|
2333
|
+
var LINKEDIN_NEXT_CLICK_SOURCE = String.raw`(async () => {
|
|
2334
|
+
const btn = document.querySelector('button.artdeco-pagination__button--next')
|
|
2335
|
+
|| document.querySelector('button[aria-label="Next"]')
|
|
2336
|
+
|| document.querySelector('[aria-label="Next"] button');
|
|
2337
|
+
if (!btn || btn.disabled) return false;
|
|
2338
|
+
btn.click();
|
|
2339
|
+
return true;
|
|
2340
|
+
})()`;
|
|
2341
|
+
function sleep(ms) {
|
|
2342
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
2343
|
+
}
|
|
2344
|
+
async function waitTabLoaded(tabId, timeoutMs = 25e3) {
|
|
2345
|
+
const t0 = Date.now();
|
|
2346
|
+
let lastUrl = "";
|
|
2347
|
+
let stableHits = 0;
|
|
2348
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
2349
|
+
const t = await chrome.tabs.get(tabId).catch(() => null);
|
|
2350
|
+
if (!t) throw new Error(`tab ${tabId} closed while loading`);
|
|
2351
|
+
if (t.status === "complete") {
|
|
2352
|
+
if (t.url === lastUrl) {
|
|
2353
|
+
stableHits++;
|
|
2354
|
+
if (stableHits >= 2) return;
|
|
2355
|
+
} else {
|
|
2356
|
+
stableHits = 0;
|
|
2357
|
+
lastUrl = t.url || "";
|
|
2358
|
+
}
|
|
2359
|
+
} else {
|
|
2360
|
+
stableHits = 0;
|
|
2361
|
+
lastUrl = "";
|
|
2362
|
+
}
|
|
2363
|
+
await sleep(400);
|
|
2364
|
+
}
|
|
2365
|
+
throw new Error(`tab ${tabId} load timeout after ${timeoutMs}ms`);
|
|
2366
|
+
}
|
|
2367
|
+
async function runLinkedInSearch(args, log, ctx) {
|
|
2368
|
+
const keywords = typeof args.keywords === "string" ? args.keywords.trim() : "";
|
|
2369
|
+
const filters = args.filters && typeof args.filters === "object" ? args.filters : {};
|
|
2370
|
+
const maxPages = Math.min(Math.max(Number(args.maxPages) || 3, 1), 10);
|
|
2371
|
+
const keepTab = args.keepTab === true;
|
|
2372
|
+
const activateTab = args.activateTab !== false;
|
|
2373
|
+
const loadTimeoutMs = Math.min(Math.max(Number(args.loadTimeoutMs) || 25e3, 5e3), 3e5);
|
|
2374
|
+
const delayBase = typeof args.delayMs === "number" ? Number(args.delayMs) : 2500;
|
|
2375
|
+
let effectiveKeywords = keywords;
|
|
2376
|
+
if (!filters.geoUrn && typeof filters.location === "string" && filters.location.trim()) {
|
|
2377
|
+
effectiveKeywords = (keywords + " " + filters.location.trim()).trim();
|
|
2378
|
+
}
|
|
2379
|
+
if (!effectiveKeywords && !filters.company && !filters.school) {
|
|
2380
|
+
throw new Error("linkedin_search_requires_keywords_or_filters");
|
|
2381
|
+
}
|
|
2382
|
+
const u = new URL("https://www.linkedin.com/search/results/people/");
|
|
2383
|
+
if (effectiveKeywords) u.searchParams.set("keywords", effectiveKeywords);
|
|
2384
|
+
if (typeof filters.geoUrn === "string" && filters.geoUrn) u.searchParams.set("geoUrn", filters.geoUrn);
|
|
2385
|
+
if (typeof filters.company === "string" && filters.company) u.searchParams.set("company", filters.company);
|
|
2386
|
+
if (typeof filters.school === "string" && filters.school) u.searchParams.set("school", filters.school);
|
|
2387
|
+
const tab = await chrome.tabs.create({ url: u.toString(), active: false });
|
|
2388
|
+
log("info", "hm.linkedin.search tab created", { tabId: tab.id, url: u.toString(), maxPages, activateTab });
|
|
2389
|
+
if (activateTab && tab.id != null) {
|
|
2390
|
+
await chrome.tabs.update(tab.id, { active: true }).catch(
|
|
2391
|
+
(err3) => log("warn", "hm.linkedin.search activate tab failed", { tabId: tab.id, error: err3.message })
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
if (ctx.sessionId && !ctx.explicitTab && tab.id) {
|
|
2395
|
+
addTabToSessionGroup(ctx.sessionId, ctx.groupTitle, tab).catch(
|
|
2396
|
+
(err3) => log("warn", "hm.linkedin.search addTabToSessionGroup failed", { error: err3.message })
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
const leads = /* @__PURE__ */ new Map();
|
|
2400
|
+
let pagesScanned = 0;
|
|
2401
|
+
let lastUrl = "";
|
|
2402
|
+
try {
|
|
2403
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
2404
|
+
if (tab.id == null) break;
|
|
2405
|
+
await waitTabLoaded(tab.id, loadTimeoutMs);
|
|
2406
|
+
const blocked = await cdpEvaluate(tab.id, LINKEDIN_WALL_CHECK_SOURCE).catch(() => null);
|
|
2407
|
+
if (blocked === "CAPTCHA" || blocked === "LOGIN_WALL") {
|
|
2408
|
+
log("warn", "hm.linkedin.search blocked by linkedin risk control", { blocked, page, url: lastUrl });
|
|
2409
|
+
throw new ControlBlockedError(blocked, `linkedin search aborted on page ${page}: ${blocked}`);
|
|
2410
|
+
}
|
|
2411
|
+
const extracted = await cdpEvaluate(tab.id, LINKEDIN_EXTRACT_SOURCE);
|
|
2412
|
+
lastUrl = extracted?.url || lastUrl;
|
|
2413
|
+
const pageLeads = extracted?.links?.talent || [];
|
|
2414
|
+
for (const l of pageLeads) {
|
|
2415
|
+
if (l.url && !leads.has(l.url)) {
|
|
2416
|
+
leads.set(l.url, { url: l.url, name: l.name ?? null, page, source: "linkedin" });
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
pagesScanned = page;
|
|
2420
|
+
log("info", "hm.linkedin.search page extracted", { page, pageLeads: pageLeads.length, total: leads.size });
|
|
2421
|
+
if (page >= maxPages) break;
|
|
2422
|
+
const clicked = await cdpEvaluate(tab.id, LINKEDIN_NEXT_CLICK_SOURCE).catch(() => false);
|
|
2423
|
+
const urlBefore = (await chrome.tabs.get(tab.id).catch(() => null))?.url || "";
|
|
2424
|
+
await sleep(clicked ? 2500 : 0);
|
|
2425
|
+
const urlAfterClick = (await chrome.tabs.get(tab.id).catch(() => null))?.url || "";
|
|
2426
|
+
if (!clicked || urlAfterClick === urlBefore) {
|
|
2427
|
+
const next = new URL(u.toString());
|
|
2428
|
+
next.searchParams.set("page", String(page + 1));
|
|
2429
|
+
if (next.toString() === urlBefore) break;
|
|
2430
|
+
await chrome.tabs.update(tab.id, { url: next.toString() });
|
|
2431
|
+
log("debug", "hm.linkedin.search fallback to page param", { page: page + 1 });
|
|
2432
|
+
}
|
|
2433
|
+
await sleep(delayBase + Math.random() * 2500);
|
|
2434
|
+
}
|
|
2435
|
+
} finally {
|
|
2436
|
+
if (!keepTab && tab.id != null) {
|
|
2437
|
+
await closeTabGracefully(tab.id);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
return {
|
|
2441
|
+
keywords: effectiveKeywords,
|
|
2442
|
+
searchUrl: u.toString(),
|
|
2443
|
+
pagesScanned,
|
|
2444
|
+
leadCount: leads.size,
|
|
2445
|
+
leads: Array.from(leads.values()),
|
|
2446
|
+
keepTab,
|
|
2447
|
+
tabId: keepTab ? tab.id ?? null : null
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
var LIEPIN_WALL_CHECK_SOURCE = String.raw`(() => {
|
|
2451
|
+
try {
|
|
2452
|
+
if (location.href === 'about:blank' || !location.hostname) return 'BLANK_KILL';
|
|
2453
|
+
const url = location.href;
|
|
2454
|
+
if (/liepin\.com\/(login|passport|signin)/i.test(url)) return 'LOGIN_WALL';
|
|
2455
|
+
if (/liepin\.com\/(safe|verify|check|captcha|slide)/i.test(url)) return 'CAPTCHA';
|
|
2456
|
+
const bodyText = (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 4000);
|
|
2457
|
+
if (/滑块|拖动验证|安全验证|验证码|verify you are human|请完成安全验证/.test(bodyText)) return 'CAPTCHA';
|
|
2458
|
+
if (document.querySelector('[class*="captcha"], [id*="captcha"], iframe[src*="captcha"], iframe[src*="slide"]')) return 'CAPTCHA';
|
|
2459
|
+
if (/请先登录|登录后查看|立即登录/.test(bodyText) && !document.querySelector('#resume-detail-basic-info')) return 'LOGIN_WALL';
|
|
2460
|
+
} catch (e) { /* 检测失败不阻断 */ }
|
|
2461
|
+
return null;
|
|
2462
|
+
})()`;
|
|
2463
|
+
var LIEPIN_EXTRACT_SOURCE = String.raw`(async () => {
|
|
2464
|
+
const waitFor = (fn, timeout, interval) => new Promise((resolve) => {
|
|
2465
|
+
const t0 = Date.now();
|
|
2466
|
+
const tick = () => {
|
|
2467
|
+
let v = null;
|
|
2468
|
+
try { v = fn(); } catch (e) {}
|
|
2469
|
+
if (v) return resolve(v);
|
|
2470
|
+
if (Date.now() - t0 > (timeout || 8000)) return resolve(null);
|
|
2471
|
+
setTimeout(tick, interval || 250);
|
|
2472
|
+
};
|
|
2473
|
+
tick();
|
|
2474
|
+
});
|
|
2475
|
+
if (document.readyState !== 'complete') {
|
|
2476
|
+
await waitFor(() => document.readyState === 'complete', 12000, 200);
|
|
2477
|
+
}
|
|
2478
|
+
const clean = (s) => {
|
|
2479
|
+
if (s == null) return null;
|
|
2480
|
+
const t = String(s).replace(/\s+/g, ' ').trim();
|
|
2481
|
+
return t || null;
|
|
2482
|
+
};
|
|
2483
|
+
const txt = (el) => clean(el ? el.textContent : null);
|
|
2484
|
+
const first = (sel, root) => (root || document).querySelector(sel);
|
|
2485
|
+
const attr = (el, name) => (el ? (el.getAttribute(name) || null) : null);
|
|
2486
|
+
const abs = (href) => {
|
|
2487
|
+
if (!href) return null;
|
|
2488
|
+
try { return new URL(href, location.href).toString(); } catch (e) { return href; }
|
|
2489
|
+
};
|
|
2490
|
+
/* sep-info 用 <i></i> 作分隔符:转 | 后按段切分 */
|
|
2491
|
+
const splitByI = (el) => {
|
|
2492
|
+
if (!el) return [];
|
|
2493
|
+
const html = el.innerHTML || '';
|
|
2494
|
+
const holder = document.createElement('div');
|
|
2495
|
+
return html.replace(/<i[^>]*>\s*<\/i>/g, '|').split('|').map((seg) => {
|
|
2496
|
+
holder.innerHTML = seg;
|
|
2497
|
+
return clean(holder.textContent);
|
|
2498
|
+
}).filter(Boolean);
|
|
2499
|
+
};
|
|
2500
|
+
/* (2012.07 - 至今, 14年2个月) → { start, end, duration } */
|
|
2501
|
+
const parsePeriod = (text) => {
|
|
2502
|
+
const m = (text || '').match(/[((]\s*([^))]*?)\s*[))]/);
|
|
2503
|
+
if (!m) return { start: null, end: null, duration: null };
|
|
2504
|
+
const parts = m[1].split(',').map((s) => clean(s)).filter(Boolean);
|
|
2505
|
+
const range = (parts[0] || '').split('-').map((s) => clean(s));
|
|
2506
|
+
return { start: range[0] || null, end: range[1] || null, duration: parts[1] || null };
|
|
2507
|
+
};
|
|
2508
|
+
/* rd-info-col 映射:每个 col 是 title+cont 对(一行可能两个 col) */
|
|
2509
|
+
const readInfoCols = (root) => {
|
|
2510
|
+
const out = {};
|
|
2511
|
+
(root || document).querySelectorAll('.rd-info-col').forEach((col) => {
|
|
2512
|
+
const title = txt(first('.rd-info-col-title', col));
|
|
2513
|
+
const cont = txt(first('.rd-info-col-cont', col));
|
|
2514
|
+
if (title && cont) out[title.replace(/[::]\s*$/, '')] = cont;
|
|
2515
|
+
});
|
|
2516
|
+
return out;
|
|
2517
|
+
};
|
|
2518
|
+
const resIdFrom = (href, scm) => {
|
|
2519
|
+
if (scm) {
|
|
2520
|
+
const m = scm.match(/cid=([^&]+)/);
|
|
2521
|
+
if (m) return m[1];
|
|
2522
|
+
}
|
|
2523
|
+
if (href) {
|
|
2524
|
+
const m = href.match(/res_id_encode=([^&]+)/);
|
|
2525
|
+
if (m) return m[1];
|
|
2526
|
+
}
|
|
2527
|
+
return null;
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
const url = location.href;
|
|
2531
|
+
const canonical = (document.querySelector('link[rel="canonical"]') || {}).href || url.split('?')[0];
|
|
2532
|
+
const entities = [];
|
|
2533
|
+
const links = { talent: [], company: [], job: [] };
|
|
2534
|
+
|
|
2535
|
+
if (document.querySelector('#resume-detail-basic-info')) {
|
|
2536
|
+
const basic = first('#resume-detail-basic-info');
|
|
2537
|
+
const sepInfos = basic.querySelectorAll('.basic-cont .sep-info');
|
|
2538
|
+
const parts0 = sepInfos.length > 0 ? splitByI(sepInfos[0]) : [];
|
|
2539
|
+
const parts1 = sepInfos.length > 1 ? splitByI(sepInfos[1]) : [];
|
|
2540
|
+
const joined0 = parts0.join(' ');
|
|
2541
|
+
const age = joined0.match(/(\d+)\s*岁/);
|
|
2542
|
+
const workYears = joined0.match(/工作\s*(\d+)\s*年/);
|
|
2543
|
+
const name = txt(first('.name-box .name', basic));
|
|
2544
|
+
const statusBox = first('.res-status-box', basic);
|
|
2545
|
+
const contactTimeRaw = txt(statusBox);
|
|
2546
|
+
const scm = attr(document.querySelector('[data-tlg-scm]'), 'data-tlg-scm');
|
|
2547
|
+
|
|
2548
|
+
const intentRoot = first('#resume-detail-job-exp-info');
|
|
2549
|
+
const workExperiences = [];
|
|
2550
|
+
document.querySelectorAll('#resume-detail-work-info .rd-work-item-cont').forEach((item) => {
|
|
2551
|
+
const cols = readInfoCols(item);
|
|
2552
|
+
workExperiences.push({
|
|
2553
|
+
company: attr(first('.rd-work-comp h5', item), 'title') || txt(first('.rd-work-comp h5', item)),
|
|
2554
|
+
...parsePeriod(txt(first('.rd-work-time', item))),
|
|
2555
|
+
position: txt(first('.job-name', item)),
|
|
2556
|
+
department: cols['所在部门'] || null,
|
|
2557
|
+
location: cols['工作地点'] || null,
|
|
2558
|
+
subordinates: cols['下属人数'] != null ? Number(cols['下属人数']) : null,
|
|
2559
|
+
reportsTo: cols['汇报对象'] || null,
|
|
2560
|
+
responsibilities: cols['职责业绩'] || null,
|
|
2561
|
+
});
|
|
2562
|
+
});
|
|
2563
|
+
const projects = [];
|
|
2564
|
+
document.querySelectorAll('#resume-detail-project-info .rd-project-item-cont').forEach((item) => {
|
|
2565
|
+
const cols = readInfoCols(item);
|
|
2566
|
+
projects.push({
|
|
2567
|
+
name: attr(first('.rd-project-name h5', item), 'title') || txt(first('.rd-project-name h5', item)),
|
|
2568
|
+
...parsePeriod(txt(first('.rd-project-time', item))),
|
|
2569
|
+
role: cols['项目职务'] || null,
|
|
2570
|
+
company: cols['所在公司'] || null,
|
|
2571
|
+
description: cols['项目描述'] || null,
|
|
2572
|
+
duties: cols['项目职责'] || null,
|
|
2573
|
+
});
|
|
2574
|
+
});
|
|
2575
|
+
const education = [];
|
|
2576
|
+
document.querySelectorAll('#resume-detail-edu-info .rd-edu-info-item').forEach((item) => {
|
|
2577
|
+
education.push({
|
|
2578
|
+
school: attr(first('.school-name', item), 'title') || txt(first('.school-name', item)),
|
|
2579
|
+
major: attr(first('.school-special', item), 'title') || txt(first('.school-special', item)),
|
|
2580
|
+
degree: attr(first('.school-degree', item), 'title') || txt(first('.school-degree', item)),
|
|
2581
|
+
period: txt(first('.school-time', item)),
|
|
2582
|
+
tags: Array.from(item.querySelectorAll('.edu-school-tags .tag-item')).map((t) => txt(t)).filter(Boolean),
|
|
2583
|
+
});
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
entities.push({
|
|
2587
|
+
entityType: 'talent',
|
|
2588
|
+
platform: 'liepin',
|
|
2589
|
+
resId: resIdFrom(url, scm),
|
|
2590
|
+
name,
|
|
2591
|
+
nameMasked: name ? /\*/.test(name) : undefined,
|
|
2592
|
+
statusTag: txt(first('.user-status-tag', basic)),
|
|
2593
|
+
gender: parts0.find((p) => /^(男|女)$/.test(p)) || null,
|
|
2594
|
+
age: age ? Number(age[1]) : null,
|
|
2595
|
+
city: parts0.find((p) => /^(北京|上海|广州|深圳|天津|重庆|杭州|南京|苏州|成都|武汉|西安|[一-龥]{2,6})$/.test(p) && !/^(男|女|保密)$/.test(p) && !/岁|年/.test(p)) || null,
|
|
2596
|
+
degree: parts0.find((p) => /(博士|硕士|MBA|本科|大专|专科|高中|中专|初中)/.test(p)) || null,
|
|
2597
|
+
workYears: workYears ? Number(workYears[1]) : null,
|
|
2598
|
+
currentCompany: parts0.length ? parts0[parts0.length - 1] : null,
|
|
2599
|
+
currentCompanyMasked: parts0.includes('保密') ? true : undefined,
|
|
2600
|
+
currentPosition: parts1.length ? parts1[0] : null,
|
|
2601
|
+
contactTime: contactTimeRaw ? contactTimeRaw.replace(/^方便联系时间[::]\s*/, '') : null,
|
|
2602
|
+
photoUrl: (() => { const img = first('.logo-box img', basic); return img ? abs(img.getAttribute('src')) : null; })(),
|
|
2603
|
+
intent: intentRoot ? {
|
|
2604
|
+
position: attr(first('.tabs .title', intentRoot), 'title') || txt(first('.tabs .title', intentRoot)),
|
|
2605
|
+
salary: txt(first('.tabs .salary', intentRoot)),
|
|
2606
|
+
city: txt(first('.dqname-wrap', intentRoot)),
|
|
2607
|
+
industries: Array.from(intentRoot.querySelectorAll('.lebels-wrap span')).map((s) => txt(s)).filter(Boolean),
|
|
2608
|
+
} : null,
|
|
2609
|
+
workExperiences,
|
|
2610
|
+
projects,
|
|
2611
|
+
education,
|
|
2612
|
+
languages: Array.from(document.querySelectorAll('#resume-detail-lang-info .rd-lang-item .lang-name')).map((el) => txt(el)).filter(Boolean),
|
|
2613
|
+
selfEvaluation: txt(first('#resume-detail-self-eva-info .resume-detail-template-cont')),
|
|
2614
|
+
statement: txt(first('.c-detail-statement')),
|
|
2615
|
+
hasAntiScrapeWatermark: document.querySelectorAll('.cozue').length > 0,
|
|
2616
|
+
url: canonical,
|
|
2617
|
+
source: 'liepin',
|
|
2618
|
+
});
|
|
2619
|
+
} else if (document.querySelector('.talent-resume-card')) {
|
|
2620
|
+
/* 人才管理页/搜索结果列表:.talent-resume-card 卡片(事实源 §7) */
|
|
2621
|
+
document.querySelectorAll('.talent-resume-card').forEach((card) => {
|
|
2622
|
+
const a = first('a.resume-title', card) || first('a[href*="showresumedetail"]', card);
|
|
2623
|
+
const href = a ? abs(attr(a, 'href')) : null;
|
|
2624
|
+
const scm = attr(card, 'data-tlg-scm') || '';
|
|
2625
|
+
const titleLine = attr(a, 'title') || txt(a);
|
|
2626
|
+
const eduItem = Array.from(card.querySelectorAll('.info-item')).find((el) => /最高学历/.test(el.textContent || ''));
|
|
2627
|
+
const workLines = [];
|
|
2628
|
+
card.querySelectorAll('.work-info .info-cont p, .info-cont p').forEach((p) => {
|
|
2629
|
+
const comp = first('.comp', p);
|
|
2630
|
+
const pos = first('.rw-title', p);
|
|
2631
|
+
if (comp || pos) {
|
|
2632
|
+
workLines.push({
|
|
2633
|
+
period: txt(p.querySelector('i')),
|
|
2634
|
+
company: attr(comp, 'title') || txt(comp),
|
|
2635
|
+
position: attr(pos, 'title') || txt(pos),
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
});
|
|
2639
|
+
const acquired = txt(first('.right-extension .temp-side', card));
|
|
2640
|
+
const lead = {
|
|
2641
|
+
url: href,
|
|
2642
|
+
name: titleLine ? (titleLine.split(/\s*[||]\s*/)[0] || titleLine) : null,
|
|
2643
|
+
titleLine,
|
|
2644
|
+
resId: resIdFrom(href, scm),
|
|
2645
|
+
education: eduItem ? clean((eduItem.textContent || '').replace(/^最高学历[::]?/, '')) : null,
|
|
2646
|
+
recentWork: workLines,
|
|
2647
|
+
acquiredAt: acquired ? acquired.replace(/^获取时间[::]?/, '') : null,
|
|
2648
|
+
source: 'liepin',
|
|
2649
|
+
};
|
|
2650
|
+
if (lead.url && !links.talent.some((l) => l.url === lead.url)) links.talent.push(lead);
|
|
2651
|
+
});
|
|
2652
|
+
} else {
|
|
2653
|
+
/* 兜底:任何含简历详情链接的页面,提取链接名单 */
|
|
2654
|
+
document.querySelectorAll('a[href*="showresumedetail"]').forEach((a) => {
|
|
2655
|
+
const href = abs(attr(a, 'href'));
|
|
2656
|
+
if (href && !links.talent.some((l) => l.url === href)) {
|
|
2657
|
+
links.talent.push({ url: href, name: txt(a), resId: resIdFrom(href, ''), source: 'liepin' });
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
const pageClass = document.querySelector('#resume-detail-basic-info')
|
|
2663
|
+
? 'talent'
|
|
2664
|
+
: (document.querySelector('.talent-resume-card') ? 'resume-list' : (links.talent.length ? 'list' : 'unknown'));
|
|
2665
|
+
return {
|
|
2666
|
+
url,
|
|
2667
|
+
canonical,
|
|
2668
|
+
pageClass,
|
|
2669
|
+
title: clean(document.title),
|
|
2670
|
+
extractedAt: new Date().toISOString(),
|
|
2671
|
+
entities,
|
|
2672
|
+
links,
|
|
2673
|
+
};
|
|
2674
|
+
})()`;
|
|
2675
|
+
var LIEPIN_FILL_AND_SEARCH_SOURCE = String.raw`(async (P) => {
|
|
2676
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2677
|
+
const clean = (s) => (s == null ? null : String(s).replace(/\s+/g, ' ').trim() || null);
|
|
2678
|
+
const delay = P.fieldDelayMs && P.fieldDelayMs >= 0 ? P.fieldDelayMs : 1500;
|
|
2679
|
+
const applied = [];
|
|
2680
|
+
const errors = [];
|
|
2681
|
+
|
|
2682
|
+
/* React 受控输入兼容:native setter + input/change 事件 */
|
|
2683
|
+
const setNativeValue = (el, value) => {
|
|
2684
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
2685
|
+
el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
|
|
2686
|
+
'value',
|
|
2687
|
+
).set;
|
|
2688
|
+
setter.call(el, value);
|
|
2689
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2690
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2691
|
+
};
|
|
2692
|
+
const setInput = (sel, value) => {
|
|
2693
|
+
const el = document.querySelector(sel);
|
|
2694
|
+
if (!el) { errors.push('missing_input:' + sel); return false; }
|
|
2695
|
+
if (el.disabled) { errors.push('disabled_input:' + sel); return false; }
|
|
2696
|
+
el.focus();
|
|
2697
|
+
setNativeValue(el, String(value));
|
|
2698
|
+
el.blur();
|
|
2699
|
+
return true;
|
|
2700
|
+
};
|
|
2701
|
+
/* 在某个容器内点击包含指定文本的按钮(如"确 定"/"搜 索") */
|
|
2702
|
+
const clickButtonIn = (rootSel, text) => {
|
|
2703
|
+
const root = rootSel ? document.querySelector(rootSel) : document;
|
|
2704
|
+
if (!root) { errors.push('missing_root:' + rootSel); return false; }
|
|
2705
|
+
const btn = Array.from(root.querySelectorAll('button, span, a')).find((b) => {
|
|
2706
|
+
const t = clean(b.textContent);
|
|
2707
|
+
return t && t.replace(/\s+/g, '') === text;
|
|
2708
|
+
});
|
|
2709
|
+
if (!btn) { errors.push('missing_button:' + text + '@' + rootSel); return false; }
|
|
2710
|
+
btn.click();
|
|
2711
|
+
return true;
|
|
2712
|
+
};
|
|
2713
|
+
/* 点击条件 tag(容器可带 #N 出现序号:如 '.sfilter-city#2' = 第 2 个 sfilter-city,即期望城市) */
|
|
2714
|
+
const clickTag = (containerSel, text) => {
|
|
2715
|
+
let sel = containerSel;
|
|
2716
|
+
let idx = 0;
|
|
2717
|
+
const m = String(containerSel).match(/^(.*)#(\d+)$/);
|
|
2718
|
+
if (m) { sel = m[1]; idx = parseInt(m[2], 10) - 1; }
|
|
2719
|
+
const containers = document.querySelectorAll(sel);
|
|
2720
|
+
const container = containers.length > idx ? containers[idx] : null;
|
|
2721
|
+
if (!container) { errors.push('missing_container:' + containerSel); return false; }
|
|
2722
|
+
const tag = Array.from(container.querySelectorAll('label.tag-item, .tag-item, span')).find((el) => {
|
|
2723
|
+
const t = clean(el.textContent);
|
|
2724
|
+
return t && (t === text || t.replace(/\s+/g, '') === String(text).replace(/\s+/g, ''));
|
|
2725
|
+
});
|
|
2726
|
+
if (!tag) { errors.push('missing_tag:' + text + '@' + containerSel); return false; }
|
|
2727
|
+
tag.click();
|
|
2728
|
+
return true;
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
try {
|
|
2732
|
+
/* 1) 关键词:顶部自动补全输入框(与 .search-btn 同容器,placeholder 搜职位/公司/行业等) */
|
|
2733
|
+
if (P.keyword) {
|
|
2734
|
+
const okInput = (() => {
|
|
2735
|
+
const box = document.querySelector('.auto-input-wrap-v3') || document.querySelector('.ant-select-auto-complete');
|
|
2736
|
+
const el = box ? box.querySelector('input.ant-select-selection-search-input') : null;
|
|
2737
|
+
if (!el) { errors.push('missing_input:keyword'); return false; }
|
|
2738
|
+
el.focus();
|
|
2739
|
+
setNativeValue(el, String(P.keyword));
|
|
2740
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2741
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2742
|
+
el.blur();
|
|
2743
|
+
return true;
|
|
2744
|
+
})();
|
|
2745
|
+
if (okInput) applied.push('keyword');
|
|
2746
|
+
await sleep(delay);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
/* 2) tag 类条件(每次点击后 human-paced) */
|
|
2750
|
+
const tagSteps = [];
|
|
2751
|
+
if (P.city) tagSteps.push(['.sfilter-city', P.city, 'city']);
|
|
2752
|
+
if (P.expectCity) tagSteps.push(['.sfilter-city#2', P.expectCity, 'expectCity']);
|
|
2753
|
+
if (P.workYear) tagSteps.push(['.sfilter-work-year', P.workYear, 'workYear']);
|
|
2754
|
+
if (P.education) tagSteps.push(['.sfilter-edu', P.education, 'education']);
|
|
2755
|
+
if (P.schoolTag) tagSteps.push(['.sfilter-additional', P.schoolTag, 'schoolTag']);
|
|
2756
|
+
if (P.language) tagSteps.push(['.sfilter-lang', P.language, 'language']);
|
|
2757
|
+
for (const [sel, text, key] of tagSteps) {
|
|
2758
|
+
if (clickTag(sel, text)) applied.push(key);
|
|
2759
|
+
await sleep(delay);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
/* 3) 区间输入类条件:填输入 + 点容器内"确定" */
|
|
2763
|
+
const rangeSteps = [];
|
|
2764
|
+
if (P.ageMin != null || P.ageMax != null) {
|
|
2765
|
+
rangeSteps.push(['#ageLow', P.ageMin, '#ageHigh', P.ageMax, '.age-shadow-box', 'age']);
|
|
2766
|
+
}
|
|
2767
|
+
if (P.wantSalaryMin != null || P.wantSalaryMax != null) {
|
|
2768
|
+
rangeSteps.push(['#wantSalaryLow', P.wantSalaryMin, '#wantSalaryHigh', P.wantSalaryMax, '.sfilter-salary .search-resume-shadow-box', 'wantSalary']);
|
|
2769
|
+
}
|
|
2770
|
+
if (P.nowSalaryMin != null || P.nowSalaryMax != null) {
|
|
2771
|
+
rangeSteps.push(['#nowSalaryLow', P.nowSalaryMin, '#nowSalaryHigh', P.nowSalaryMax, null, 'nowSalary']);
|
|
2772
|
+
}
|
|
2773
|
+
for (const [loSel, lo, hiSel, hi, boxSel, key] of rangeSteps) {
|
|
2774
|
+
let ok = true;
|
|
2775
|
+
if (lo != null) ok = setInput(loSel, lo) && ok;
|
|
2776
|
+
await sleep(Math.max(600, Math.floor(delay / 2)));
|
|
2777
|
+
if (hi != null) ok = setInput(hiSel, hi) && ok;
|
|
2778
|
+
await sleep(Math.max(600, Math.floor(delay / 2)));
|
|
2779
|
+
/* 确定按钮:优先容器内,其次全局匹配(目前年薪容器无专属 class) */
|
|
2780
|
+
if (ok && !clickButtonIn(boxSel, '确定')) {
|
|
2781
|
+
ok = clickButtonIn(null, '确定');
|
|
2782
|
+
}
|
|
2783
|
+
if (ok) applied.push(key);
|
|
2784
|
+
await sleep(delay);
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
/* 4) 工作年限自定义:先点"自定义"tag 解锁输入,再填 + 确定 */
|
|
2788
|
+
if (P.workYearsMin != null || P.workYearsMax != null) {
|
|
2789
|
+
if (clickTag('.sfilter-work-year', '自定义')) {
|
|
2790
|
+
applied.push('workYearCustom');
|
|
2791
|
+
await sleep(delay);
|
|
2792
|
+
let ok = true;
|
|
2793
|
+
if (P.workYearsMin != null) ok = setInput('#workYearsLow', P.workYearsMin) && ok;
|
|
2794
|
+
await sleep(Math.max(600, Math.floor(delay / 2)));
|
|
2795
|
+
if (P.workYearsMax != null) ok = setInput('#workYearsHigh', P.workYearsMax) && ok;
|
|
2796
|
+
await sleep(Math.max(600, Math.floor(delay / 2)));
|
|
2797
|
+
if (ok && clickButtonIn('.year-custom-box', '确定')) applied.push('workYears');
|
|
2798
|
+
}
|
|
2799
|
+
await sleep(delay);
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
/* 5) 复选框:海外工作经验 / 管理经验 */
|
|
2803
|
+
for (const [name, key] of [['workAbroad', 'workAbroad'], ['managerial', 'managerial']]) {
|
|
2804
|
+
if (P[key]) {
|
|
2805
|
+
const input = document.querySelector('input[name="' + name + '"]');
|
|
2806
|
+
if (input && !input.checked) {
|
|
2807
|
+
const wrapper = input.closest('label') || input;
|
|
2808
|
+
wrapper.click();
|
|
2809
|
+
applied.push(key);
|
|
2810
|
+
await sleep(delay);
|
|
2811
|
+
} else if (!input) {
|
|
2812
|
+
errors.push('missing_checkbox:' + name);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/* 6) 提交搜索前停顿(类人工审阅),点击顶部"搜 索" */
|
|
2818
|
+
await sleep(Math.max(1500, delay));
|
|
2819
|
+
const searchBtn = document.querySelector('button.search-btn');
|
|
2820
|
+
if (!searchBtn) { errors.push('missing_button:search'); return { applied, errors, submitted: false }; }
|
|
2821
|
+
searchBtn.click();
|
|
2822
|
+
applied.push('submit');
|
|
2823
|
+
return { applied, errors, submitted: true };
|
|
2824
|
+
} catch (e) {
|
|
2825
|
+
errors.push('fill_exception:' + String((e && e.message) || e));
|
|
2826
|
+
return { applied, errors, submitted: false };
|
|
2827
|
+
}
|
|
2828
|
+
})`;
|
|
2829
|
+
var LIEPIN_RESULT_WAIT_SOURCE = String.raw`(() => {
|
|
2830
|
+
return new Promise((resolve) => {
|
|
2831
|
+
const t0 = Date.now();
|
|
2832
|
+
const tick = () => {
|
|
2833
|
+
let hit = false;
|
|
2834
|
+
try {
|
|
2835
|
+
hit = !!(document.querySelector('.talent-resume-card') || document.querySelector('a[href*="showresumedetail"]'));
|
|
2836
|
+
if (!hit && /没有找到|暂无相关|0\s*条结果|共\s*0\s*条/.test(document.body ? document.body.innerText : '')) return resolve('empty');
|
|
2837
|
+
} catch (e) {}
|
|
2838
|
+
if (hit) return resolve(true);
|
|
2839
|
+
if (Date.now() - t0 > 20000) return resolve(false);
|
|
2840
|
+
setTimeout(tick, 400);
|
|
2841
|
+
};
|
|
2842
|
+
tick();
|
|
2843
|
+
});
|
|
2844
|
+
})()`;
|
|
2845
|
+
var LIEPIN_NEXT_CLICK_SOURCE = String.raw`(async () => {
|
|
2846
|
+
const next = document.querySelector('.ant-pagination-next button')
|
|
2847
|
+
|| document.querySelector('.ant-pagination-next')
|
|
2848
|
+
|| document.querySelector('button[aria-label="下一页"]');
|
|
2849
|
+
if (!next || next.getAttribute('aria-disabled') === 'true' || next.disabled) return false;
|
|
2850
|
+
next.click();
|
|
2851
|
+
return true;
|
|
2852
|
+
})()`;
|
|
2853
|
+
function liepinBlockedCode(blocked) {
|
|
2854
|
+
if (blocked === "LOGIN_WALL") return "LIE_PIN_LOGIN_WALL";
|
|
2855
|
+
if (blocked === "BLANK_KILL") return "LIE_PIN_BLANK_KILL";
|
|
2856
|
+
return "LIE_PIN_CAPTCHA";
|
|
2857
|
+
}
|
|
2858
|
+
async function runLiepinSearch(args, log, ctx) {
|
|
2859
|
+
const keyword = typeof args.keyword === "string" ? args.keyword.trim() : "";
|
|
2860
|
+
if (!keyword) throw new Error("liepin_search_requires_keyword");
|
|
2861
|
+
const maxPages = Math.min(Math.max(Number(args.maxPages) || 1, 1), 5);
|
|
2862
|
+
const keepTab = args.keepTab === true;
|
|
2863
|
+
const fieldDelayMs = typeof args.delayMs === "number" ? Number(args.delayMs) : 1500;
|
|
2864
|
+
const fillParams = { keyword, fieldDelayMs };
|
|
2865
|
+
for (const k of [
|
|
2866
|
+
"city",
|
|
2867
|
+
"expectCity",
|
|
2868
|
+
"workYear",
|
|
2869
|
+
"education",
|
|
2870
|
+
"schoolTag",
|
|
2871
|
+
"language",
|
|
2872
|
+
"ageMin",
|
|
2873
|
+
"ageMax",
|
|
2874
|
+
"wantSalaryMin",
|
|
2875
|
+
"wantSalaryMax",
|
|
2876
|
+
"nowSalaryMin",
|
|
2877
|
+
"nowSalaryMax",
|
|
2878
|
+
"workYearsMin",
|
|
2879
|
+
"workYearsMax",
|
|
2880
|
+
"workAbroad",
|
|
2881
|
+
"managerial"
|
|
2882
|
+
]) {
|
|
2883
|
+
if (args[k] !== void 0) fillParams[k] = args[k];
|
|
2884
|
+
}
|
|
2885
|
+
const searchUrl = "https://h.liepin.com/search/getConditionItem";
|
|
2886
|
+
const tab = await chrome.tabs.create({ url: searchUrl, active: false });
|
|
2887
|
+
log("info", "hm.liepin.search tab created", { tabId: tab.id, url: searchUrl, maxPages, keyword });
|
|
2888
|
+
if (ctx.sessionId && !ctx.explicitTab && tab.id) {
|
|
2889
|
+
addTabToSessionGroup(ctx.sessionId, ctx.groupTitle, tab).catch(
|
|
2890
|
+
(err3) => log("warn", "hm.liepin.search addTabToSessionGroup failed", { error: err3.message })
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
const leads = /* @__PURE__ */ new Map();
|
|
2894
|
+
const applied = [];
|
|
2895
|
+
const fillErrors = [];
|
|
2896
|
+
let pagesScanned = 0;
|
|
2897
|
+
const assertNotBlocked = async (phase) => {
|
|
2898
|
+
if (tab.id == null) return;
|
|
2899
|
+
const t = await chrome.tabs.get(tab.id).catch(() => null);
|
|
2900
|
+
if (!t) throw new Error(`tab closed while ${phase}`);
|
|
2901
|
+
if (!t.url || t.url === "about:blank") {
|
|
2902
|
+
throw new ControlBlockedError("LIE_PIN_BLANK_KILL", `liepin search aborted at ${phase}: page killed to about:blank (secscan)`);
|
|
2903
|
+
}
|
|
2904
|
+
const blocked = await cdpEvaluate(tab.id, LIEPIN_WALL_CHECK_SOURCE).catch(() => null);
|
|
2905
|
+
if (blocked) {
|
|
2906
|
+
const code = liepinBlockedCode(blocked);
|
|
2907
|
+
log("warn", "hm.liepin.search blocked by liepin risk control", { blocked, code, phase });
|
|
2908
|
+
throw new ControlBlockedError(code, `liepin search aborted at ${phase}: ${blocked}`);
|
|
2909
|
+
}
|
|
2910
|
+
};
|
|
2911
|
+
try {
|
|
2912
|
+
if (tab.id == null) throw new Error("failed_to_create_tab");
|
|
2913
|
+
await waitTabLoaded(tab.id, 3e4);
|
|
2914
|
+
await sleep(1500);
|
|
2915
|
+
await assertNotBlocked("after-load");
|
|
2916
|
+
const fillExpr = "(() => { const P = " + JSON.stringify(fillParams) + "; return (" + LIEPIN_FILL_AND_SEARCH_SOURCE + ")(P); })()";
|
|
2917
|
+
const fillResult = await cdpEvaluate(tab.id, fillExpr, 6e4);
|
|
2918
|
+
applied.push(...fillResult?.applied ?? []);
|
|
2919
|
+
fillErrors.push(...fillResult?.errors ?? []);
|
|
2920
|
+
if (!fillResult?.submitted) {
|
|
2921
|
+
log("warn", "hm.liepin.search fill did not submit", { fillErrors });
|
|
2922
|
+
throw new Error("liepin_search_fill_failed: " + fillErrors.join(";"));
|
|
2923
|
+
}
|
|
2924
|
+
log("info", "hm.liepin.search form submitted", { applied, fillErrors });
|
|
2925
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
2926
|
+
if (tab.id == null) break;
|
|
2927
|
+
const waited = await cdpEvaluate(tab.id, LIEPIN_RESULT_WAIT_SOURCE, 3e4).catch(() => false);
|
|
2928
|
+
await sleep(1200);
|
|
2929
|
+
await assertNotBlocked(`page-${page}`);
|
|
2930
|
+
if (waited === "empty") {
|
|
2931
|
+
log("info", "hm.liepin.search empty result", { page });
|
|
2932
|
+
break;
|
|
2933
|
+
}
|
|
2934
|
+
const extracted = await cdpEvaluate(tab.id, LIEPIN_EXTRACT_SOURCE, 3e4);
|
|
2935
|
+
const pageLeads = extracted?.links?.talent || [];
|
|
2936
|
+
for (const l of pageLeads) {
|
|
2937
|
+
if (l.url && !leads.has(l.url)) {
|
|
2938
|
+
leads.set(l.url, {
|
|
2939
|
+
url: l.url,
|
|
2940
|
+
name: l.name ?? null,
|
|
2941
|
+
titleLine: l.titleLine ?? null,
|
|
2942
|
+
resId: l.resId ?? null,
|
|
2943
|
+
page,
|
|
2944
|
+
source: "liepin"
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
pagesScanned = page;
|
|
2949
|
+
log("info", "hm.liepin.search page extracted", { page, pageLeads: pageLeads.length, total: leads.size });
|
|
2950
|
+
if (page >= maxPages) break;
|
|
2951
|
+
const clicked = await cdpEvaluate(tab.id, LIEPIN_NEXT_CLICK_SOURCE).catch(() => false);
|
|
2952
|
+
if (!clicked) break;
|
|
2953
|
+
await sleep(3e3 + Math.random() * 2e3);
|
|
2954
|
+
await assertNotBlocked(`page-${page}-next`);
|
|
2955
|
+
}
|
|
2956
|
+
} finally {
|
|
2957
|
+
if (!keepTab && tab.id != null) {
|
|
2958
|
+
await closeTabGracefully(tab.id);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
return {
|
|
2962
|
+
keyword,
|
|
2963
|
+
searchUrl,
|
|
2964
|
+
pagesScanned,
|
|
2965
|
+
leadCount: leads.size,
|
|
2966
|
+
leads: Array.from(leads.values()),
|
|
2967
|
+
applied,
|
|
2968
|
+
fillErrors,
|
|
2969
|
+
keepTab,
|
|
2970
|
+
tabId: keepTab ? tab.id ?? null : null
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2216
2973
|
var HIREDCHINA_EXTRACT_SOURCE = String.raw`(async () => {
|
|
2217
2974
|
const waitFor = (fn, timeout, interval) => new Promise((resolve) => {
|
|
2218
2975
|
const t0 = Date.now();
|
|
@@ -2436,6 +3193,14 @@ async function handleCookiesCommand(args) {
|
|
|
2436
3193
|
}
|
|
2437
3194
|
throw new Error(`cookies_unknown_action: ${action}`);
|
|
2438
3195
|
}
|
|
3196
|
+
async function closeTabGracefully(tabId) {
|
|
3197
|
+
await detachIfAttached(tabId);
|
|
3198
|
+
await Promise.race([
|
|
3199
|
+
chrome.tabs.remove(tabId),
|
|
3200
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("tab_remove_timeout")), 5e3))
|
|
3201
|
+
]).catch(() => {
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
2439
3204
|
async function handleTabCommand(command, args, log) {
|
|
2440
3205
|
const tabArg = args.tab ?? args.targetId ?? args.page;
|
|
2441
3206
|
switch (command) {
|
|
@@ -2467,7 +3232,7 @@ async function handleTabCommand(command, args, log) {
|
|
|
2467
3232
|
}
|
|
2468
3233
|
case "cp.tab.close": {
|
|
2469
3234
|
if (!tabArg) throw new Error("tab_close_requires_tab_id");
|
|
2470
|
-
await
|
|
3235
|
+
await closeTabGracefully(Number(tabArg));
|
|
2471
3236
|
return { closed: true, page: String(tabArg) };
|
|
2472
3237
|
}
|
|
2473
3238
|
case "cp.tab.bind": {
|
|
@@ -2650,7 +3415,7 @@ function maybeDiscover(url, title) {
|
|
|
2650
3415
|
});
|
|
2651
3416
|
}
|
|
2652
3417
|
|
|
2653
|
-
// ../../node_modules/.pnpm/@chromepilot+logger-kit@0.1.
|
|
3418
|
+
// ../../node_modules/.pnpm/@chromepilot+logger-kit@0.1.4_@nestjs+common@10.4.22_class-transformer@0.5.1_class-validator@_legcruqwso5xb42h6lrzoll4b4/node_modules/@chromepilot/logger-kit/dist/client/logger.js
|
|
2654
3419
|
function stringifyArg(arg) {
|
|
2655
3420
|
if (typeof arg === "string")
|
|
2656
3421
|
return arg;
|
|
@@ -2802,7 +3567,7 @@ var Logger = class {
|
|
|
2802
3567
|
}
|
|
2803
3568
|
};
|
|
2804
3569
|
|
|
2805
|
-
// ../../node_modules/.pnpm/@chromepilot+logger-kit@0.1.
|
|
3570
|
+
// ../../node_modules/.pnpm/@chromepilot+logger-kit@0.1.4_@nestjs+common@10.4.22_class-transformer@0.5.1_class-validator@_legcruqwso5xb42h6lrzoll4b4/node_modules/@chromepilot/logger-kit/dist/client/transports/http-transport.js
|
|
2806
3571
|
async function createHttpTransport(opts) {
|
|
2807
3572
|
const serverUrl = opts.serverUrl.replace(/\/$/, "");
|
|
2808
3573
|
return async (logs) => {
|
|
@@ -2989,36 +3754,58 @@ createBackgroundLogForwarder({
|
|
|
2989
3754
|
}).then((f) => {
|
|
2990
3755
|
logForwarder = f;
|
|
2991
3756
|
});
|
|
2992
|
-
var
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
const item = sendQueue.shift();
|
|
3757
|
+
var OFFSCREEN_URL = "offscreen/offscreen.html";
|
|
3758
|
+
function sendToOffscreen(raw) {
|
|
3759
|
+
chrome.runtime.sendMessage({ type: "offscreen:send", raw }).catch(() => {
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
function sendInitToOffscreen() {
|
|
3763
|
+
const manifest = chrome.runtime.getManifest();
|
|
3764
|
+
chrome.runtime.sendMessage({ type: "offscreen:init", extensionId: chrome.runtime.id, version: manifest.version }).catch(() => {
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
async function ensureOffscreen() {
|
|
3768
|
+
try {
|
|
3769
|
+
const contexts = await chrome.runtime.getContexts({
|
|
3770
|
+
contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT]
|
|
3771
|
+
});
|
|
3772
|
+
if (contexts.length > 0) {
|
|
3773
|
+
sendInitToOffscreen();
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3012
3776
|
try {
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3777
|
+
await chrome.offscreen.createDocument({
|
|
3778
|
+
url: OFFSCREEN_URL,
|
|
3779
|
+
// D1: WEBSOCKET 枚举从未在正式 Chrome 落地(152 实测抛枚举非法),必须用 WORKERS;
|
|
3780
|
+
// @types/chrome 0.0.268 的 Reason 枚举同样没有 WEBSOCKET 成员,断言方向一致
|
|
3781
|
+
reasons: ["WORKERS"],
|
|
3782
|
+
justification: "hunter-mate \u9700\u4E0E\u672C\u5730 server \u4FDD\u6301\u5E38\u9A7B WebSocket \u8FDE\u63A5(MV3 SW \u4F1A\u88AB\u7A7A\u95F2\u56DE\u6536)"
|
|
3783
|
+
});
|
|
3784
|
+
sendInitToOffscreen();
|
|
3785
|
+
} catch (createErr) {
|
|
3786
|
+
const msg = createErr instanceof Error ? createErr.message : String(createErr);
|
|
3787
|
+
sendLog("warn", "ensureOffscreen createDocument failed, retry after closeDocument", { error: msg });
|
|
3788
|
+
try {
|
|
3789
|
+
await chrome.offscreen.closeDocument();
|
|
3790
|
+
} catch {
|
|
3791
|
+
}
|
|
3792
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3793
|
+
await chrome.offscreen.createDocument({
|
|
3794
|
+
url: OFFSCREEN_URL,
|
|
3795
|
+
reasons: ["WORKERS"],
|
|
3796
|
+
justification: "hunter-mate \u9700\u4E0E\u672C\u5730 server \u4FDD\u6301\u5E38\u9A7B WebSocket \u8FDE\u63A5(MV3 SW \u4F1A\u88AB\u7A7A\u95F2\u56DE\u6536)"
|
|
3797
|
+
});
|
|
3798
|
+
sendInitToOffscreen();
|
|
3017
3799
|
}
|
|
3800
|
+
} catch (err3) {
|
|
3801
|
+
console.error("[HunterMate] ensureOffscreen failed", err3);
|
|
3802
|
+
sendLog("error", "ensureOffscreen failed", { error: err3 instanceof Error ? err3.message : String(err3) });
|
|
3018
3803
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3804
|
+
try {
|
|
3805
|
+
const st = await chrome.runtime.sendMessage({ type: "offscreen:get-state" });
|
|
3806
|
+
state.wsConnected = !!st?.wsConnected;
|
|
3807
|
+
broadcastState();
|
|
3808
|
+
} catch {
|
|
3022
3809
|
}
|
|
3023
3810
|
}
|
|
3024
3811
|
function sendLog(level, message, meta) {
|
|
@@ -3026,14 +3813,7 @@ function sendLog(level, message, meta) {
|
|
|
3026
3813
|
backgroundLogger[level === "debug" ? "debug" : level]("extension:sendLog", { message, meta });
|
|
3027
3814
|
} catch {
|
|
3028
3815
|
}
|
|
3029
|
-
|
|
3030
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
3031
|
-
try {
|
|
3032
|
-
ws.send(JSON.stringify({ type: "log", level, message, meta }));
|
|
3033
|
-
} catch (e) {
|
|
3034
|
-
console.error("[HunterMate] sendLog failed", e);
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3816
|
+
sendToOffscreen(JSON.stringify({ type: "log", level, message, meta }));
|
|
3037
3817
|
const prefix = `[HunterMate] ${message}`;
|
|
3038
3818
|
if (level === "debug") {
|
|
3039
3819
|
console.debug(prefix, meta ?? "");
|
|
@@ -3106,99 +3886,72 @@ function broadcastState() {
|
|
|
3106
3886
|
});
|
|
3107
3887
|
});
|
|
3108
3888
|
}
|
|
3109
|
-
function
|
|
3110
|
-
if (
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
broadcastState();
|
|
3125
|
-
backgroundLogger.info("ws:connected", { extensionId: chrome.runtime.id });
|
|
3126
|
-
};
|
|
3127
|
-
socket.onclose = () => {
|
|
3128
|
-
socket = null;
|
|
3129
|
-
state.wsConnected = false;
|
|
3130
|
-
broadcastState();
|
|
3131
|
-
backgroundLogger.warn("ws:closed", { reconnectDelayMs: 5e3 });
|
|
3132
|
-
setTimeout(connect, 5e3);
|
|
3133
|
-
};
|
|
3134
|
-
socket.onerror = (err3) => {
|
|
3135
|
-
backgroundLogger.error("ws:error", { message: String(err3) });
|
|
3136
|
-
};
|
|
3137
|
-
socket.onmessage = async (event) => {
|
|
3138
|
-
let data;
|
|
3139
|
-
try {
|
|
3140
|
-
data = JSON.parse(event.data);
|
|
3141
|
-
} catch {
|
|
3142
|
-
return;
|
|
3143
|
-
}
|
|
3144
|
-
if (data.type === "pong" || data.type === "status" || data.type === "ack") return;
|
|
3145
|
-
if (data.type === "reload") {
|
|
3146
|
-
console.log("[HunterMate] received reload broadcast, reloading extension in 200ms...");
|
|
3147
|
-
setTimeout(() => chrome.runtime.reload(), 200);
|
|
3148
|
-
return;
|
|
3149
|
-
}
|
|
3150
|
-
if (data.type === "cmd" && data.requestId) {
|
|
3151
|
-
const startedAt = Date.now();
|
|
3152
|
-
sendLog("info", "extension handling control command", { command: data.command, requestId: data.requestId, params: data.params });
|
|
3153
|
-
let outcome;
|
|
3154
|
-
if (data.command === "hm.status") {
|
|
3155
|
-
const manifest = chrome.runtime.getManifest();
|
|
3156
|
-
outcome = {
|
|
3157
|
-
status: "ok",
|
|
3158
|
-
result: {
|
|
3159
|
-
...state,
|
|
3160
|
-
tabStatesCount: tabStates.size,
|
|
3161
|
-
extBasePath: chrome.runtime.getURL(""),
|
|
3162
|
-
manifestPermissions: manifest.permissions,
|
|
3163
|
-
hasSidePanelInManifest: manifest.permissions?.includes("sidePanel"),
|
|
3164
|
-
chromeSidePanelType: typeof chrome.sidePanel
|
|
3165
|
-
}
|
|
3166
|
-
};
|
|
3167
|
-
} else {
|
|
3168
|
-
try {
|
|
3169
|
-
outcome = await executeControlCommand(String(data.command ?? ""), data.params ?? {}, {
|
|
3170
|
-
onLog: (level, message, meta) => sendLog(level, message, meta),
|
|
3171
|
-
sessionId: typeof data.sessionId === "string" ? data.sessionId : void 0
|
|
3172
|
-
});
|
|
3173
|
-
} catch (e) {
|
|
3174
|
-
outcome = { status: "error", message: e instanceof Error ? e.message : String(e) };
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3177
|
-
const resp = outcome.status === "ok" ? {
|
|
3178
|
-
type: "cmd_response",
|
|
3179
|
-
requestId: data.requestId,
|
|
3889
|
+
async function handleServerMessage(data) {
|
|
3890
|
+
if (!data || typeof data !== "object") return;
|
|
3891
|
+
if (data.type === "pong" || data.type === "status" || data.type === "ack") return;
|
|
3892
|
+
if (data.type === "reload") {
|
|
3893
|
+
console.log("[HunterMate] received reload broadcast, reloading extension in 200ms...");
|
|
3894
|
+
setTimeout(() => chrome.runtime.reload(), 200);
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
if (data.type === "cmd" && data.requestId) {
|
|
3898
|
+
const startedAt = Date.now();
|
|
3899
|
+
sendLog("info", "extension handling control command", { command: data.command, requestId: data.requestId, params: data.params });
|
|
3900
|
+
let outcome;
|
|
3901
|
+
if (data.command === "hm.status") {
|
|
3902
|
+
const manifest = chrome.runtime.getManifest();
|
|
3903
|
+
outcome = {
|
|
3180
3904
|
status: "ok",
|
|
3181
|
-
result:
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3905
|
+
result: {
|
|
3906
|
+
...state,
|
|
3907
|
+
tabStatesCount: tabStates.size,
|
|
3908
|
+
extBasePath: chrome.runtime.getURL(""),
|
|
3909
|
+
manifestPermissions: manifest.permissions,
|
|
3910
|
+
hasSidePanelInManifest: manifest.permissions?.includes("sidePanel"),
|
|
3911
|
+
chromeSidePanelType: typeof chrome.sidePanel,
|
|
3912
|
+
wsHeldBy: "offscreen"
|
|
3913
|
+
}
|
|
3189
3914
|
};
|
|
3915
|
+
} else {
|
|
3190
3916
|
try {
|
|
3191
|
-
|
|
3192
|
-
|
|
3917
|
+
outcome = await executeControlCommand(String(data.command ?? ""), data.params ?? {}, {
|
|
3918
|
+
onLog: (level, message, meta) => sendLog(level, message, meta),
|
|
3919
|
+
sessionId: typeof data.sessionId === "string" ? data.sessionId : void 0
|
|
3920
|
+
});
|
|
3193
3921
|
} catch (e) {
|
|
3194
|
-
|
|
3922
|
+
outcome = { status: "error", message: e instanceof Error ? e.message : String(e) };
|
|
3195
3923
|
}
|
|
3196
|
-
return;
|
|
3197
3924
|
}
|
|
3198
|
-
|
|
3199
|
-
|
|
3925
|
+
const resp = outcome.status === "ok" ? {
|
|
3926
|
+
type: "cmd_response",
|
|
3927
|
+
requestId: data.requestId,
|
|
3928
|
+
status: "ok",
|
|
3929
|
+
// undefined 显式归一为 null:JSON.stringify 会丢 undefined 键,
|
|
3930
|
+
// 调用方无法区分"无 result 键"与"result 为 undefined"
|
|
3931
|
+
result: outcome.result === void 0 ? null : outcome.result,
|
|
3932
|
+
durationMs: Date.now() - startedAt
|
|
3933
|
+
} : {
|
|
3934
|
+
type: "cmd_response",
|
|
3935
|
+
requestId: data.requestId,
|
|
3936
|
+
status: "error",
|
|
3937
|
+
error: { code: outcome.code, message: outcome.message },
|
|
3938
|
+
durationMs: Date.now() - startedAt
|
|
3939
|
+
};
|
|
3940
|
+
sendToOffscreen(JSON.stringify(resp));
|
|
3941
|
+
sendLog("debug", "cmd_response sent", { requestId: data.requestId, status: resp.status, durationMs: resp.durationMs });
|
|
3942
|
+
}
|
|
3200
3943
|
}
|
|
3201
|
-
|
|
3944
|
+
chrome.runtime.onMessage.addListener((message) => {
|
|
3945
|
+
if (message?.type === "offscreen:ws-state") {
|
|
3946
|
+
state.wsConnected = !!message.wsConnected;
|
|
3947
|
+
broadcastState();
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
if (message?.type === "offscreen:ws-message") {
|
|
3951
|
+
void handleServerMessage(message.payload);
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
});
|
|
3202
3955
|
var RECONNECT_ALARM_NAME = "hm:reconnect";
|
|
3203
3956
|
function ensureReconnectAlarm() {
|
|
3204
3957
|
chrome.alarms.create(RECONNECT_ALARM_NAME, { periodInMinutes: 0.5 }).catch((err3) => console.error("[HunterMate] create reconnect alarm failed", err3));
|
|
@@ -3206,21 +3959,24 @@ function ensureReconnectAlarm() {
|
|
|
3206
3959
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
3207
3960
|
if (alarm.name !== RECONNECT_ALARM_NAME) return;
|
|
3208
3961
|
checkHttpHealth();
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
}
|
|
3212
|
-
if (!socket) {
|
|
3213
|
-
connect();
|
|
3214
|
-
}
|
|
3962
|
+
sendLog("debug", "sw:alarm-tick", { wsConnected: state.wsConnected });
|
|
3963
|
+
void ensureOffscreen();
|
|
3215
3964
|
});
|
|
3216
3965
|
ensureReconnectAlarm();
|
|
3966
|
+
void ensureOffscreen();
|
|
3967
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
3968
|
+
console.log("[HunterMate] onInstalled: cold-start offscreen");
|
|
3969
|
+
ensureReconnectAlarm();
|
|
3970
|
+
void ensureOffscreen();
|
|
3971
|
+
});
|
|
3972
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
3973
|
+
console.log("[HunterMate] onStartup: cold-start offscreen");
|
|
3974
|
+
ensureReconnectAlarm();
|
|
3975
|
+
void ensureOffscreen();
|
|
3976
|
+
});
|
|
3217
3977
|
setInterval(() => {
|
|
3218
3978
|
checkHttpHealth();
|
|
3219
|
-
|
|
3220
|
-
socket.send(JSON.stringify({ type: "ping" }));
|
|
3221
|
-
} else {
|
|
3222
|
-
connect();
|
|
3223
|
-
}
|
|
3979
|
+
void ensureOffscreen();
|
|
3224
3980
|
broadcastState();
|
|
3225
3981
|
}, 2e4);
|
|
3226
3982
|
setInterval(() => {
|
|
@@ -3305,11 +4061,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
3305
4061
|
if (message.type === "recording-event") {
|
|
3306
4062
|
const tabId = sender.tab?.id ?? -1;
|
|
3307
4063
|
const payload = { ...message.payload, tabId };
|
|
3308
|
-
|
|
3309
|
-
socket.send(JSON.stringify(payload));
|
|
3310
|
-
} else {
|
|
3311
|
-
enqueueRecordingEvent(payload);
|
|
3312
|
-
}
|
|
4064
|
+
sendToOffscreen(JSON.stringify(payload));
|
|
3313
4065
|
const ts = tabStates.get(tabId);
|
|
3314
4066
|
if (ts && message.payload?.sessionId === ts.recordingSessionId) {
|
|
3315
4067
|
ts.eventCount++;
|