@ha7ch/job-pro 1.0.93
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/agibot.js +399 -0
- package/dist/alibaba.js +509 -0
- package/dist/antgroup.js +397 -0
- package/dist/apply.js +1373 -0
- package/dist/baichuan.js +49 -0
- package/dist/baidu.js +452 -0
- package/dist/bilibili.js +455 -0
- package/dist/byd.js +412 -0
- package/dist/bytedance.js +619 -0
- package/dist/cainiao.js +56 -0
- package/dist/cambricon.js +33 -0
- package/dist/cdp.js +237 -0
- package/dist/cicc.js +56 -0
- package/dist/coverage.js +60 -0
- package/dist/deepseek.js +25 -0
- package/dist/didi.js +381 -0
- package/dist/feishu.js +577 -0
- package/dist/galaxyuniversal.js +24 -0
- package/dist/geely.js +35 -0
- package/dist/greenhouse.js +432 -0
- package/dist/hikvision.js +58 -0
- package/dist/horizonrobotics.js +46 -0
- package/dist/hoyoverse.js +26 -0
- package/dist/huawei.js +537 -0
- package/dist/iflytek.js +380 -0
- package/dist/index.js +1828 -0
- package/dist/iqiyi.js +494 -0
- package/dist/jd.js +559 -0
- package/dist/kuaishou.js +496 -0
- package/dist/lever.js +455 -0
- package/dist/liauto.js +393 -0
- package/dist/liepin.js +357 -0
- package/dist/lilith.js +300 -0
- package/dist/megvii.js +27 -0
- package/dist/meituan.js +633 -0
- package/dist/memory.js +76 -0
- package/dist/mihoyo.js +308 -0
- package/dist/minimax.js +32 -0
- package/dist/moka.js +473 -0
- package/dist/moonshot.js +24 -0
- package/dist/netease.js +424 -0
- package/dist/nio.js +24 -0
- package/dist/oppo.js +285 -0
- package/dist/pdd.js +614 -0
- package/dist/pingan.js +493 -0
- package/dist/sensetime.js +51 -0
- package/dist/sf.js +310 -0
- package/dist/stepfun.js +24 -0
- package/dist/tencent.js +770 -0
- package/dist/trip.js +396 -0
- package/dist/unitree.js +418 -0
- package/dist/vivo.js +361 -0
- package/dist/webank.js +55 -0
- package/dist/wecruit.js +438 -0
- package/dist/weibo.js +337 -0
- package/dist/weride.js +29 -0
- package/dist/xiaohongshu.js +480 -0
- package/dist/xiaomi.js +529 -0
- package/dist/xpeng.js +34 -0
- package/dist/zerooneai.js +42 -0
- package/dist/zhipu.js +478 -0
- package/extension/README.md +79 -0
- package/extension/background.js +177 -0
- package/extension/manifest.json +55 -0
- package/extension/popup.html +37 -0
- package/extension/popup.js +54 -0
- package/package.json +61 -0
package/dist/xiaomi.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
// Thin client for Xiaomi's public campus-recruiting API.
|
|
2
|
+
//
|
|
3
|
+
// Xiaomi does NOT use jobs.bytedance.com or xiaomi.jobs.feishu.cn.
|
|
4
|
+
// It self-hosts the ByteDance ATSX (飞书招聘) platform at:
|
|
5
|
+
//
|
|
6
|
+
// https://xiaomi.jobs.f.mioffice.cn/ (mioffice.cn = Xiaomi's Feishu fork)
|
|
7
|
+
//
|
|
8
|
+
// The API shape is IDENTICAL to jobs.bytedance.com:
|
|
9
|
+
// POST /api/v1/search/job/posts
|
|
10
|
+
// GET /api/v1/config/job/filters/{path}
|
|
11
|
+
//
|
|
12
|
+
// The key difference: Xiaomi requires three portal-scoping headers to switch
|
|
13
|
+
// between campus (校招) and internship (实习) pools:
|
|
14
|
+
// portal-channel: "campus" | "internship"
|
|
15
|
+
// portal-platform: "pc"
|
|
16
|
+
// website-path: "campus" | "internship"
|
|
17
|
+
//
|
|
18
|
+
// Without those headers the API defaults to 社招 (experienced/social hire).
|
|
19
|
+
//
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Endpoint inventory (probed 2026-05, API identical to ByteDance ATSX):
|
|
22
|
+
//
|
|
23
|
+
// POST https://xiaomi.jobs.f.mioffice.cn/api/v1/search/job/posts
|
|
24
|
+
// Payload: { keyword, limit, offset, portal_type:3, portal_entrance:1,
|
|
25
|
+
// language:"zh", recruitment_id_list?, job_function_id_list?,
|
|
26
|
+
// location_code_list?, subject_id_list? }
|
|
27
|
+
// Response: { code:0, data:{ job_post_list:[...], count:<int> }, message:"ok" }
|
|
28
|
+
//
|
|
29
|
+
// GET https://xiaomi.jobs.f.mioffice.cn/api/v1/config/job/filters/campus
|
|
30
|
+
// Returns: { job_function_list, city_list, recruitment_type_list,
|
|
31
|
+
// job_subject_list, ... }
|
|
32
|
+
//
|
|
33
|
+
// ============================================================
|
|
34
|
+
// Portal pools (controlled by headers, confirmed 2026-05):
|
|
35
|
+
//
|
|
36
|
+
// portal-channel: "campus" → 357 posts (正式 / new-grad, 招聘类型=校招)
|
|
37
|
+
// portal-channel: "internship" → 729 posts (实习 / intern, 招聘类型=校招)
|
|
38
|
+
// no channel header → 2681 posts (社招 / experienced, NOT campus)
|
|
39
|
+
//
|
|
40
|
+
// ============================================================
|
|
41
|
+
// Filter taxonomy (from GET /api/v1/config/job/filters/campus, portal-channel: campus):
|
|
42
|
+
//
|
|
43
|
+
// DIMENSION 1 — job_function_id_list (职能类别)
|
|
44
|
+
// 7178759516879405165 = 软件研发类 / Software R&D
|
|
45
|
+
// 7178830559051874412 = 硬件研发类 / Hardware R&D
|
|
46
|
+
// 7467761476330340460 = 算法类 / Algorithm
|
|
47
|
+
// 7542849286137479277 = 芯片类 / Chip
|
|
48
|
+
// 7467761529010634860 = 测试类 / Testing
|
|
49
|
+
// 7467761246949179500 = 运维类 / Maintenance
|
|
50
|
+
// 7178035552473448557 = 产品类 / Product
|
|
51
|
+
// 7178035552473464941 = 设计类 / Design
|
|
52
|
+
// 7178830559051858028 = 外语外派类 / Global Expatriate
|
|
53
|
+
// 7178759516879388781 = 服务类 / Service
|
|
54
|
+
// 7178035552473481325 = 运营类 / Operation
|
|
55
|
+
// 7178035552473497709 = 市场类 / Marketing
|
|
56
|
+
// 7178035552473514093 = 职能类 / Corporate Function
|
|
57
|
+
// 7178035552473530477 = 供应链类 / Supply Chain
|
|
58
|
+
// 7493065498218479788 = 汽车工程类 / Automotive Engineering
|
|
59
|
+
// 7493065498218496172 = 汽车销售类 / Automotive Sales
|
|
60
|
+
// 7493065498218512556 = 汽车服务类 / Automotive Service
|
|
61
|
+
// 7493065498218528940 = 数据类 / Data
|
|
62
|
+
//
|
|
63
|
+
// DIMENSION 2 — location_code_list (工作地点, city codes — 56 cities total)
|
|
64
|
+
// CT_11=北京 CT_125=上海 CT_128=深圳 CT_154=武汉 CT_107=南京 CT_155=西安
|
|
65
|
+
// CT_163=新加坡 CT_199=苏州 CT_66=济南 CT_25=大连 (+46 more)
|
|
66
|
+
//
|
|
67
|
+
// DIMENSION 3 — recruitment_id_list (campus pool filters)
|
|
68
|
+
// "201" = 正式 (new-grad, matches default campus tab)
|
|
69
|
+
// "202" = 实习 (intern — use portal-channel: internship for this pool)
|
|
70
|
+
//
|
|
71
|
+
// DIMENSION 4 — job_subject_list (special programs, campus pool, 2 active 2026-05)
|
|
72
|
+
// "7532449299457327213" = 2026届境外校招计划 (overseas campus)
|
|
73
|
+
// "7603687083995121983" = 2026届春季校招计划 (spring campus)
|
|
74
|
+
//
|
|
75
|
+
// ============================================================
|
|
76
|
+
// Detail page URLs (both return HTTP 200):
|
|
77
|
+
// campus: https://xiaomi.jobs.f.mioffice.cn/campus/position/${id}/detail
|
|
78
|
+
// internship: https://xiaomi.jobs.f.mioffice.cn/internship/position/${id}/detail
|
|
79
|
+
//
|
|
80
|
+
// ============================================================
|
|
81
|
+
// Feishu/ATSX platform note:
|
|
82
|
+
// Xiaomi uses its own Feishu fork (mioffice.cn) running ByteDance's ATSX
|
|
83
|
+
// recruiting backend. The API is STRUCTURALLY IDENTICAL to jobs.bytedance.com —
|
|
84
|
+
// same POST body shape, same response envelope (code/data/message), same field
|
|
85
|
+
// names, same city codes (CT_xx). The ONLY differences are:
|
|
86
|
+
// 1. Domain: *.f.mioffice.cn instead of jobs.bytedance.com
|
|
87
|
+
// 2. Portal scoping via portal-channel / website-path headers
|
|
88
|
+
// Any future company on Feishu Recruiting (feishu.cn/jobs.*.feishu.cn or
|
|
89
|
+
// *.jobs.f.mioffice.cn) can be adapted from this file with ~10 lines of change.
|
|
90
|
+
//
|
|
91
|
+
// ============================================================
|
|
92
|
+
// ---- PositionSummary field mapping (Xiaomi → canonical) ----
|
|
93
|
+
// post_id ← item.id (stringified)
|
|
94
|
+
// title ← item.title
|
|
95
|
+
// project ← item.job_function.name (职能类别; job_category is null in campus)
|
|
96
|
+
// recruit_label ← item.recruit_type.name (e.g. "正式" / "实习")
|
|
97
|
+
// bgs ← "" (not exposed in public search)
|
|
98
|
+
// work_cities ← item.city_info.name + city_list joined with " / " for multi-city
|
|
99
|
+
// apply_url ← https://xiaomi.jobs.f.mioffice.cn/campus/position/${id}/detail
|
|
100
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
101
|
+
export { checkResume };
|
|
102
|
+
const API_ROOT = "https://xiaomi.jobs.f.mioffice.cn/api/v1";
|
|
103
|
+
const CAMPUS_PAGE = "https://xiaomi.jobs.f.mioffice.cn/campus/";
|
|
104
|
+
const INTERN_PAGE = "https://xiaomi.jobs.f.mioffice.cn/internship/";
|
|
105
|
+
const CAMPUS_DETAIL = (id) => `https://xiaomi.jobs.f.mioffice.cn/campus/position/${encodeURIComponent(id)}/detail`;
|
|
106
|
+
const INTERN_DETAIL = (id) => `https://xiaomi.jobs.f.mioffice.cn/internship/position/${encodeURIComponent(id)}/detail`;
|
|
107
|
+
function makeHeaders(channel) {
|
|
108
|
+
return {
|
|
109
|
+
"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",
|
|
110
|
+
Accept: "application/json, text/plain, */*",
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
"portal-channel": channel,
|
|
113
|
+
"portal-platform": "pc",
|
|
114
|
+
"website-path": channel,
|
|
115
|
+
Referer: channel === "campus" ? CAMPUS_PAGE : INTERN_PAGE,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function call(path, body, channel = "campus") {
|
|
119
|
+
const url = `${API_ROOT}${path}`;
|
|
120
|
+
let response;
|
|
121
|
+
try {
|
|
122
|
+
response = await fetch(url, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: makeHeaders(channel),
|
|
125
|
+
body: JSON.stringify(body),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
136
|
+
}
|
|
137
|
+
let payload;
|
|
138
|
+
try {
|
|
139
|
+
payload = (await response.json());
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ok: payload.code === 0,
|
|
146
|
+
data: payload.data,
|
|
147
|
+
message: payload.message || (payload.code === 0 ? "ok" : "upstream error"),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function summarizePosition(item, channel) {
|
|
151
|
+
const id = String(item.id ?? "");
|
|
152
|
+
const cityList = item.city_list ?? [];
|
|
153
|
+
let work_cities;
|
|
154
|
+
if (cityList.length > 1) {
|
|
155
|
+
work_cities = cityList
|
|
156
|
+
.map((c) => c.name ?? "")
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join(" / ");
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
work_cities = item.city_info?.name ?? (cityList[0]?.name ?? "");
|
|
162
|
+
}
|
|
163
|
+
// Xiaomi's campus API returns job_category as null; job_function carries the category name
|
|
164
|
+
const project = item.job_function?.name ?? item.job_category?.name ?? "";
|
|
165
|
+
const detailFn = channel === "internship" ? INTERN_DETAIL : CAMPUS_DETAIL;
|
|
166
|
+
return {
|
|
167
|
+
post_id: id,
|
|
168
|
+
title: item.title ?? "",
|
|
169
|
+
project,
|
|
170
|
+
recruit_label: item.recruit_type?.name ?? "",
|
|
171
|
+
bgs: "",
|
|
172
|
+
work_cities,
|
|
173
|
+
apply_url: id ? detailFn(id) : (channel === "internship" ? INTERN_PAGE : CAMPUS_PAGE),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// ---------- searchPositions ----------
|
|
177
|
+
export async function searchPositions(opts = {}) {
|
|
178
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
179
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
180
|
+
const offset = (page - 1) * pageSize;
|
|
181
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
182
|
+
const channel = opts.channel ?? "campus";
|
|
183
|
+
const asStringList = (v) => {
|
|
184
|
+
if (v === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
const arr = Array.isArray(v) ? v : [v];
|
|
187
|
+
return arr.map(String);
|
|
188
|
+
};
|
|
189
|
+
// Default filter: 201=正式 in campus channel, 202=实习 in internship channel
|
|
190
|
+
const defaultRecruitId = channel === "internship" ? "202" : "201";
|
|
191
|
+
const recruitmentIdList = asStringList(opts.recruitmentIdList) ?? [defaultRecruitId];
|
|
192
|
+
const payload = {
|
|
193
|
+
keyword,
|
|
194
|
+
limit: pageSize,
|
|
195
|
+
offset,
|
|
196
|
+
portal_type: 3,
|
|
197
|
+
portal_entrance: 1,
|
|
198
|
+
language: "zh",
|
|
199
|
+
recruitment_id_list: recruitmentIdList,
|
|
200
|
+
};
|
|
201
|
+
const jobFunctionIdList = asStringList(opts.jobFunctionIdList);
|
|
202
|
+
if (jobFunctionIdList?.length) {
|
|
203
|
+
payload.job_function_id_list = jobFunctionIdList;
|
|
204
|
+
}
|
|
205
|
+
const cityIdList = asStringList(opts.cityIdList);
|
|
206
|
+
if (cityIdList?.length) {
|
|
207
|
+
payload.location_code_list = cityIdList;
|
|
208
|
+
}
|
|
209
|
+
const subjectIdList = asStringList(opts.subjectIdList);
|
|
210
|
+
if (subjectIdList?.length) {
|
|
211
|
+
payload.subject_id_list = subjectIdList;
|
|
212
|
+
}
|
|
213
|
+
const response = await call("/search/job/posts", payload, channel);
|
|
214
|
+
if (!response.ok || !response.data) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
message: response.message,
|
|
218
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
219
|
+
query: payload,
|
|
220
|
+
positions: [],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const rows = response.data.job_post_list ?? [];
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
227
|
+
query: payload,
|
|
228
|
+
channel,
|
|
229
|
+
page,
|
|
230
|
+
page_size: pageSize,
|
|
231
|
+
total: response.data.count ?? rows.length,
|
|
232
|
+
positions: rows.map((r) => summarizePosition(r, channel)),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// ---------- fetchAllPositions ----------
|
|
236
|
+
export async function fetchAllPositions(opts = {}) {
|
|
237
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
|
|
238
|
+
const maxPages = Math.max(1, opts.maxPages ?? 5);
|
|
239
|
+
const channel = opts.channel ?? "campus";
|
|
240
|
+
const bucket = [];
|
|
241
|
+
let total;
|
|
242
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
243
|
+
const result = await searchPositions({ ...opts, page, pageSize, channel });
|
|
244
|
+
if (!result.ok) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
message: result.message,
|
|
248
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
249
|
+
fetched: bucket.length,
|
|
250
|
+
positions: bucket,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (total === undefined)
|
|
254
|
+
total = result.total;
|
|
255
|
+
if (!result.positions.length)
|
|
256
|
+
break;
|
|
257
|
+
bucket.push(...result.positions);
|
|
258
|
+
if (total !== undefined && bucket.length >= total)
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
ok: true,
|
|
263
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
264
|
+
channel,
|
|
265
|
+
total: total ?? bucket.length,
|
|
266
|
+
fetched: bucket.length,
|
|
267
|
+
positions: bucket,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// ---------- fetchPositionDetail ----------
|
|
271
|
+
// Xiaomi has no public per-post detail REST endpoint.
|
|
272
|
+
// We paginate the search and filter by id (same strategy as bytedance.ts).
|
|
273
|
+
export async function fetchPositionDetail(postId, opts = {}) {
|
|
274
|
+
const id = (postId ?? "").trim();
|
|
275
|
+
const channel = opts.channel ?? "campus";
|
|
276
|
+
if (!id) {
|
|
277
|
+
return { ok: false, source: "xiaomi.jobs.f.mioffice.cn", message: "post_id is required" };
|
|
278
|
+
}
|
|
279
|
+
const pageSize = 100;
|
|
280
|
+
const maxPages = 5;
|
|
281
|
+
const defaultRecruitId = channel === "internship" ? "202" : "201";
|
|
282
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
283
|
+
const offset = (page - 1) * pageSize;
|
|
284
|
+
const payload = {
|
|
285
|
+
keyword: "",
|
|
286
|
+
limit: pageSize,
|
|
287
|
+
offset,
|
|
288
|
+
portal_type: 3,
|
|
289
|
+
portal_entrance: 1,
|
|
290
|
+
language: "zh",
|
|
291
|
+
recruitment_id_list: [defaultRecruitId],
|
|
292
|
+
};
|
|
293
|
+
const response = await call("/search/job/posts", payload, channel);
|
|
294
|
+
if (!response.ok || !response.data)
|
|
295
|
+
break;
|
|
296
|
+
const posts = response.data.job_post_list ?? [];
|
|
297
|
+
const found = posts.find((p) => String(p.id) === id);
|
|
298
|
+
if (found) {
|
|
299
|
+
const summary = summarizePosition(found, channel);
|
|
300
|
+
return {
|
|
301
|
+
ok: true,
|
|
302
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
303
|
+
post_id: id,
|
|
304
|
+
title: found.title ?? "",
|
|
305
|
+
direction: found.sub_title ?? "",
|
|
306
|
+
description: found.description ?? "",
|
|
307
|
+
requirements: found.requirement ?? "",
|
|
308
|
+
work_cities: found.city_list ?? (found.city_info ? [found.city_info] : []),
|
|
309
|
+
apply_url: summary.apply_url,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (posts.length < pageSize)
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
318
|
+
post_id: id,
|
|
319
|
+
message: `post ${id} not found in ${channel} pool (searched up to ${maxPages * pageSize} posts)`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
let _filterCache = null;
|
|
323
|
+
export async function fetchDictionaries() {
|
|
324
|
+
if (_filterCache !== null)
|
|
325
|
+
return _filterCache;
|
|
326
|
+
const url = `${API_ROOT}/config/job/filters/campus`;
|
|
327
|
+
let response;
|
|
328
|
+
try {
|
|
329
|
+
response = await fetch(url, { headers: makeHeaders("campus") });
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
const r = {
|
|
333
|
+
ok: false,
|
|
334
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
335
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
336
|
+
};
|
|
337
|
+
_filterCache = r;
|
|
338
|
+
return r;
|
|
339
|
+
}
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
const r = {
|
|
342
|
+
ok: false,
|
|
343
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
344
|
+
message: `HTTP ${response.status}`,
|
|
345
|
+
};
|
|
346
|
+
_filterCache = r;
|
|
347
|
+
return r;
|
|
348
|
+
}
|
|
349
|
+
let payload;
|
|
350
|
+
try {
|
|
351
|
+
payload = await response.json();
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
const r = {
|
|
355
|
+
ok: false,
|
|
356
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
357
|
+
message: `bad JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
358
|
+
};
|
|
359
|
+
_filterCache = r;
|
|
360
|
+
return r;
|
|
361
|
+
}
|
|
362
|
+
if (payload.code !== 0 || !payload.data) {
|
|
363
|
+
const r = {
|
|
364
|
+
ok: false,
|
|
365
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
366
|
+
message: payload.message ?? "upstream error",
|
|
367
|
+
};
|
|
368
|
+
_filterCache = r;
|
|
369
|
+
return r;
|
|
370
|
+
}
|
|
371
|
+
const d = payload.data;
|
|
372
|
+
const jobFunctions = (d.job_function_list ?? []).map((f) => ({
|
|
373
|
+
id: f.id ?? "",
|
|
374
|
+
name: f.name ?? "",
|
|
375
|
+
en_name: f.en_name ?? "",
|
|
376
|
+
}));
|
|
377
|
+
const cities = (d.city_list ?? []).map((c) => ({
|
|
378
|
+
code: c.code ?? "",
|
|
379
|
+
name: c.name ?? "",
|
|
380
|
+
en_name: c.en_name ?? "",
|
|
381
|
+
}));
|
|
382
|
+
const subjects = (d.job_subject_list ?? []).map((s) => ({
|
|
383
|
+
id: s.id ?? "",
|
|
384
|
+
name: s.name?.zh_cn ?? s.name?.i18n ?? "",
|
|
385
|
+
}));
|
|
386
|
+
// Recruitment type list only exposes "校招" (id=2) as the parent.
|
|
387
|
+
// The children 201=正式, 202=实习 are inferred from actual recruit_type fields.
|
|
388
|
+
const recruitmentTypes = [
|
|
389
|
+
{ id: "201", name: "正式", note: "campus new-grad (portal-channel: campus, ~357 posts)" },
|
|
390
|
+
{ id: "202", name: "实习", note: "intern (portal-channel: internship, ~729 posts)" },
|
|
391
|
+
];
|
|
392
|
+
const result = {
|
|
393
|
+
ok: true,
|
|
394
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
395
|
+
jobFunctions,
|
|
396
|
+
cities,
|
|
397
|
+
subjects,
|
|
398
|
+
recruitmentTypes,
|
|
399
|
+
};
|
|
400
|
+
_filterCache = result;
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
// ---------- stub notices (no public notices endpoint) ----------
|
|
404
|
+
const STUB_NOTICES = {
|
|
405
|
+
ok: false,
|
|
406
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
407
|
+
message: "Xiaomi: no public notices endpoint",
|
|
408
|
+
};
|
|
409
|
+
export async function listNotices() {
|
|
410
|
+
return STUB_NOTICES;
|
|
411
|
+
}
|
|
412
|
+
export async function getNotice(_id) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
416
|
+
message: "Xiaomi: no public notices endpoint",
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
423
|
+
message: "Xiaomi: no public notices endpoint",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// ---------- matchResume ----------
|
|
427
|
+
export async function matchResume(text, opts = {}) {
|
|
428
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
429
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
430
|
+
const channel = opts.channel ?? "campus";
|
|
431
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
432
|
+
if (!terms.length) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
436
|
+
message: "could not extract any technical signals from the text",
|
|
437
|
+
preview: (text ?? "").slice(0, 120),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
441
|
+
const list = await searchPositions({ keyword, page: 1, pageSize: 100, channel });
|
|
442
|
+
if (!list.ok) {
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
446
|
+
message: list.message,
|
|
447
|
+
positions: [],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const defaultRecruitId = channel === "internship" ? "202" : "201";
|
|
451
|
+
const payload = {
|
|
452
|
+
keyword,
|
|
453
|
+
limit: 100,
|
|
454
|
+
offset: 0,
|
|
455
|
+
portal_type: 3,
|
|
456
|
+
portal_entrance: 1,
|
|
457
|
+
language: "zh",
|
|
458
|
+
recruitment_id_list: [defaultRecruitId],
|
|
459
|
+
};
|
|
460
|
+
const raw = await call("/search/job/posts", payload, channel);
|
|
461
|
+
const rawPosts = raw.ok ? (raw.data?.job_post_list ?? []) : [];
|
|
462
|
+
const rawById = new Map();
|
|
463
|
+
for (const p of rawPosts) {
|
|
464
|
+
rawById.set(String(p.id ?? ""), p);
|
|
465
|
+
}
|
|
466
|
+
const scored = [];
|
|
467
|
+
for (const p of list.positions) {
|
|
468
|
+
const rp = rawById.get(p.post_id);
|
|
469
|
+
const blob = [
|
|
470
|
+
p.title,
|
|
471
|
+
p.project,
|
|
472
|
+
p.recruit_label,
|
|
473
|
+
p.work_cities,
|
|
474
|
+
rp?.description ?? "",
|
|
475
|
+
rp?.requirement ?? "",
|
|
476
|
+
].join(" ");
|
|
477
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
478
|
+
if (score > 0) {
|
|
479
|
+
scored.push({
|
|
480
|
+
score,
|
|
481
|
+
position: p,
|
|
482
|
+
reasons,
|
|
483
|
+
description: rp?.description,
|
|
484
|
+
requirements: rp?.requirement,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
scored.sort((a, b) => b.score - a.score);
|
|
489
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
490
|
+
if (!shortlist.length) {
|
|
491
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({
|
|
492
|
+
score: 0,
|
|
493
|
+
position,
|
|
494
|
+
reasons: [],
|
|
495
|
+
description: rawById.get(position.post_id)?.description,
|
|
496
|
+
requirements: rawById.get(position.post_id)?.requirement,
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
500
|
+
const mr = s.reasons.length > 0
|
|
501
|
+
? s.reasons.slice(0, 5)
|
|
502
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
503
|
+
return {
|
|
504
|
+
...s.position,
|
|
505
|
+
description: s.description,
|
|
506
|
+
requirements: s.requirements,
|
|
507
|
+
match_reasons: mr,
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
return {
|
|
511
|
+
ok: true,
|
|
512
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
513
|
+
channel,
|
|
514
|
+
extracted_terms: terms,
|
|
515
|
+
city_preferences: cities,
|
|
516
|
+
matches,
|
|
517
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
518
|
+
"The only authority on selection is HR.",
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
522
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
523
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
524
|
+
host: "xiaomi.jobs.f.mioffice.cn",
|
|
525
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
526
|
+
channel: "campus",
|
|
527
|
+
applyUrlPrefix: "https://xiaomi.jobs.f.mioffice.cn/campus/position",
|
|
528
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
529
|
+
});
|
package/dist/xpeng.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Thin wrapper for 小鹏汽车 (XPeng Motors) careers, hosted on Greenhouse.
|
|
2
|
+
//
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Discovery notes (probed 2026-05):
|
|
5
|
+
//
|
|
6
|
+
// Attempted endpoints:
|
|
7
|
+
// https://career.xiaopeng.com — 000 (DNS / connection refused)
|
|
8
|
+
// https://job.xiaopeng.com — 000 (DNS / connection refused)
|
|
9
|
+
// https://xpeng.jobs.feishu.cn — HTTP 400 (no portal configured)
|
|
10
|
+
// https://xpeng.app.mokahr.com — no Moka tenant
|
|
11
|
+
//
|
|
12
|
+
// Live endpoint: https://boards-api.greenhouse.io/v1/boards/xpengmotors/jobs
|
|
13
|
+
// Greenhouse slug: xpengmotors
|
|
14
|
+
// Tenant: XPENG (US AI / autonomous-driving R&D operation)
|
|
15
|
+
// Total positions: ~29 (probed 2026-05) — mostly San Jose / Santa Clara
|
|
16
|
+
// interns and AI / data / autonomous-driving roles.
|
|
17
|
+
//
|
|
18
|
+
// ============================================================
|
|
19
|
+
// This adapter covers XPeng's US / international Greenhouse board only.
|
|
20
|
+
// The China-side campus / social board hosted on careers.xiaopeng.com is
|
|
21
|
+
// not publicly reachable from outside their network at the moment, but
|
|
22
|
+
// when it becomes accessible a sibling adapter can be added.
|
|
23
|
+
import { createAdapter } from "./greenhouse.js";
|
|
24
|
+
const adapter = createAdapter({ slug: "xpengmotors", label: "XPeng" });
|
|
25
|
+
export const searchPositions = adapter.searchPositions;
|
|
26
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
27
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
28
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
29
|
+
export const listNotices = adapter.listNotices;
|
|
30
|
+
export const getNotice = adapter.getNotice;
|
|
31
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
32
|
+
export const matchResume = adapter.matchResume;
|
|
33
|
+
export const checkResume = adapter.checkResume;
|
|
34
|
+
export const fetchApplicationSchema = adapter.fetchApplicationSchema;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Thin client for 01.AI / 零一万物 recruiting portal.
|
|
2
|
+
//
|
|
3
|
+
// Portal: https://01ai.jobs.feishu.cn/
|
|
4
|
+
// Platform: Feishu Recruiting (ATSX) SaaS — same API surface as nio.ts / moonshot.ts.
|
|
5
|
+
//
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Discovery (2026-05):
|
|
8
|
+
//
|
|
9
|
+
// www.01.ai/ → Strikingly site, links to portal
|
|
10
|
+
// 01ai.jobs.feishu.cn/index/ → Feishu ATSX, channel "index"
|
|
11
|
+
// tenant "零一万物" / "社招官网"
|
|
12
|
+
//
|
|
13
|
+
// The portal channel slug is "index" (not "social" / "campus") — the
|
|
14
|
+
// tenant only configured one channel and it's named "index".
|
|
15
|
+
//
|
|
16
|
+
// ============================================================
|
|
17
|
+
// PositionSummary field mapping (Feishu → canonical):
|
|
18
|
+
// post_id ← String(item.id)
|
|
19
|
+
// title ← item.title
|
|
20
|
+
// project ← item.job_category?.name ?? item.job_function?.name
|
|
21
|
+
// recruit_label ← item.recruit_type?.name
|
|
22
|
+
// bgs ← "" (not exposed in public search)
|
|
23
|
+
// work_cities ← city_list joined " / " (city_info used as fallback)
|
|
24
|
+
// apply_url ← https://01ai.jobs.feishu.cn/index/position/${id}/detail
|
|
25
|
+
import { createAdapter } from "./feishu.js";
|
|
26
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
27
|
+
export { extractResumeSignals, scoreOverlap, checkResume };
|
|
28
|
+
const _adapter = createAdapter({
|
|
29
|
+
host: "01ai.jobs.feishu.cn",
|
|
30
|
+
channel: "index",
|
|
31
|
+
label: "01.AI (零一万物)",
|
|
32
|
+
applyUrlPrefix: "https://01ai.jobs.feishu.cn/index/position",
|
|
33
|
+
});
|
|
34
|
+
export const searchPositions = _adapter.searchPositions;
|
|
35
|
+
export const fetchAllPositions = _adapter.fetchAllPositions;
|
|
36
|
+
export const fetchPositionDetail = _adapter.fetchPositionDetail;
|
|
37
|
+
export const fetchDictionaries = _adapter.fetchDictionaries;
|
|
38
|
+
export const listNotices = _adapter.listNotices;
|
|
39
|
+
export const getNotice = _adapter.getNotice;
|
|
40
|
+
export const findNoticesByQuestion = _adapter.findNoticesByQuestion;
|
|
41
|
+
export const matchResume = _adapter.matchResume;
|
|
42
|
+
export const fetchApplicationSchema = _adapter.fetchApplicationSchema;
|