job-pro 0.7.3 → 0.7.5
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/cdp.js +197 -0
- package/dist/deepseek.js +23 -386
- package/dist/galaxyuniversal.js +22 -388
- package/dist/hikvision.js +192 -144
- package/dist/index.js +1 -1
- package/dist/lilith.js +250 -135
- package/dist/megvii.js +25 -455
- package/dist/moonshot.js +22 -395
- package/dist/stepfun.js +22 -381
- package/package.json +5 -3
package/dist/cdp.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Headless-browser helper for adapters whose upstream is gated by anti-bot
|
|
2
|
+
// signatures that the CLI can't reproduce from raw HTTP.
|
|
3
|
+
//
|
|
4
|
+
// Usage pattern:
|
|
5
|
+
// 1. `await getBrowser()` returns a process-singleton puppeteer-core Browser
|
|
6
|
+
// attached to the user's system Chrome.
|
|
7
|
+
// 2. Call `viaBrowser(url, async page => …)` to navigate and run a fn in
|
|
8
|
+
// the page context, then receive the return value.
|
|
9
|
+
//
|
|
10
|
+
// Why puppeteer-core (not puppeteer): we attach to the user's existing
|
|
11
|
+
// Chrome installation; no 100MB Chromium download. Trade-off: we need a
|
|
12
|
+
// working Chrome executable path.
|
|
13
|
+
//
|
|
14
|
+
// Failure modes:
|
|
15
|
+
// * puppeteer-core not installed → ENOENT on dynamic import → caller
|
|
16
|
+
// receives `{ ok:false, reason:"puppeteer-not-installed", message: … }`
|
|
17
|
+
// and renders it as the canonical ok:false stub.
|
|
18
|
+
// * No Chrome found at any well-known path → same error shape with
|
|
19
|
+
// `reason:"chrome-not-found"`.
|
|
20
|
+
// * Browser launch failed (sandbox, profile lock, …) → `reason:"launch-failed"`.
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
const CHROME_PATHS = [
|
|
23
|
+
// macOS
|
|
24
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
25
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
26
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
27
|
+
// Linux
|
|
28
|
+
"/usr/bin/google-chrome",
|
|
29
|
+
"/usr/bin/google-chrome-stable",
|
|
30
|
+
"/usr/bin/chromium",
|
|
31
|
+
"/usr/bin/chromium-browser",
|
|
32
|
+
// Windows (when running under WSL / Git Bash)
|
|
33
|
+
"/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
34
|
+
];
|
|
35
|
+
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
|
|
36
|
+
// ---------- singleton browser ----------
|
|
37
|
+
let _browser = null;
|
|
38
|
+
let _browserError = null;
|
|
39
|
+
let _launching = null;
|
|
40
|
+
async function loadPuppeteer() {
|
|
41
|
+
try {
|
|
42
|
+
// Dynamic import; if puppeteer-core was tree-shaken or uninstalled,
|
|
43
|
+
// this rejects with ERR_MODULE_NOT_FOUND.
|
|
44
|
+
const mod = (await import("puppeteer-core"));
|
|
45
|
+
return { ok: true, mod: mod.default };
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: {
|
|
51
|
+
reason: "puppeteer-not-installed",
|
|
52
|
+
message: "`puppeteer-core` is not installed. Install it locally with " +
|
|
53
|
+
"`npm i puppeteer-core` (or `pnpm add puppeteer-core`). " +
|
|
54
|
+
`Original error: ${err instanceof Error ? err.message : String(err)}`,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function findChrome() {
|
|
60
|
+
if (process.env.JOB_PRO_CHROME && existsSync(process.env.JOB_PRO_CHROME)) {
|
|
61
|
+
return process.env.JOB_PRO_CHROME;
|
|
62
|
+
}
|
|
63
|
+
for (const p of CHROME_PATHS) {
|
|
64
|
+
if (existsSync(p))
|
|
65
|
+
return p;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
async function launchOnce() {
|
|
70
|
+
const pp = await loadPuppeteer();
|
|
71
|
+
if (!pp.ok)
|
|
72
|
+
return pp.error;
|
|
73
|
+
const chrome = findChrome();
|
|
74
|
+
if (!chrome) {
|
|
75
|
+
return {
|
|
76
|
+
reason: "chrome-not-found",
|
|
77
|
+
message: "No Chrome/Chromium executable found. Tried: " +
|
|
78
|
+
CHROME_PATHS.join(", ") +
|
|
79
|
+
". Set $JOB_PRO_CHROME=/path/to/chrome to override.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Optional egress proxy — useful for geo-fenced upstreams (e.g. hikvision
|
|
83
|
+
// requires a CN-egress to pass its Tencent EdgeOne 403 check). Set
|
|
84
|
+
// `$JOB_PRO_HTTPS_PROXY=http://user:pass@host:port` or `socks5://host:port`.
|
|
85
|
+
const proxy = process.env.JOB_PRO_HTTPS_PROXY?.trim();
|
|
86
|
+
const proxyArg = proxy ? [`--proxy-server=${proxy}`] : [];
|
|
87
|
+
try {
|
|
88
|
+
const browser = await pp.mod.launch({
|
|
89
|
+
executablePath: chrome,
|
|
90
|
+
headless: true,
|
|
91
|
+
args: [
|
|
92
|
+
"--no-sandbox",
|
|
93
|
+
"--disable-blink-features=AutomationControlled",
|
|
94
|
+
"--disable-features=IsolateOrigins,site-per-process",
|
|
95
|
+
...proxyArg,
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
return browser;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
reason: "launch-failed",
|
|
103
|
+
message: `Chrome failed to launch: ${err instanceof Error ? err.message : String(err)}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Get a process-singleton headless browser. Subsequent calls reuse it. */
|
|
108
|
+
export async function getBrowser() {
|
|
109
|
+
if (_browser)
|
|
110
|
+
return { ok: true, browser: _browser };
|
|
111
|
+
if (_browserError)
|
|
112
|
+
return { ok: false, error: _browserError };
|
|
113
|
+
if (!_launching) {
|
|
114
|
+
_launching = launchOnce();
|
|
115
|
+
}
|
|
116
|
+
const result = await _launching;
|
|
117
|
+
_launching = null;
|
|
118
|
+
if ("reason" in result) {
|
|
119
|
+
_browserError = result;
|
|
120
|
+
return { ok: false, error: result };
|
|
121
|
+
}
|
|
122
|
+
_browser = result;
|
|
123
|
+
return { ok: true, browser: result };
|
|
124
|
+
}
|
|
125
|
+
/** Close the singleton browser (call before process exit). */
|
|
126
|
+
export async function closeBrowser() {
|
|
127
|
+
if (_browser) {
|
|
128
|
+
try {
|
|
129
|
+
await _browser.close();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* ignore */
|
|
133
|
+
}
|
|
134
|
+
_browser = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// On Node exit, best-effort close the browser to avoid zombie processes.
|
|
138
|
+
let _exitHookInstalled = false;
|
|
139
|
+
function ensureExitHook() {
|
|
140
|
+
if (_exitHookInstalled)
|
|
141
|
+
return;
|
|
142
|
+
_exitHookInstalled = true;
|
|
143
|
+
const cleanup = () => {
|
|
144
|
+
if (_browser) {
|
|
145
|
+
try {
|
|
146
|
+
// synchronous best-effort kill; puppeteer launches Chrome as a child
|
|
147
|
+
// process tracked by the Browser object, so close() handles SIGTERM.
|
|
148
|
+
void _browser.close().catch(() => undefined);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* ignore */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
process.on("exit", cleanup);
|
|
156
|
+
process.on("SIGINT", () => {
|
|
157
|
+
cleanup();
|
|
158
|
+
process.exit(130);
|
|
159
|
+
});
|
|
160
|
+
process.on("SIGTERM", () => {
|
|
161
|
+
cleanup();
|
|
162
|
+
process.exit(143);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** Open a page, run fn against it, and close the page. The singleton browser stays open. */
|
|
166
|
+
export async function withPage(fn) {
|
|
167
|
+
ensureExitHook();
|
|
168
|
+
const b = await getBrowser();
|
|
169
|
+
if (!b.ok)
|
|
170
|
+
return b;
|
|
171
|
+
let page = null;
|
|
172
|
+
try {
|
|
173
|
+
page = await b.browser.newPage();
|
|
174
|
+
await page.setUserAgent(USER_AGENT);
|
|
175
|
+
const value = await fn(page);
|
|
176
|
+
return { ok: true, value };
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: {
|
|
182
|
+
reason: "launch-failed",
|
|
183
|
+
message: `page operation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
if (page) {
|
|
189
|
+
try {
|
|
190
|
+
await page.close();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* ignore */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
package/dist/deepseek.js
CHANGED
|
@@ -1,387 +1,24 @@
|
|
|
1
|
-
// DeepSeek (深度求索) / High-Flyer (幻方量化)
|
|
1
|
+
// DeepSeek (深度求索) / High-Flyer (幻方量化) careers — Moka SSR + AES-128-CBC.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import { createDecipheriv } from "node:crypto";
|
|
26
|
-
export { checkResume, extractResumeSignals, scoreOverlap };
|
|
27
|
-
const SOURCE = "app.mokahr.com/high-flyer";
|
|
28
|
-
const ORG_SLUG = "high-flyer";
|
|
29
|
-
const SITE_ID = 140576;
|
|
30
|
-
const PORTAL_URL = `https://app.mokahr.com/social-recruitment/${ORG_SLUG}/${SITE_ID}`;
|
|
31
|
-
const API_ENDPOINT = "https://app.mokahr.com/api/outer/ats-apply/website/jobs/v2";
|
|
32
|
-
const DEFAULT_HEADERS = {
|
|
33
|
-
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
34
|
-
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
35
|
-
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
36
|
-
};
|
|
37
|
-
function htmlDecode(s) {
|
|
38
|
-
return s
|
|
39
|
-
.replace(/"/g, '"')
|
|
40
|
-
.replace(/&/g, "&")
|
|
41
|
-
.replace(/</g, "<")
|
|
42
|
-
.replace(/>/g, ">")
|
|
43
|
-
.replace(/'/g, "'")
|
|
44
|
-
.replace(/'/g, "'");
|
|
45
|
-
}
|
|
46
|
-
function parseInitData(html) {
|
|
47
|
-
const m = html.match(/<input[^>]*id="init-data"[^>]*value="([^"]+)"/);
|
|
48
|
-
if (!m)
|
|
49
|
-
return null;
|
|
50
|
-
try {
|
|
51
|
-
return JSON.parse(htmlDecode(m[1]));
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
async function fetchPortalHtml() {
|
|
58
|
-
let r1;
|
|
59
|
-
try {
|
|
60
|
-
r1 = await fetch(PORTAL_URL, { method: "GET", headers: DEFAULT_HEADERS, redirect: "manual" });
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
64
|
-
}
|
|
65
|
-
const cookies = [];
|
|
66
|
-
// getSetCookie() must be called bound to the Headers object (Node undici brandCheck)
|
|
67
|
-
const headersAny = r1.headers;
|
|
68
|
-
if (typeof headersAny.getSetCookie === "function") {
|
|
69
|
-
for (const v of headersAny.getSetCookie.call(r1.headers) ?? []) {
|
|
70
|
-
const c = v.split(";")[0];
|
|
71
|
-
if (c)
|
|
72
|
-
cookies.push(c);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (cookies.length === 0) {
|
|
76
|
-
const raw = r1.headers.get("set-cookie");
|
|
77
|
-
if (raw)
|
|
78
|
-
cookies.push(...raw.split(/,(?=[^;]+=)/).map((c) => c.split(";")[0].trim()));
|
|
79
|
-
}
|
|
80
|
-
const cookieHeader = cookies.join("; ");
|
|
81
|
-
let r2;
|
|
82
|
-
try {
|
|
83
|
-
r2 = await fetch(PORTAL_URL, {
|
|
84
|
-
method: "GET",
|
|
85
|
-
headers: { ...DEFAULT_HEADERS, Cookie: cookieHeader },
|
|
86
|
-
redirect: "follow",
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
91
|
-
}
|
|
92
|
-
if (!r2.ok)
|
|
93
|
-
return { ok: false, message: `HTTP ${r2.status}` };
|
|
94
|
-
return { ok: true, html: await r2.text(), cookieHeader, message: "ok" };
|
|
95
|
-
}
|
|
96
|
-
function decryptMoka(envelope, aesIv) {
|
|
97
|
-
if (!envelope.data || !envelope.necromancer)
|
|
98
|
-
return null;
|
|
99
|
-
try {
|
|
100
|
-
const decipher = createDecipheriv("aes-128-cbc", Buffer.from(envelope.necromancer, "utf8"), Buffer.from(aesIv, "utf8"));
|
|
101
|
-
const plain = Buffer.concat([
|
|
102
|
-
decipher.update(Buffer.from(envelope.data, "base64")),
|
|
103
|
-
decipher.final(),
|
|
104
|
-
]);
|
|
105
|
-
return JSON.parse(plain.toString("utf8"));
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
async function fetchEncryptedPage(pageNum, pageSize, aesIv, cookieHeader) {
|
|
112
|
-
let response;
|
|
113
|
-
try {
|
|
114
|
-
response = await fetch(`${API_ENDPOINT}?orgId=${encodeURIComponent(ORG_SLUG)}`, {
|
|
115
|
-
method: "POST",
|
|
116
|
-
headers: {
|
|
117
|
-
...DEFAULT_HEADERS,
|
|
118
|
-
Accept: "application/json,*/*",
|
|
119
|
-
"Content-Type": "application/json",
|
|
120
|
-
Origin: "https://app.mokahr.com",
|
|
121
|
-
Referer: PORTAL_URL,
|
|
122
|
-
Cookie: cookieHeader,
|
|
123
|
-
},
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
orgId: ORG_SLUG,
|
|
126
|
-
siteId: String(SITE_ID),
|
|
127
|
-
pageNum,
|
|
128
|
-
pageSize,
|
|
129
|
-
needStat: true,
|
|
130
|
-
}),
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
catch (err) {
|
|
134
|
-
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
135
|
-
}
|
|
136
|
-
if (!response.ok)
|
|
137
|
-
return { ok: false, message: `HTTP ${response.status}` };
|
|
138
|
-
let envelope;
|
|
139
|
-
try {
|
|
140
|
-
envelope = await response.json();
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return { ok: false, message: "bad JSON" };
|
|
144
|
-
}
|
|
145
|
-
const decoded = decryptMoka(envelope, aesIv);
|
|
146
|
-
if (!decoded || decoded.code !== 0 || !decoded.data) {
|
|
147
|
-
return { ok: false, message: decoded?.msg || envelope.msg || "decrypt error" };
|
|
148
|
-
}
|
|
149
|
-
return {
|
|
150
|
-
ok: true,
|
|
151
|
-
jobs: decoded.data.jobs ?? [],
|
|
152
|
-
total: decoded.data.jobStats?.total ?? 0,
|
|
153
|
-
message: "ok",
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
function buildCityMap(groups) {
|
|
157
|
-
const out = {};
|
|
158
|
-
if (!groups)
|
|
159
|
-
return out;
|
|
160
|
-
for (const g of groups) {
|
|
161
|
-
if (typeof g.cityId === "number" && g.label)
|
|
162
|
-
out[g.cityId] = g.label;
|
|
163
|
-
}
|
|
164
|
-
return out;
|
|
165
|
-
}
|
|
166
|
-
function workCities(job, cityMap) {
|
|
167
|
-
const uniq = [];
|
|
168
|
-
for (const loc of job.locations ?? []) {
|
|
169
|
-
const label = (typeof loc.cityId === "number" && cityMap[loc.cityId]) || loc.country || "";
|
|
170
|
-
if (label && !uniq.includes(label))
|
|
171
|
-
uniq.push(label);
|
|
172
|
-
}
|
|
173
|
-
return uniq.join(" / ");
|
|
174
|
-
}
|
|
175
|
-
function recruitLabel(job) {
|
|
176
|
-
if (job.commitment)
|
|
177
|
-
return job.commitment;
|
|
178
|
-
if (job.hireMode === 1)
|
|
179
|
-
return "全职";
|
|
180
|
-
if (job.hireMode === 2)
|
|
181
|
-
return "实习";
|
|
182
|
-
return "";
|
|
183
|
-
}
|
|
184
|
-
function summarize(job, cityMap) {
|
|
185
|
-
return {
|
|
186
|
-
post_id: String(job.id),
|
|
187
|
-
title: job.title ?? "",
|
|
188
|
-
project: job.zhineng?.name ?? "",
|
|
189
|
-
recruit_label: recruitLabel(job),
|
|
190
|
-
bgs: job.department?.name ?? "",
|
|
191
|
-
work_cities: workCities(job, cityMap),
|
|
192
|
-
apply_url: `${PORTAL_URL}#/jobs/${encodeURIComponent(job.id)}`,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function matchesKeyword(job, kw) {
|
|
196
|
-
if (!kw)
|
|
197
|
-
return true;
|
|
198
|
-
const lc = kw.toLowerCase();
|
|
199
|
-
return ((job.title ?? "").toLowerCase().includes(lc) ||
|
|
200
|
-
(job.zhineng?.name ?? "").toLowerCase().includes(lc) ||
|
|
201
|
-
(job.department?.name ?? "").toLowerCase().includes(lc));
|
|
202
|
-
}
|
|
203
|
-
export async function searchPositions(opts = {}) {
|
|
204
|
-
const pageSize = opts.pageSize ?? 20;
|
|
205
|
-
const page = opts.page ?? 1;
|
|
206
|
-
const keyword = opts.keyword ?? "";
|
|
207
|
-
const portal = await fetchPortalHtml();
|
|
208
|
-
if (!portal.ok || !portal.html) {
|
|
209
|
-
return {
|
|
210
|
-
ok: false,
|
|
211
|
-
source: SOURCE,
|
|
212
|
-
message: portal.message,
|
|
213
|
-
query: { keyword, page, pageSize },
|
|
214
|
-
positions: [],
|
|
215
|
-
total: 0,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
const init = parseInitData(portal.html);
|
|
219
|
-
if (!init || !init.jobs || !init.jobStats) {
|
|
220
|
-
return {
|
|
221
|
-
ok: false,
|
|
222
|
-
source: SOURCE,
|
|
223
|
-
message: "Moka init-data missing jobs/jobStats",
|
|
224
|
-
query: { keyword, page, pageSize },
|
|
225
|
-
positions: [],
|
|
226
|
-
total: 0,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
const cityMap = buildCityMap(init.jobsGroupedByLocation);
|
|
230
|
-
let jobs = init.jobs;
|
|
231
|
-
const total = init.jobStats.total ?? jobs.length;
|
|
232
|
-
if (page > 1 && init.aesIv && portal.cookieHeader) {
|
|
233
|
-
const more = await fetchEncryptedPage(page, pageSize, init.aesIv, portal.cookieHeader);
|
|
234
|
-
if (!more.ok || !more.jobs) {
|
|
235
|
-
return {
|
|
236
|
-
ok: false,
|
|
237
|
-
source: SOURCE,
|
|
238
|
-
message: `pagination failed: ${more.message}`,
|
|
239
|
-
query: { keyword, page, pageSize },
|
|
240
|
-
positions: [],
|
|
241
|
-
total,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
jobs = more.jobs;
|
|
245
|
-
}
|
|
246
|
-
const filtered = jobs.filter((j) => matchesKeyword(j, keyword)).slice(0, pageSize);
|
|
247
|
-
return {
|
|
248
|
-
ok: true,
|
|
249
|
-
source: SOURCE,
|
|
250
|
-
query: { keyword, page, pageSize },
|
|
251
|
-
page,
|
|
252
|
-
page_size: pageSize,
|
|
253
|
-
total,
|
|
254
|
-
positions: filtered.map((j) => summarize(j, cityMap)),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
export async function fetchAllPositions(opts = {}) {
|
|
258
|
-
const pageSize = opts.pageSize ?? 20;
|
|
259
|
-
const maxPages = Math.max(1, opts.maxPages ?? 50);
|
|
260
|
-
const keyword = opts.keyword ?? "";
|
|
261
|
-
const portal = await fetchPortalHtml();
|
|
262
|
-
if (!portal.ok || !portal.html) {
|
|
263
|
-
return {
|
|
264
|
-
ok: false,
|
|
265
|
-
source: SOURCE,
|
|
266
|
-
message: portal.message,
|
|
267
|
-
total: 0,
|
|
268
|
-
fetched: 0,
|
|
269
|
-
positions: [],
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
const init = parseInitData(portal.html);
|
|
273
|
-
if (!init || !init.jobs || !init.jobStats || !init.aesIv) {
|
|
274
|
-
return {
|
|
275
|
-
ok: false,
|
|
276
|
-
source: SOURCE,
|
|
277
|
-
message: "Moka init-data missing required fields",
|
|
278
|
-
total: 0,
|
|
279
|
-
fetched: 0,
|
|
280
|
-
positions: [],
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
const cityMap = buildCityMap(init.jobsGroupedByLocation);
|
|
284
|
-
const total = init.jobStats.total ?? 0;
|
|
285
|
-
const collected = [...init.jobs];
|
|
286
|
-
let page = 2;
|
|
287
|
-
while (collected.length < total && page <= maxPages) {
|
|
288
|
-
const more = await fetchEncryptedPage(page, pageSize, init.aesIv, portal.cookieHeader ?? "");
|
|
289
|
-
if (!more.ok || !more.jobs || more.jobs.length === 0)
|
|
290
|
-
break;
|
|
291
|
-
collected.push(...more.jobs);
|
|
292
|
-
page += 1;
|
|
293
|
-
}
|
|
294
|
-
const filtered = collected.filter((j) => matchesKeyword(j, keyword));
|
|
295
|
-
return {
|
|
296
|
-
ok: true,
|
|
297
|
-
source: SOURCE,
|
|
298
|
-
total,
|
|
299
|
-
fetched: filtered.length,
|
|
300
|
-
positions: filtered.map((j) => summarize(j, cityMap)),
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
export async function fetchPositionDetail(postId) {
|
|
304
|
-
return {
|
|
305
|
-
ok: false,
|
|
306
|
-
source: SOURCE,
|
|
307
|
-
message: "Moka detail endpoint is also AES-encrypted and not implemented; " +
|
|
308
|
-
"use the apply_url deeplink for the full JD.",
|
|
309
|
-
post_id: postId,
|
|
310
|
-
apply_url: `${PORTAL_URL}#/jobs/${encodeURIComponent(postId)}`,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
export async function fetchDictionaries() {
|
|
314
|
-
const portal = await fetchPortalHtml();
|
|
315
|
-
if (!portal.ok || !portal.html) {
|
|
316
|
-
return { ok: false, source: SOURCE, message: portal.message };
|
|
317
|
-
}
|
|
318
|
-
const init = parseInitData(portal.html);
|
|
319
|
-
if (!init) {
|
|
320
|
-
return { ok: false, source: SOURCE, message: "Moka init-data missing" };
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
ok: true,
|
|
324
|
-
source: SOURCE,
|
|
325
|
-
locations: init.jobsGroupedByLocation ?? [],
|
|
326
|
-
moka_org: { slug: ORG_SLUG, siteId: SITE_ID, url: PORTAL_URL },
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
export async function listNotices() {
|
|
330
|
-
return {
|
|
331
|
-
ok: false,
|
|
332
|
-
source: SOURCE,
|
|
333
|
-
message: "DeepSeek: no public notices endpoint",
|
|
334
|
-
notices: [],
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
export async function getNotice(noticeId) {
|
|
338
|
-
return {
|
|
339
|
-
ok: false,
|
|
340
|
-
source: SOURCE,
|
|
341
|
-
message: "DeepSeek: no public notices endpoint",
|
|
342
|
-
notice_id: noticeId,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
346
|
-
return {
|
|
347
|
-
ok: false,
|
|
348
|
-
source: SOURCE,
|
|
349
|
-
question,
|
|
350
|
-
message: "DeepSeek: no public notices endpoint",
|
|
351
|
-
matches: [],
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
export async function matchResume(text, opts = {}) {
|
|
355
|
-
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
356
|
-
const candidates = Math.max(20, opts.candidates ?? 100);
|
|
357
|
-
const all = await fetchAllPositions({
|
|
358
|
-
pageSize: 20,
|
|
359
|
-
maxPages: Math.ceil(candidates / 15),
|
|
360
|
-
});
|
|
361
|
-
if (!all.ok) {
|
|
362
|
-
return {
|
|
363
|
-
ok: false,
|
|
364
|
-
source: SOURCE,
|
|
365
|
-
extracted_terms: terms,
|
|
366
|
-
city_preferences: cities,
|
|
367
|
-
matches: [],
|
|
368
|
-
message: all.message,
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
const topN = Math.max(1, opts.topN ?? 10);
|
|
372
|
-
const scored = all.positions
|
|
373
|
-
.map((p) => ({
|
|
374
|
-
p,
|
|
375
|
-
score: scoreOverlap(`${p.title} ${p.project} ${p.bgs}`, terms, cities).score,
|
|
376
|
-
}))
|
|
377
|
-
.sort((a, b) => b.score - a.score)
|
|
378
|
-
.slice(0, topN)
|
|
379
|
-
.map((x) => x.p);
|
|
380
|
-
return {
|
|
381
|
-
ok: true,
|
|
382
|
-
source: SOURCE,
|
|
383
|
-
extracted_terms: terms,
|
|
384
|
-
city_preferences: cities,
|
|
385
|
-
matches: scored,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
3
|
+
// Portal: https://app.mokahr.com/social-recruitment/high-flyer/140576
|
|
4
|
+
// (High-Flyer is the parent quant fund; DeepSeek's careers share the
|
|
5
|
+
// same Moka tenant.) Probed 2026-05; ~37 social-hire positions.
|
|
6
|
+
// See cli/src/moka.ts for the shared factory.
|
|
7
|
+
import { createAdapter } from "./moka.js";
|
|
8
|
+
const adapter = createAdapter({
|
|
9
|
+
orgSlug: "high-flyer",
|
|
10
|
+
label: "DeepSeek / High-Flyer",
|
|
11
|
+
channels: [
|
|
12
|
+
{ siteId: 140576, kind: "social-recruitment", recruitType: "social" },
|
|
13
|
+
],
|
|
14
|
+
defaultRecruitType: "social",
|
|
15
|
+
});
|
|
16
|
+
export const searchPositions = adapter.searchPositions;
|
|
17
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
18
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
19
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
20
|
+
export const listNotices = adapter.listNotices;
|
|
21
|
+
export const getNotice = adapter.getNotice;
|
|
22
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
23
|
+
export const matchResume = adapter.matchResume;
|
|
24
|
+
export const checkResume = adapter.checkResume;
|