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/mihoyo.js
CHANGED
|
@@ -1,100 +1,274 @@
|
|
|
1
|
-
// Thin client for 米哈游 / miHoYo
|
|
1
|
+
// Thin client for 米哈游 / miHoYo recruiting portal.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// from JS bundles we can't traverse without a headless browser.
|
|
3
|
+
// Portal: https://jobs.mihoyo.com/ (the old campus.mihoyo.com permanently
|
|
4
|
+
// redirects here)
|
|
5
|
+
// API host: https://ats.openout.mihoyo.com/ats-portal
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// careers.mihoyo.com → SSL handshake timed out (geo-blocked / VPN-only?)
|
|
11
|
-
// hr.mihoyo.com → 404
|
|
12
|
-
// mihoyo.jobs.feishu.cn → POST 400 on every channel string tried
|
|
13
|
-
// ("campus" / "mihoyo" / "social" / "1" / "school_recruit").
|
|
14
|
-
// GET returns a generic Feishu shell — meaning
|
|
15
|
-
// miHoYo is NOT a real Feishu Recruiting tenant.
|
|
16
|
-
// careers.mihoyo.com via Workday → not configured
|
|
17
|
-
// mihoyo.mokahr.com → Moka requires session auth (verified for the
|
|
18
|
-
// class of orgs — Moka public anon API is gated)
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Discovery (2026-05):
|
|
19
9
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
10
|
+
// campus.mihoyo.com → permanently redirects to jobs.mihoyo.com
|
|
11
|
+
// jobs.mihoyo.com → React SPA shell
|
|
12
|
+
// ats.openout.mihoyo.com → real ATS backend (in the bundle: baseURL)
|
|
13
|
+
//
|
|
14
|
+
// The bundle whitelist contains /v1/job/category/list, /v1/job/get/id_list,
|
|
15
|
+
// /v1/job/project_count/list — but the actual search endpoint that returns
|
|
16
|
+
// summarized job rows (the one the SPA hits to render the list page) is
|
|
17
|
+
// /v1/job/list (probed; unauth-OK with channelDetailIds + hireType + pageNo).
|
|
18
|
+
// /v1/job/info gives full per-position detail.
|
|
19
|
+
//
|
|
20
|
+
// "channel" semantics (decoded from the bundle's enums):
|
|
21
|
+
// R.CAMPUS = 1, R.JOBS = 1 (same value), R.RECOMMEND = 2
|
|
22
|
+
// hireType enum: JOBS = 0 (social), CAMPUS = 1
|
|
23
|
+
// Default surface = social: channelDetailIds=[1], hireType=0.
|
|
24
|
+
//
|
|
25
|
+
// ============================================================
|
|
26
|
+
// Response shape (probed 2026-05):
|
|
27
|
+
// data.list[]:
|
|
28
|
+
// id, title, competencyType, jobNature, projectName,
|
|
29
|
+
// addressDetailList[].addressDetail, channelDetailIds
|
|
30
|
+
// data.total — canonical total count
|
|
31
|
+
//
|
|
32
|
+
// PositionSummary field mapping:
|
|
33
|
+
// post_id ← String(job.id)
|
|
34
|
+
// title ← job.title
|
|
35
|
+
// project ← job.competencyType (job category)
|
|
36
|
+
// recruit_label ← job.jobNature ("全职" / "实习")
|
|
37
|
+
// bgs ← job.projectName ("社会招聘" / "校园招聘")
|
|
38
|
+
// work_cities ← addressDetailList[].addressDetail joined " / "
|
|
39
|
+
// apply_url ← https://jobs.mihoyo.com/#/position/${id}
|
|
40
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
41
|
+
export { checkResume, extractResumeSignals, scoreOverlap };
|
|
42
|
+
const SOURCE = "jobs.mihoyo.com";
|
|
43
|
+
const API_ROOT = "https://ats.openout.mihoyo.com/ats-portal";
|
|
44
|
+
const PORTAL_URL = "https://jobs.mihoyo.com";
|
|
45
|
+
const APPLY_URL_PREFIX = `${PORTAL_URL}/#/position`;
|
|
46
|
+
// Default channel: social ("社招"). Bundle constant R.JOBS = 1.
|
|
47
|
+
const CHANNEL_DETAIL_IDS = [1];
|
|
48
|
+
const HIRE_TYPE_SOCIAL = 0;
|
|
49
|
+
const HEADERS = {
|
|
50
|
+
"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",
|
|
51
|
+
Accept: "application/json, text/plain, */*",
|
|
52
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
Origin: PORTAL_URL,
|
|
55
|
+
Referer: `${PORTAL_URL}/`,
|
|
56
|
+
};
|
|
57
|
+
async function postJson(path, body) {
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await fetch(`${API_ROOT}${path}`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: HEADERS,
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok)
|
|
70
|
+
return { ok: false, message: `HTTP ${response.status}` };
|
|
71
|
+
let payload;
|
|
72
|
+
try {
|
|
73
|
+
payload = (await response.json());
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
77
|
+
}
|
|
78
|
+
if (payload.code !== 0 || !payload.data) {
|
|
79
|
+
return { ok: false, message: payload.message || "upstream error" };
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, data: payload.data, message: "ok" };
|
|
82
|
+
}
|
|
83
|
+
function summarize(row) {
|
|
84
|
+
const id = String(row.id ?? "");
|
|
85
|
+
const cities = (row.addressDetailList ?? [])
|
|
86
|
+
.map((a) => a.addressDetail ?? "")
|
|
87
|
+
.filter(Boolean);
|
|
32
88
|
return {
|
|
33
|
-
|
|
89
|
+
post_id: id,
|
|
90
|
+
title: row.title ?? "",
|
|
91
|
+
project: row.competencyType ?? "",
|
|
92
|
+
recruit_label: row.jobNature ?? "",
|
|
93
|
+
bgs: row.projectName ?? "",
|
|
94
|
+
work_cities: cities.join(" / "),
|
|
95
|
+
apply_url: id ? `${APPLY_URL_PREFIX}/${encodeURIComponent(id)}` : PORTAL_URL,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ---------- searchPositions ----------
|
|
99
|
+
export async function searchPositions(opts = {}) {
|
|
100
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
101
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
102
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
103
|
+
const body = {
|
|
104
|
+
channelDetailIds: opts.channelDetailIds ?? CHANNEL_DETAIL_IDS,
|
|
105
|
+
hireType: opts.hireType ?? HIRE_TYPE_SOCIAL,
|
|
106
|
+
pageSize,
|
|
107
|
+
pageNo: page,
|
|
108
|
+
};
|
|
109
|
+
if (keyword)
|
|
110
|
+
body.jobName = keyword;
|
|
111
|
+
const response = await postJson("/v1/job/list", body);
|
|
112
|
+
if (!response.ok || !response.data) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
message: response.message,
|
|
116
|
+
source: SOURCE,
|
|
117
|
+
query: body,
|
|
118
|
+
positions: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const rows = response.data.list ?? [];
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
34
124
|
source: SOURCE,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
125
|
+
query: body,
|
|
126
|
+
page,
|
|
127
|
+
page_size: pageSize,
|
|
128
|
+
total: response.data.total ?? rows.length,
|
|
129
|
+
positions: rows.map(summarize),
|
|
38
130
|
};
|
|
39
131
|
}
|
|
40
|
-
|
|
132
|
+
// ---------- fetchAllPositions ----------
|
|
133
|
+
export async function fetchAllPositions(opts = {}) {
|
|
134
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
|
|
135
|
+
const maxPages = Math.max(1, opts.maxPages ?? 10);
|
|
136
|
+
const bucket = [];
|
|
137
|
+
let total;
|
|
138
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
139
|
+
const result = await searchPositions({ ...opts, page, pageSize });
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
message: result.message,
|
|
144
|
+
source: SOURCE,
|
|
145
|
+
fetched: bucket.length,
|
|
146
|
+
positions: bucket,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (total === undefined)
|
|
150
|
+
total = result.total;
|
|
151
|
+
if (!result.positions.length)
|
|
152
|
+
break;
|
|
153
|
+
bucket.push(...result.positions);
|
|
154
|
+
if (total !== undefined && bucket.length >= total)
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
41
157
|
return {
|
|
42
|
-
ok:
|
|
158
|
+
ok: true,
|
|
43
159
|
source: SOURCE,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
positions: [],
|
|
160
|
+
total: total ?? bucket.length,
|
|
161
|
+
fetched: bucket.length,
|
|
162
|
+
positions: bucket,
|
|
48
163
|
};
|
|
49
164
|
}
|
|
165
|
+
// ---------- fetchPositionDetail ----------
|
|
50
166
|
export async function fetchPositionDetail(postId) {
|
|
167
|
+
const id = (postId ?? "").trim();
|
|
168
|
+
if (!id)
|
|
169
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
170
|
+
const response = await postJson("/v1/job/info", { id });
|
|
171
|
+
if (!response.ok || !response.data) {
|
|
172
|
+
return { ok: false, source: SOURCE, message: response.message, post_id: id };
|
|
173
|
+
}
|
|
174
|
+
const d = response.data;
|
|
175
|
+
const summary = summarize(d);
|
|
51
176
|
return {
|
|
52
|
-
ok:
|
|
177
|
+
ok: true,
|
|
53
178
|
source: SOURCE,
|
|
54
|
-
|
|
55
|
-
|
|
179
|
+
post_id: summary.post_id,
|
|
180
|
+
title: d.title ?? "",
|
|
181
|
+
direction: d.objectName ?? "",
|
|
182
|
+
description: d.description ?? "",
|
|
183
|
+
requirements: d.jobRequire ?? "",
|
|
184
|
+
addition: d.addition ?? "",
|
|
185
|
+
work_cities: d.addressDetailList ?? [],
|
|
186
|
+
project: d.competencyType ?? "",
|
|
187
|
+
recruit_label: d.jobNature ?? "",
|
|
188
|
+
hire_type_name: d.hireTypeName ?? "",
|
|
189
|
+
apply_url: summary.apply_url,
|
|
56
190
|
};
|
|
57
191
|
}
|
|
192
|
+
// ---------- fetchDictionaries ----------
|
|
193
|
+
let _filterCache = null;
|
|
58
194
|
export async function fetchDictionaries() {
|
|
59
|
-
|
|
60
|
-
|
|
195
|
+
if (_filterCache !== null)
|
|
196
|
+
return _filterCache;
|
|
197
|
+
const social = await postJson("/v1/job/category/list", {
|
|
198
|
+
channelDetailIds: CHANNEL_DETAIL_IDS,
|
|
199
|
+
hireType: HIRE_TYPE_SOCIAL,
|
|
200
|
+
});
|
|
201
|
+
const campus = await postJson("/v1/job/category/list", { channelDetailIds: CHANNEL_DETAIL_IDS, hireType: 1 });
|
|
202
|
+
if (!social.ok && !campus.ok) {
|
|
203
|
+
const result = {
|
|
204
|
+
ok: false,
|
|
205
|
+
source: SOURCE,
|
|
206
|
+
message: social.message || campus.message,
|
|
207
|
+
};
|
|
208
|
+
_filterCache = result;
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
const mapList = (list) => list.map((c) => ({
|
|
212
|
+
competencyType: c.competencyType ?? "",
|
|
213
|
+
competencyTypeName: c.competencyTypeName ?? "",
|
|
214
|
+
competencyTypeEnName: c.competencyTypeEnName ?? "",
|
|
215
|
+
count: c.count ?? 0,
|
|
216
|
+
}));
|
|
217
|
+
const result = {
|
|
218
|
+
ok: true,
|
|
61
219
|
source: SOURCE,
|
|
62
|
-
|
|
220
|
+
categories_social: social.ok && social.data ? mapList(social.data) : [],
|
|
221
|
+
categories_campus: campus.ok && campus.data ? mapList(campus.data) : [],
|
|
63
222
|
};
|
|
223
|
+
_filterCache = result;
|
|
224
|
+
return result;
|
|
64
225
|
}
|
|
226
|
+
// ---------- stub notices ----------
|
|
227
|
+
const NOTICES_STUB = {
|
|
228
|
+
ok: false,
|
|
229
|
+
source: SOURCE,
|
|
230
|
+
message: "miHoYo: no public notices endpoint",
|
|
231
|
+
};
|
|
65
232
|
export async function listNotices() {
|
|
66
|
-
return {
|
|
67
|
-
ok: false,
|
|
68
|
-
source: SOURCE,
|
|
69
|
-
message: STUB_MESSAGE,
|
|
70
|
-
notices: [],
|
|
71
|
-
};
|
|
233
|
+
return { ...NOTICES_STUB, notices: [] };
|
|
72
234
|
}
|
|
73
235
|
export async function getNotice(noticeId) {
|
|
74
|
-
return {
|
|
75
|
-
ok: false,
|
|
76
|
-
source: SOURCE,
|
|
77
|
-
message: STUB_MESSAGE,
|
|
78
|
-
notice_id: noticeId,
|
|
79
|
-
};
|
|
236
|
+
return { ...NOTICES_STUB, notice_id: noticeId };
|
|
80
237
|
}
|
|
81
238
|
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
82
|
-
return {
|
|
83
|
-
ok: false,
|
|
84
|
-
source: SOURCE,
|
|
85
|
-
question,
|
|
86
|
-
message: STUB_MESSAGE,
|
|
87
|
-
matches: [],
|
|
88
|
-
};
|
|
239
|
+
return { ...NOTICES_STUB, question, matches: [] };
|
|
89
240
|
}
|
|
90
|
-
|
|
241
|
+
// ---------- matchResume ----------
|
|
242
|
+
export async function matchResume(text, opts = {}) {
|
|
243
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
244
|
+
const candidates = Math.max(topN, opts.candidates ?? 100);
|
|
91
245
|
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
246
|
+
if (!terms.length) {
|
|
247
|
+
return {
|
|
248
|
+
ok: false,
|
|
249
|
+
source: SOURCE,
|
|
250
|
+
message: "could not extract any technical signals from the text",
|
|
251
|
+
preview: (text ?? "").slice(0, 120),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const keyword = terms[0];
|
|
255
|
+
const list = await searchPositions({ keyword, page: 1, pageSize: Math.min(100, candidates) });
|
|
256
|
+
if (!list.ok) {
|
|
257
|
+
return { ok: false, source: SOURCE, message: list.message, positions: [] };
|
|
258
|
+
}
|
|
259
|
+
const scored = (list.positions ?? [])
|
|
260
|
+
.map((p) => ({
|
|
261
|
+
p,
|
|
262
|
+
score: scoreOverlap(`${p.title} ${p.project} ${p.bgs}`, terms, cities).score,
|
|
263
|
+
}))
|
|
264
|
+
.sort((a, b) => b.score - a.score)
|
|
265
|
+
.slice(0, topN)
|
|
266
|
+
.map((x) => x.p);
|
|
92
267
|
return {
|
|
93
|
-
ok:
|
|
268
|
+
ok: true,
|
|
94
269
|
source: SOURCE,
|
|
95
270
|
extracted_terms: terms,
|
|
96
271
|
city_preferences: cities,
|
|
97
|
-
matches:
|
|
98
|
-
message: STUB_MESSAGE,
|
|
272
|
+
matches: scored,
|
|
99
273
|
};
|
|
100
274
|
}
|