job-pro 0.7.0 → 0.7.2
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/adapter.js +17 -0
- package/dist/alibaba.js +2 -2
- package/dist/baichuan.js +38 -123
- package/dist/bilibili.js +43 -0
- package/dist/byd.js +333 -112
- package/dist/cambricon.js +379 -39
- package/dist/deepseek.js +373 -25
- package/dist/galaxyuniversal.js +375 -23
- package/dist/horizonrobotics.js +40 -61
- package/dist/iflytek.js +299 -57
- package/dist/index.js +55 -50
- package/dist/megvii.js +387 -150
- package/dist/mihoyo.js +241 -67
- package/dist/moonshot.js +387 -54
- package/dist/oppo.js +212 -56
- package/dist/pdd.js +278 -129
- package/dist/sensetime.js +47 -183
- package/dist/sf.js +249 -36
- package/dist/stepfun.js +333 -105
- package/dist/vivo.js +291 -41
- package/dist/wecruit.js +385 -0
- package/dist/weibo.js +259 -88
- package/dist/zerooneai.js +40 -37
- package/package.json +3 -2
package/dist/stepfun.js
CHANGED
|
@@ -1,126 +1,330 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 阶跃星辰 / StepFun recruiting via app.mokahr.com.
|
|
2
2
|
//
|
|
3
3
|
// ============================================================
|
|
4
|
-
//
|
|
4
|
+
// HOW THIS WORKS (probed 2026-05):
|
|
5
5
|
//
|
|
6
|
-
// https://
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// SSR HTML at https://app.mokahr.com/social-recruitment/step/94904 embeds
|
|
7
|
+
// `<input id="init-data" value="<JSON>">` with the first 15 jobs and
|
|
8
|
+
// `jobStats.total`. Deeper pages come from
|
|
9
|
+
// POST /api/outer/ats-apply/website/jobs/v2?orgId=step (AES-128-CBC
|
|
10
|
+
// envelope: key=necromancer, iv=aesIv from SSR HTML).
|
|
10
11
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// jiebuxingchen.jobs.feishu.cn
|
|
15
|
-
// stepai.jobs.feishu.cn
|
|
16
|
-
// stepfunai.jobs.feishu.cn
|
|
17
|
-
// step-fun.jobs.feishu.cn
|
|
18
|
-
// jieyu.jobs.feishu.cn
|
|
19
|
-
// steppfun.jobs.feishu.cn
|
|
12
|
+
// CONFIRMED MOKA ORG:
|
|
13
|
+
// slug=step, siteId=94904, mode=social
|
|
14
|
+
// Portal: https://app.mokahr.com/social-recruitment/step/94904
|
|
20
15
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// /api/campus/v1/org/step/positions → 404
|
|
30
|
-
// /api/campus/v1/jobs?orgSlug=step → 404
|
|
31
|
-
// /api/v1/jobs?orgSlug=step → 404
|
|
32
|
-
// /api/v1/social/positions?orgSlug=step → 404
|
|
33
|
-
// /api/campus/v2/positions?orgSlug=step → 404
|
|
34
|
-
// This matches the Moka "social-only" posture described in the task brief
|
|
35
|
-
// (auth-gated, no unauthenticated public position-list endpoint).
|
|
36
|
-
//
|
|
37
|
-
// ============================================================
|
|
38
|
-
// INFRASTRUCTURE NOTES:
|
|
39
|
-
//
|
|
40
|
-
// StepFun (阶跃星辰) is a Beijing-based AI lab founded 2023. Their public
|
|
41
|
-
// website (stepfun.com) is a Next.js consumer-facing chat app. They have
|
|
42
|
-
// not published a public unauthenticated job-search API on any discovered
|
|
43
|
-
// subdomain or ATS platform.
|
|
44
|
-
//
|
|
45
|
-
// POSSIBLE FUTURE UNBLOCKING:
|
|
46
|
-
// (a) StepFun activating a Feishu ATSX tenant (watch *.jobs.feishu.cn)
|
|
47
|
-
// (b) StepFun activating the Moka public social-recruitment module
|
|
48
|
-
// (c) StepFun building a custom career page with an open JSON API
|
|
49
|
-
// (d) Third-party aggregators: Boss直聘, 拉勾, 实习僧 (separate adapters)
|
|
50
|
-
//
|
|
51
|
-
// ============================================================
|
|
52
|
-
// STUB CONTRACT:
|
|
53
|
-
// All functions return ok:false with STUB_MESSAGE.
|
|
54
|
-
// checkResume is re-exported from tencent.ts (works offline on resume text).
|
|
55
|
-
// PositionSummary matches the canonical shape used by every other adapter.
|
|
56
|
-
//
|
|
57
|
-
// ============================================================
|
|
58
|
-
// ---- PositionSummary field mapping (StepFun → canonical, for when API becomes accessible) ----
|
|
59
|
-
// post_id ← position ID from Feishu ATSX or Moka publishId
|
|
60
|
-
// title ← position name / 职位名称
|
|
61
|
-
// project ← job category / 职位类别 (e.g. "算法研究", "后端开发", "大模型")
|
|
62
|
-
// recruit_label ← recruit type / 招聘类型 (e.g. "社招", "校招", "实习")
|
|
63
|
-
// bgs ← business line — not exposed in known public payloads → ""
|
|
64
|
-
// work_cities ← work location / 工作地点 (e.g. "北京" / "上海")
|
|
65
|
-
// apply_url ← https://app.mokahr.com/social-recruitment/step/94904 (portal URL)
|
|
16
|
+
// PositionSummary field mapping:
|
|
17
|
+
// post_id ← job.id
|
|
18
|
+
// title ← job.title
|
|
19
|
+
// project ← job.zhineng?.name (e.g. "算法类")
|
|
20
|
+
// recruit_label ← job.commitment || hireMode label
|
|
21
|
+
// bgs ← job.department?.name
|
|
22
|
+
// work_cities ← locations[].cityId → label via jobsGroupedByLocation
|
|
23
|
+
// apply_url ← portal#/jobs/{id}
|
|
66
24
|
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
25
|
+
import { createDecipheriv } from "node:crypto";
|
|
26
|
+
export { checkResume, extractResumeSignals, scoreOverlap };
|
|
27
|
+
const SOURCE = "app.mokahr.com/step";
|
|
28
|
+
const ORG_SLUG = "step";
|
|
29
|
+
const SITE_ID = 94904;
|
|
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
|
+
const headersAny = r1.headers;
|
|
67
|
+
if (typeof headersAny.getSetCookie === "function") {
|
|
68
|
+
for (const v of headersAny.getSetCookie.call(r1.headers) ?? []) {
|
|
69
|
+
const c = v.split(";")[0];
|
|
70
|
+
if (c)
|
|
71
|
+
cookies.push(c);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (cookies.length === 0) {
|
|
75
|
+
const raw = r1.headers.get("set-cookie");
|
|
76
|
+
if (raw)
|
|
77
|
+
cookies.push(...raw.split(/,(?=[^;]+=)/).map((c) => c.split(";")[0].trim()));
|
|
78
|
+
}
|
|
79
|
+
const cookieHeader = cookies.join("; ");
|
|
80
|
+
let r2;
|
|
81
|
+
try {
|
|
82
|
+
r2 = await fetch(PORTAL_URL, {
|
|
83
|
+
method: "GET",
|
|
84
|
+
headers: { ...DEFAULT_HEADERS, Cookie: cookieHeader },
|
|
85
|
+
redirect: "follow",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
90
|
+
}
|
|
91
|
+
if (!r2.ok)
|
|
92
|
+
return { ok: false, message: `HTTP ${r2.status}` };
|
|
93
|
+
return { ok: true, html: await r2.text(), cookieHeader, message: "ok" };
|
|
94
|
+
}
|
|
95
|
+
function decryptMoka(envelope, aesIv) {
|
|
96
|
+
if (!envelope.data || !envelope.necromancer)
|
|
97
|
+
return null;
|
|
98
|
+
try {
|
|
99
|
+
const decipher = createDecipheriv("aes-128-cbc", Buffer.from(envelope.necromancer, "utf8"), Buffer.from(aesIv, "utf8"));
|
|
100
|
+
const plain = Buffer.concat([
|
|
101
|
+
decipher.update(Buffer.from(envelope.data, "base64")),
|
|
102
|
+
decipher.final(),
|
|
103
|
+
]);
|
|
104
|
+
return JSON.parse(plain.toString("utf8"));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function fetchEncryptedPage(pageNum, pageSize, aesIv, cookieHeader) {
|
|
111
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await fetch(`${API_ENDPOINT}?orgId=${encodeURIComponent(ORG_SLUG)}`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
...DEFAULT_HEADERS,
|
|
117
|
+
Accept: "application/json,*/*",
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
Origin: "https://app.mokahr.com",
|
|
120
|
+
Referer: PORTAL_URL,
|
|
121
|
+
Cookie: cookieHeader,
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
orgId: ORG_SLUG,
|
|
125
|
+
siteId: String(SITE_ID),
|
|
126
|
+
pageNum,
|
|
127
|
+
pageSize,
|
|
128
|
+
needStat: true,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
134
|
+
}
|
|
135
|
+
if (!response.ok)
|
|
136
|
+
return { ok: false, message: `HTTP ${response.status}` };
|
|
137
|
+
let envelope;
|
|
138
|
+
try {
|
|
139
|
+
envelope = await response.json();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { ok: false, message: "bad JSON" };
|
|
143
|
+
}
|
|
144
|
+
const decoded = decryptMoka(envelope, aesIv);
|
|
145
|
+
if (!decoded || decoded.code !== 0 || !decoded.data) {
|
|
146
|
+
return { ok: false, message: decoded?.msg || envelope.msg || "decrypt error" };
|
|
147
|
+
}
|
|
78
148
|
return {
|
|
79
|
-
ok:
|
|
149
|
+
ok: true,
|
|
150
|
+
jobs: decoded.data.jobs ?? [],
|
|
151
|
+
total: decoded.data.jobStats?.total ?? 0,
|
|
152
|
+
message: "ok",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function buildCityMap(groups) {
|
|
156
|
+
const out = {};
|
|
157
|
+
if (!groups)
|
|
158
|
+
return out;
|
|
159
|
+
for (const g of groups) {
|
|
160
|
+
if (typeof g.cityId === "number" && g.label)
|
|
161
|
+
out[g.cityId] = g.label;
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
function workCities(job, cityMap) {
|
|
166
|
+
const uniq = [];
|
|
167
|
+
for (const loc of job.locations ?? []) {
|
|
168
|
+
const label = (typeof loc.cityId === "number" && cityMap[loc.cityId]) || loc.country || "";
|
|
169
|
+
if (label && !uniq.includes(label))
|
|
170
|
+
uniq.push(label);
|
|
171
|
+
}
|
|
172
|
+
return uniq.join(" / ");
|
|
173
|
+
}
|
|
174
|
+
function recruitLabel(job) {
|
|
175
|
+
if (job.commitment)
|
|
176
|
+
return job.commitment;
|
|
177
|
+
if (job.hireMode === 1)
|
|
178
|
+
return "全职";
|
|
179
|
+
if (job.hireMode === 2)
|
|
180
|
+
return "实习";
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
function summarize(job, cityMap) {
|
|
184
|
+
return {
|
|
185
|
+
post_id: String(job.id),
|
|
186
|
+
title: job.title ?? "",
|
|
187
|
+
project: job.zhineng?.name ?? "",
|
|
188
|
+
recruit_label: recruitLabel(job),
|
|
189
|
+
bgs: job.department?.name ?? "",
|
|
190
|
+
work_cities: workCities(job, cityMap),
|
|
191
|
+
apply_url: `${PORTAL_URL}#/jobs/${encodeURIComponent(job.id)}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function matchesKeyword(job, kw) {
|
|
195
|
+
if (!kw)
|
|
196
|
+
return true;
|
|
197
|
+
const lc = kw.toLowerCase();
|
|
198
|
+
return ((job.title ?? "").toLowerCase().includes(lc) ||
|
|
199
|
+
(job.zhineng?.name ?? "").toLowerCase().includes(lc) ||
|
|
200
|
+
(job.department?.name ?? "").toLowerCase().includes(lc));
|
|
201
|
+
}
|
|
202
|
+
export async function searchPositions(opts = {}) {
|
|
203
|
+
const pageSize = opts.pageSize ?? 20;
|
|
204
|
+
const page = opts.page ?? 1;
|
|
205
|
+
const keyword = opts.keyword ?? "";
|
|
206
|
+
const portal = await fetchPortalHtml();
|
|
207
|
+
if (!portal.ok || !portal.html) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
source: SOURCE,
|
|
211
|
+
message: portal.message,
|
|
212
|
+
query: { keyword, page, pageSize },
|
|
213
|
+
positions: [],
|
|
214
|
+
total: 0,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const init = parseInitData(portal.html);
|
|
218
|
+
if (!init || !init.jobs || !init.jobStats) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
source: SOURCE,
|
|
222
|
+
message: "Moka init-data missing jobs/jobStats",
|
|
223
|
+
query: { keyword, page, pageSize },
|
|
224
|
+
positions: [],
|
|
225
|
+
total: 0,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const cityMap = buildCityMap(init.jobsGroupedByLocation);
|
|
229
|
+
let jobs = init.jobs;
|
|
230
|
+
const total = init.jobStats.total ?? jobs.length;
|
|
231
|
+
if (page > 1 && init.aesIv && portal.cookieHeader) {
|
|
232
|
+
const more = await fetchEncryptedPage(page, pageSize, init.aesIv, portal.cookieHeader);
|
|
233
|
+
if (!more.ok || !more.jobs) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
source: SOURCE,
|
|
237
|
+
message: `pagination failed: ${more.message}`,
|
|
238
|
+
query: { keyword, page, pageSize },
|
|
239
|
+
positions: [],
|
|
240
|
+
total,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
jobs = more.jobs;
|
|
244
|
+
}
|
|
245
|
+
const filtered = jobs.filter((j) => matchesKeyword(j, keyword)).slice(0, pageSize);
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
80
248
|
source: SOURCE,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
query: {
|
|
87
|
-
keyword: _opts.keyword ?? "",
|
|
88
|
-
page: _opts.page ?? 1,
|
|
89
|
-
pageSize: _opts.pageSize ?? 20,
|
|
90
|
-
recruitType: _opts.recruitType ?? "social",
|
|
91
|
-
},
|
|
92
|
-
positions: [],
|
|
93
|
-
total: 0,
|
|
249
|
+
query: { keyword, page, pageSize },
|
|
250
|
+
page,
|
|
251
|
+
page_size: pageSize,
|
|
252
|
+
total,
|
|
253
|
+
positions: filtered.map((j) => summarize(j, cityMap)),
|
|
94
254
|
};
|
|
95
255
|
}
|
|
96
|
-
|
|
97
|
-
|
|
256
|
+
export async function fetchAllPositions(opts = {}) {
|
|
257
|
+
const pageSize = opts.pageSize ?? 20;
|
|
258
|
+
const maxPages = Math.max(1, opts.maxPages ?? 50);
|
|
259
|
+
const keyword = opts.keyword ?? "";
|
|
260
|
+
const portal = await fetchPortalHtml();
|
|
261
|
+
if (!portal.ok || !portal.html) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
source: SOURCE,
|
|
265
|
+
message: portal.message,
|
|
266
|
+
total: 0,
|
|
267
|
+
fetched: 0,
|
|
268
|
+
positions: [],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const init = parseInitData(portal.html);
|
|
272
|
+
if (!init || !init.jobs || !init.jobStats || !init.aesIv) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
source: SOURCE,
|
|
276
|
+
message: "Moka init-data missing required fields",
|
|
277
|
+
total: 0,
|
|
278
|
+
fetched: 0,
|
|
279
|
+
positions: [],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const cityMap = buildCityMap(init.jobsGroupedByLocation);
|
|
283
|
+
const total = init.jobStats.total ?? 0;
|
|
284
|
+
const collected = [...init.jobs];
|
|
285
|
+
let page = 2;
|
|
286
|
+
while (collected.length < total && page <= maxPages) {
|
|
287
|
+
const more = await fetchEncryptedPage(page, pageSize, init.aesIv, portal.cookieHeader ?? "");
|
|
288
|
+
if (!more.ok || !more.jobs || more.jobs.length === 0)
|
|
289
|
+
break;
|
|
290
|
+
collected.push(...more.jobs);
|
|
291
|
+
page += 1;
|
|
292
|
+
}
|
|
293
|
+
const filtered = collected.filter((j) => matchesKeyword(j, keyword));
|
|
98
294
|
return {
|
|
99
|
-
ok:
|
|
295
|
+
ok: true,
|
|
100
296
|
source: SOURCE,
|
|
101
|
-
|
|
102
|
-
fetched:
|
|
103
|
-
positions:
|
|
104
|
-
total: 0,
|
|
297
|
+
total,
|
|
298
|
+
fetched: filtered.length,
|
|
299
|
+
positions: filtered.map((j) => summarize(j, cityMap)),
|
|
105
300
|
};
|
|
106
301
|
}
|
|
107
|
-
|
|
108
|
-
export async function fetchPositionDetail(_postId) {
|
|
302
|
+
export async function fetchPositionDetail(postId) {
|
|
109
303
|
return {
|
|
110
304
|
ok: false,
|
|
111
305
|
source: SOURCE,
|
|
112
|
-
message:
|
|
306
|
+
message: "Moka detail endpoint is also AES-encrypted and not implemented; " +
|
|
307
|
+
"use the apply_url deeplink for the full JD.",
|
|
308
|
+
post_id: postId,
|
|
309
|
+
apply_url: `${PORTAL_URL}#/jobs/${encodeURIComponent(postId)}`,
|
|
113
310
|
};
|
|
114
311
|
}
|
|
115
|
-
// ---- fetchDictionaries ----
|
|
116
312
|
export async function fetchDictionaries() {
|
|
313
|
+
const portal = await fetchPortalHtml();
|
|
314
|
+
if (!portal.ok || !portal.html) {
|
|
315
|
+
return { ok: false, source: SOURCE, message: portal.message };
|
|
316
|
+
}
|
|
317
|
+
const init = parseInitData(portal.html);
|
|
318
|
+
if (!init) {
|
|
319
|
+
return { ok: false, source: SOURCE, message: "Moka init-data missing" };
|
|
320
|
+
}
|
|
117
321
|
return {
|
|
118
|
-
ok:
|
|
322
|
+
ok: true,
|
|
119
323
|
source: SOURCE,
|
|
120
|
-
|
|
324
|
+
locations: init.jobsGroupedByLocation ?? [],
|
|
325
|
+
moka_org: { slug: ORG_SLUG, siteId: SITE_ID, url: PORTAL_URL },
|
|
121
326
|
};
|
|
122
327
|
}
|
|
123
|
-
// ---- notices ----
|
|
124
328
|
export async function listNotices() {
|
|
125
329
|
return {
|
|
126
330
|
ok: false,
|
|
@@ -142,13 +346,37 @@ export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
|
142
346
|
message: "StepFun (阶跃星辰): no public notices endpoint",
|
|
143
347
|
};
|
|
144
348
|
}
|
|
145
|
-
|
|
146
|
-
|
|
349
|
+
export async function matchResume(text, opts = {}) {
|
|
350
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
351
|
+
const candidates = Math.max(20, opts.candidates ?? 100);
|
|
352
|
+
const all = await fetchAllPositions({
|
|
353
|
+
pageSize: 20,
|
|
354
|
+
maxPages: Math.ceil(candidates / 15),
|
|
355
|
+
});
|
|
356
|
+
if (!all.ok) {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
source: SOURCE,
|
|
360
|
+
extracted_terms: terms,
|
|
361
|
+
city_preferences: cities,
|
|
362
|
+
matches: [],
|
|
363
|
+
message: all.message,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const topN = Math.max(1, opts.topN ?? 10);
|
|
367
|
+
const scored = all.positions
|
|
368
|
+
.map((p) => ({
|
|
369
|
+
p,
|
|
370
|
+
score: scoreOverlap(`${p.title} ${p.project} ${p.bgs}`, terms, cities).score,
|
|
371
|
+
}))
|
|
372
|
+
.sort((a, b) => b.score - a.score)
|
|
373
|
+
.slice(0, topN)
|
|
374
|
+
.map((x) => x.p);
|
|
147
375
|
return {
|
|
148
|
-
ok:
|
|
376
|
+
ok: true,
|
|
149
377
|
source: SOURCE,
|
|
150
|
-
|
|
151
|
-
|
|
378
|
+
extracted_terms: terms,
|
|
379
|
+
city_preferences: cities,
|
|
380
|
+
matches: scored,
|
|
152
381
|
};
|
|
153
382
|
}
|
|
154
|
-
export { extractResumeSignals, scoreOverlap };
|