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