@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
@@ -0,0 +1,397 @@
1
+ // 蚂蚁集团 (Ant Group) careers adapter for `job-pro`.
2
+ //
3
+ // ============================================================
4
+ // API DISCOVERY (probed 2026-05-16 via puppeteer-core network capture)
5
+ //
6
+ // `talent.antgroup.com` is an Ant Bigfish SPA. Its public-facing job feed
7
+ // is served by `hrcareersweb.antgroup.com` with two anonymous endpoints:
8
+ //
9
+ // POST /api/campus/position/search — 467 校招 / 实习 positions
10
+ // POST /api/social/position/search — 922 社招 positions
11
+ //
12
+ // Both accept JSON `{ key, pageIndex, pageSize, channel?, language, … }`
13
+ // and return:
14
+ // { success:true, errorMsg:"成功", content:[…RawPosition], totalCount,
15
+ // pageSize, currentPage }
16
+ //
17
+ // The `channel` field is required only on the social endpoint
18
+ // (`"group_official_site"`). The `ctoken=…` query parameter that the
19
+ // browser SPA appends is NOT required for unauthenticated reads.
20
+ //
21
+ // queryCollections / favoritePosition / login-required endpoints return
22
+ // `errorCode:"LOGIN_EXPIRED"` for anonymous callers — those are user
23
+ // dashboard surfaces, not the public search.
24
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
25
+ export { checkResume };
26
+ const SOURCE = "hrcareersweb.antgroup.com";
27
+ const API_ROOT = "https://hrcareersweb.antgroup.com/api";
28
+ const CAMPUS_PAGE = "https://talent.antgroup.com/campus-list";
29
+ const SOCIAL_PAGE = "https://talent.antgroup.com/off-campus-position";
30
+ const DETAIL_URL = (recruitType, id) => recruitType === "campus"
31
+ ? `https://talent.antgroup.com/campus-list?positionId=${encodeURIComponent(id)}`
32
+ : `https://talent.antgroup.com/off-campus-position-detail?positionId=${encodeURIComponent(id)}`;
33
+ const DEFAULT_HEADERS = {
34
+ "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",
35
+ Accept: "application/json, text/plain, */*",
36
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
37
+ "Content-Type": "application/json;charset=UTF-8",
38
+ Origin: "https://talent.antgroup.com",
39
+ };
40
+ async function post(path, body, referer) {
41
+ let response;
42
+ try {
43
+ response = await fetch(`${API_ROOT}${path}`, {
44
+ method: "POST",
45
+ headers: { ...DEFAULT_HEADERS, Referer: referer },
46
+ body: JSON.stringify(body),
47
+ });
48
+ }
49
+ catch (err) {
50
+ return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
51
+ }
52
+ if (!response.ok)
53
+ return { ok: false, message: `HTTP ${response.status}` };
54
+ let env;
55
+ try {
56
+ env = (await response.json());
57
+ }
58
+ catch (err) {
59
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
60
+ }
61
+ if (env.success !== true) {
62
+ return { ok: false, message: env.errorMsg ?? `errorCode=${env.errorCode ?? "?"}` };
63
+ }
64
+ return { ok: true, content: env.content, totalCount: env.totalCount ?? 0, message: "ok" };
65
+ }
66
+ function summarize(item, recruitType) {
67
+ const id = String(item.id ?? item.code ?? "");
68
+ const locs = Array.isArray(item.workLocations) ? item.workLocations.filter(Boolean).join(" / ") : "";
69
+ return {
70
+ post_id: id,
71
+ title: (item.name ?? "").trim(),
72
+ project: item.project?.trim() ||
73
+ item.categoryName?.trim() ||
74
+ (Array.isArray(item.categories) ? item.categories.filter(Boolean).join(" / ") : ""),
75
+ recruit_label: (item.positionType ?? "").trim() || (recruitType === "campus" ? "校招" : "社招"),
76
+ bgs: (item.department ?? "").trim(),
77
+ work_cities: locs,
78
+ apply_url: id ? DETAIL_URL(recruitType, id) : recruitType === "campus" ? CAMPUS_PAGE : SOCIAL_PAGE,
79
+ };
80
+ }
81
+ async function searchSingle(recruitType, opts) {
82
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 20));
83
+ const page = Math.max(1, opts.page ?? 1);
84
+ const keyword = (opts.keyword ?? "").trim().slice(0, 60);
85
+ const body = {
86
+ key: keyword,
87
+ pageIndex: page,
88
+ pageSize,
89
+ language: "zh",
90
+ };
91
+ if (recruitType === "social") {
92
+ body.channel = "group_official_site";
93
+ body.regions = "";
94
+ body.categories = "";
95
+ body.subCategories = "";
96
+ body.bgCode = opts.bgCode ?? "";
97
+ body.socialQrCode = "";
98
+ }
99
+ const referer = recruitType === "campus" ? CAMPUS_PAGE : SOCIAL_PAGE;
100
+ const r = await post(`/${recruitType}/position/search`, body, referer);
101
+ if (!r.ok) {
102
+ return { ok: false, total: 0, positions: [], message: r.message };
103
+ }
104
+ return {
105
+ ok: true,
106
+ total: r.totalCount ?? 0,
107
+ positions: (r.content ?? []).map((p) => summarize(p, recruitType)),
108
+ message: "ok",
109
+ };
110
+ }
111
+ // ---------- searchPositions ----------
112
+ export async function searchPositions(opts = {}) {
113
+ const recruitType = opts.recruitType ?? "all";
114
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 20));
115
+ const page = Math.max(1, opts.page ?? 1);
116
+ if (recruitType === "campus" || recruitType === "social") {
117
+ const r = await searchSingle(recruitType, opts);
118
+ if (!r.ok) {
119
+ return {
120
+ ok: false,
121
+ source: SOURCE,
122
+ message: r.message,
123
+ query: { recruitType, page, pageSize, keyword: opts.keyword ?? "" },
124
+ positions: [],
125
+ };
126
+ }
127
+ return {
128
+ ok: true,
129
+ source: SOURCE,
130
+ query: { recruitType, page, pageSize, keyword: opts.keyword ?? "" },
131
+ page,
132
+ page_size: pageSize,
133
+ total: r.total,
134
+ positions: r.positions,
135
+ };
136
+ }
137
+ // "all" → ask both endpoints for the same page
138
+ const [campus, social] = await Promise.all([
139
+ searchSingle("campus", opts),
140
+ searchSingle("social", opts),
141
+ ]);
142
+ const positions = [...campus.positions, ...social.positions];
143
+ const total = (campus.ok ? campus.total : 0) + (social.ok ? social.total : 0);
144
+ if (!campus.ok && !social.ok) {
145
+ return {
146
+ ok: false,
147
+ source: SOURCE,
148
+ message: campus.message,
149
+ query: { recruitType: "all", page, pageSize, keyword: opts.keyword ?? "" },
150
+ positions: [],
151
+ };
152
+ }
153
+ return {
154
+ ok: true,
155
+ source: SOURCE,
156
+ query: { recruitType: "all", page, pageSize, keyword: opts.keyword ?? "" },
157
+ page,
158
+ page_size: pageSize,
159
+ total,
160
+ positions,
161
+ };
162
+ }
163
+ // ---------- fetchAllPositions ----------
164
+ export async function fetchAllPositions(opts = {}) {
165
+ const recruitType = opts.recruitType ?? "all";
166
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 30));
167
+ const maxPages = Math.max(1, opts.maxPages ?? 40);
168
+ async function drain(rt) {
169
+ const bucket = [];
170
+ let total = 0;
171
+ let lastMsg = "ok";
172
+ for (let page = 1; page <= maxPages; page++) {
173
+ const r = await searchSingle(rt, { ...opts, page, pageSize });
174
+ if (!r.ok) {
175
+ lastMsg = r.message;
176
+ if (bucket.length === 0)
177
+ return { ok: false, total: 0, positions: [], message: r.message };
178
+ break;
179
+ }
180
+ if (total === 0)
181
+ total = r.total;
182
+ if (!r.positions.length)
183
+ break;
184
+ bucket.push(...r.positions);
185
+ if (bucket.length >= total)
186
+ break;
187
+ }
188
+ return { ok: true, total, positions: bucket, message: lastMsg };
189
+ }
190
+ if (recruitType === "campus" || recruitType === "social") {
191
+ const r = await drain(recruitType);
192
+ if (!r.ok) {
193
+ return {
194
+ ok: false,
195
+ source: SOURCE,
196
+ message: r.message,
197
+ total: 0,
198
+ fetched: 0,
199
+ positions: [],
200
+ };
201
+ }
202
+ return {
203
+ ok: true,
204
+ source: SOURCE,
205
+ total: r.total,
206
+ fetched: r.positions.length,
207
+ positions: r.positions,
208
+ };
209
+ }
210
+ const [c, s] = await Promise.all([drain("campus"), drain("social")]);
211
+ return {
212
+ ok: true,
213
+ source: SOURCE,
214
+ total: (c.ok ? c.total : 0) + (s.ok ? s.total : 0),
215
+ fetched: c.positions.length + s.positions.length,
216
+ positions: [...c.positions, ...s.positions],
217
+ };
218
+ }
219
+ // ---------- fetchPositionDetail ----------
220
+ // The list endpoint already returns description/requirement/teamDescription
221
+ // inline — no separate detail endpoint needed. Scan campus then social.
222
+ export async function fetchPositionDetail(postId) {
223
+ const id = (postId ?? "").trim();
224
+ if (!id)
225
+ return { ok: false, source: SOURCE, message: "post_id is required" };
226
+ for (const rt of ["campus", "social"]) {
227
+ // pageSize >= 50 triggers a silent upstream rejection (returns
228
+ // {totalCount: 0, content: []}). 20 is the SPA's own default and the
229
+ // largest size that reliably returns data. Compensate by widening
230
+ // maxPages from 20 → 50 to preserve ~the same scan depth.
231
+ const pageSize = 20;
232
+ const maxPages = 50;
233
+ for (let page = 1; page <= maxPages; page++) {
234
+ const body = {
235
+ key: "",
236
+ pageIndex: page,
237
+ pageSize,
238
+ language: "zh",
239
+ };
240
+ if (rt === "social") {
241
+ body.channel = "group_official_site";
242
+ body.regions = "";
243
+ body.categories = "";
244
+ body.subCategories = "";
245
+ body.bgCode = "";
246
+ body.socialQrCode = "";
247
+ }
248
+ const referer = rt === "campus" ? CAMPUS_PAGE : SOCIAL_PAGE;
249
+ const r = await post(`/${rt}/position/search`, body, referer);
250
+ if (!r.ok)
251
+ break;
252
+ const found = (r.content ?? []).find((p) => String(p.id ?? p.code) === id);
253
+ if (found) {
254
+ return {
255
+ ok: true,
256
+ source: SOURCE,
257
+ post_id: id,
258
+ title: found.name ?? "",
259
+ project: found.project ?? found.categoryName ?? "",
260
+ recruit_label: found.positionType ?? (rt === "campus" ? "校招" : "社招"),
261
+ department: found.department ?? "",
262
+ work_cities: found.workLocations ?? [],
263
+ publish_time: found.publishTime ?? "",
264
+ graduation_time: found.graduationTime ?? "",
265
+ experience: found.experience ?? "",
266
+ degree: found.degree ?? "",
267
+ description: found.description ?? "",
268
+ requirements: found.requirement ?? "",
269
+ team_description: found.teamDescription ?? "",
270
+ apply_url: DETAIL_URL(rt, id),
271
+ };
272
+ }
273
+ if (r.totalCount && (r.content?.length ?? 0) < pageSize)
274
+ break;
275
+ }
276
+ }
277
+ return {
278
+ ok: false,
279
+ source: SOURCE,
280
+ post_id: id,
281
+ message: `post ${id} not found in campus or social feeds`,
282
+ };
283
+ }
284
+ // ---------- fetchDictionaries ----------
285
+ let _dictCache = null;
286
+ export async function fetchDictionaries() {
287
+ if (_dictCache !== null)
288
+ return _dictCache;
289
+ const [depRes, regRes, catRes] = await Promise.all([
290
+ post("/social/category/listDept", { channel: "group_official_site", language: "zh" }, SOCIAL_PAGE),
291
+ post("/region/hot", { channel: "group_official_site", language: "zh" }, SOCIAL_PAGE),
292
+ post("/social/category/list", { channel: "group_official_site", language: "zh" }, SOCIAL_PAGE),
293
+ ]);
294
+ if (!depRes.ok && !regRes.ok && !catRes.ok) {
295
+ const r = { ok: false, source: SOURCE, message: depRes.message };
296
+ _dictCache = r;
297
+ return r;
298
+ }
299
+ const result = {
300
+ ok: true,
301
+ source: SOURCE,
302
+ bgs: (depRes.content ?? []).map((d) => ({ code: d.code ?? "", name: d.name ?? "" })),
303
+ regions: (regRes.content ?? []).map((d) => ({ code: d.code ?? "", name: d.name ?? "" })),
304
+ categories: catRes.content ?? [],
305
+ };
306
+ _dictCache = result;
307
+ return result;
308
+ }
309
+ // ---------- notices ----------
310
+ const NOTICES_MSG = "Ant Group (蚂蚁集团): no public notices endpoint on hrcareersweb";
311
+ export async function listNotices() {
312
+ return { ok: false, source: SOURCE, message: NOTICES_MSG, notices: [] };
313
+ }
314
+ export async function getNotice(noticeId) {
315
+ return { ok: false, source: SOURCE, message: NOTICES_MSG, notice_id: noticeId };
316
+ }
317
+ export async function findNoticesByQuestion(question, _opts = {}) {
318
+ return { ok: false, source: SOURCE, question, message: NOTICES_MSG, matches: [] };
319
+ }
320
+ // ---------- matchResume ----------
321
+ export async function matchResume(text, opts = {}) {
322
+ const topN = Math.max(1, opts.topN ?? 5);
323
+ const candidates = Math.max(topN, opts.candidates ?? 20);
324
+ const { terms, cities } = extractResumeSignals(text ?? "");
325
+ if (!terms.length) {
326
+ return {
327
+ ok: false,
328
+ source: SOURCE,
329
+ message: "could not extract any technical signals from the text",
330
+ preview: (text ?? "").slice(0, 120),
331
+ };
332
+ }
333
+ const keyword = terms.slice(0, 3).join(" ");
334
+ const list = await searchPositions({ keyword, page: 1, pageSize: 50, recruitType: "all" });
335
+ if (!list.ok) {
336
+ return { ok: false, source: SOURCE, message: list.message, positions: [] };
337
+ }
338
+ const scored = [];
339
+ for (const p of list.positions) {
340
+ const blob = [p.title, p.project, p.recruit_label, p.bgs, p.work_cities].join(" ");
341
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
342
+ if (score > 0)
343
+ scored.push({ score, position: p, reasons });
344
+ }
345
+ scored.sort((a, b) => b.score - a.score);
346
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
347
+ if (!shortlist.length) {
348
+ shortlist = list.positions.slice(0, candidates).map((position) => ({ score: 0, position, reasons: [] }));
349
+ }
350
+ const matches = shortlist.slice(0, topN).map((s) => {
351
+ const mr = s.reasons.length > 0
352
+ ? s.reasons.slice(0, 5)
353
+ : ["no specific keyword overlap — surfaced from initial keyword search"];
354
+ return { ...s.position, match_reasons: mr };
355
+ });
356
+ return {
357
+ ok: true,
358
+ source: SOURCE,
359
+ extracted_terms: terms,
360
+ city_preferences: cities,
361
+ matches,
362
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
363
+ "The only authority on selection is HR.",
364
+ };
365
+ }
366
+ export { extractResumeSignals, scoreOverlap };
367
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_antgroup } from "./apply.js";
368
+ export async function fetchApplicationSchema(postId) {
369
+ const id = (postId ?? "").trim();
370
+ if (!id)
371
+ return { ok: false, source: "hrcareersweb.antgroup.com", message: "post_id is required" };
372
+ let title = "";
373
+ let applyUrl = "https://hrcareersweb.antgroup.com";
374
+ try {
375
+ const detail = (await fetchPositionDetail(id));
376
+ if (detail?.ok === false) {
377
+ return { ok: false, source: "hrcareersweb.antgroup.com", message: detail.message ?? "post not found" };
378
+ }
379
+ title = detail?.title ?? "";
380
+ if (detail?.apply_url)
381
+ applyUrl = detail.apply_url;
382
+ }
383
+ catch { }
384
+ return {
385
+ ok: true,
386
+ schema: _buildBespokeApplySchema_antgroup({
387
+ source: "hrcareersweb.antgroup.com",
388
+ postId: id,
389
+ jobTitle: title,
390
+ applyUrl,
391
+ submitEndpoint: "https://hrcareersweb.antgroup.com/api/social/application/apply",
392
+ submitKind: "multipart-session",
393
+ endpointVerified: true,
394
+ submitNotes: "Ant Group — POST /api/social/application/apply (or /api/campus/application/apply for campus). Endpoint extracted from talent.antgroup.com's Yuyan/Alipay umi bundle 180020010001257966/umi.6f081e74.js (3.9MB). Anon-probed → HTTP 200 + {success:false, errorMsg:\"登录过期\", errorCode:\"LOGIN_EXPIRED\"} = real auth-gated route. The original /api/social/position/apply was wrong path (position → application). Alipay OAuth session required.",
395
+ }),
396
+ };
397
+ }