@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.
Files changed (68) hide show
  1. package/dist/adapter.js +17 -0
  2. package/dist/agibot.js +399 -0
  3. package/dist/alibaba.js +509 -0
  4. package/dist/antgroup.js +397 -0
  5. package/dist/apply.js +1373 -0
  6. package/dist/baichuan.js +49 -0
  7. package/dist/baidu.js +452 -0
  8. package/dist/bilibili.js +455 -0
  9. package/dist/byd.js +412 -0
  10. package/dist/bytedance.js +619 -0
  11. package/dist/cainiao.js +56 -0
  12. package/dist/cambricon.js +33 -0
  13. package/dist/cdp.js +237 -0
  14. package/dist/cicc.js +56 -0
  15. package/dist/coverage.js +60 -0
  16. package/dist/deepseek.js +25 -0
  17. package/dist/didi.js +381 -0
  18. package/dist/feishu.js +577 -0
  19. package/dist/galaxyuniversal.js +24 -0
  20. package/dist/geely.js +35 -0
  21. package/dist/greenhouse.js +432 -0
  22. package/dist/hikvision.js +58 -0
  23. package/dist/horizonrobotics.js +46 -0
  24. package/dist/hoyoverse.js +26 -0
  25. package/dist/huawei.js +537 -0
  26. package/dist/iflytek.js +380 -0
  27. package/dist/index.js +1828 -0
  28. package/dist/iqiyi.js +494 -0
  29. package/dist/jd.js +559 -0
  30. package/dist/kuaishou.js +496 -0
  31. package/dist/lever.js +455 -0
  32. package/dist/liauto.js +393 -0
  33. package/dist/liepin.js +357 -0
  34. package/dist/lilith.js +300 -0
  35. package/dist/megvii.js +27 -0
  36. package/dist/meituan.js +633 -0
  37. package/dist/memory.js +76 -0
  38. package/dist/mihoyo.js +308 -0
  39. package/dist/minimax.js +32 -0
  40. package/dist/moka.js +473 -0
  41. package/dist/moonshot.js +24 -0
  42. package/dist/netease.js +424 -0
  43. package/dist/nio.js +24 -0
  44. package/dist/oppo.js +285 -0
  45. package/dist/pdd.js +614 -0
  46. package/dist/pingan.js +493 -0
  47. package/dist/sensetime.js +51 -0
  48. package/dist/sf.js +310 -0
  49. package/dist/stepfun.js +24 -0
  50. package/dist/tencent.js +770 -0
  51. package/dist/trip.js +396 -0
  52. package/dist/unitree.js +418 -0
  53. package/dist/vivo.js +361 -0
  54. package/dist/webank.js +55 -0
  55. package/dist/wecruit.js +438 -0
  56. package/dist/weibo.js +337 -0
  57. package/dist/weride.js +29 -0
  58. package/dist/xiaohongshu.js +480 -0
  59. package/dist/xiaomi.js +529 -0
  60. package/dist/xpeng.js +34 -0
  61. package/dist/zerooneai.js +42 -0
  62. package/dist/zhipu.js +478 -0
  63. package/extension/README.md +79 -0
  64. package/extension/background.js +177 -0
  65. package/extension/manifest.json +55 -0
  66. package/extension/popup.html +37 -0
  67. package/extension/popup.js +54 -0
  68. 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;