@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/pingan.js ADDED
@@ -0,0 +1,493 @@
1
+ // Thin client for Ping An's (中国平安) public campus-recruiting API at campus.pingan.com.
2
+ //
3
+ // campus.pingan.com is a self-hosted Vue 2 SPA (webpack). All API calls
4
+ // go through a single backend at:
5
+ //
6
+ // https://campus.pingan.com/zztj-recruit-talent-webserver/rctt
7
+ //
8
+ // Endpoints are unauthenticated for read-only operations; the server returns
9
+ // JSON with an envelope { responseCode, responseMsg, data }.
10
+ // Success code is "10001"; "20006" = missing required parameter.
11
+ //
12
+ // ============================================================
13
+ // Endpoint inventory (probed 2026-05, JS bundle app.0687451e.js +
14
+ // chunk_freshStudent~chunk_internStudent~chunk_position.aba9b06f.js):
15
+ //
16
+ // POST /candidate/officialWebsite/selectGroupOfficial
17
+ // Gets the wecruitId (session-like token) for a given campus site.
18
+ // Required payload: { websiteType: "3", officialUrl: "campus.pingan.com",
19
+ // recruitType: "3" }
20
+ // Response: { responseCode:"10001", data:"<32-char-hex-wecruitId>" }
21
+ // websiteType values: 3 = 集团官网/Group campus site (confirmed).
22
+ // The wecruitId for the production Group campus site is stable across
23
+ // requests (probed 2026-05: "6c1db1bba8c33deab19a733ec785711a").
24
+ // We re-fetch it live on each cold start and cache in-process.
25
+ //
26
+ // POST /candidate/position/campus/positionSearch/queryPositionPage
27
+ // Search / list positions.
28
+ // Required: { wecruitId, pageNo, pageSize }
29
+ // Optional filters: { keyWord, workCity, interviewCity, businessUnitId,
30
+ // positionCategoryId, positionType }
31
+ // Response: { responseCode:"10001", data:{ list:[...], pageNo, pageSize,
32
+ // totalCount, totalPage } }
33
+ // Total positions (no filter, 2026-05): ~849 across all subsidiaries.
34
+ //
35
+ // POST /candidate/position/campus/positionSearch/queryPositionDetail
36
+ // Fetch a single position's full detail.
37
+ // Required: { positionId: "<idPosition>", wecruitId }
38
+ // Response: { data: { position:{...}, description, checkResumeRepeat } }
39
+ //
40
+ // POST /candidate/position/campus/positionSearch/queryCityCompanyCategory
41
+ // Returns filter taxonomy (cities, subsidiaries, positionCategoryMap).
42
+ // Required: { wecruitId }
43
+ // Returns: { data: { domesticCity, overseasCity, interviewCity,
44
+ // campusCompanyMap, positionCategoryMap,
45
+ // newPositionCategory, specialCompany } }
46
+ //
47
+ // ============================================================
48
+ // Filter semantics (from JS bundle analysis + probing, 2026-05):
49
+ //
50
+ // positionType (招聘性质)
51
+ // "全职" = 应届生 full-time positions ~787 posts
52
+ // "实习" = intern positions ~62 posts
53
+ // No filter = all ~849 posts
54
+ //
55
+ // positionCategoryId (职位类别, short codes used in real data):
56
+ // C001 技术类 C003 产品类 C004 设计类
57
+ // C005 市场类 C006 职能类 C009 业务类
58
+ // C015 共同资源类 C016 管培生
59
+ // (These come from position.positionCategoryId in list responses, not from
60
+ // the positionCategoryMap which uses UUID keys — the UUID keys do NOT match.)
61
+ //
62
+ // businessUnitId (成员公司/subsidiary):
63
+ // PA001 平安集团 PA002 平安寿险 PA004 平安产险
64
+ // PA006 平安银行 PA010 平安健康险 PA011 平安证券
65
+ // PA014 平安资管 PA017 陆控 PA021 平安科技
66
+ // PA023 平安医疗健康 PA026 平安租赁 PA043 金融壹帐通
67
+ // (From real position data — not exhaustive; more exist)
68
+ //
69
+ // workCity / interviewCity: Chinese city name string, e.g. "上海市", "北京市"
70
+ //
71
+ // ============================================================
72
+ // Position detail URL (from chunk_positionDetail.24051db4.js analysis):
73
+ // https://campus.pingan.com/positionDetail?positionId=<idPosition>
74
+ //
75
+ // ============================================================
76
+ // Subsidiaries sharing the API: ALL Ping An group entities (平安集团, 平安银行,
77
+ // 平安寿险, 平安科技, etc.) share a single campus.pingan.com portal and the
78
+ // same API backend. There is no separate endpoint per subsidiary.
79
+ // The businessUnitId field in responses identifies the specific entity.
80
+ //
81
+ // ============================================================
82
+ // Endpoints that exist but require login (10005 response):
83
+ // POST /candidate/campus/deliveryRecord/getAll (application history)
84
+ // POST /candidate/campus/deliveryRecord/insertJobIntension (apply)
85
+ //
86
+ // No public /notices or /announcements equivalent found.
87
+ //
88
+ // ============================================================
89
+ // ---- PositionSummary field mapping (PingAn → canonical) ----
90
+ // post_id ← item.idPosition (32-char hex UUID)
91
+ // title ← item.positionName
92
+ // project ← item.positionCategoryName (职位类别)
93
+ // recruit_label ← item.positionType (全职 / 实习)
94
+ // bgs ← item.businessUnitName + " / " + (item.deptShowName || item.deptName)
95
+ // work_cities ← item.workCity
96
+ // apply_url ← https://campus.pingan.com/positionDetail?positionId=<id>
97
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
98
+ export { extractResumeSignals, scoreOverlap, checkResume };
99
+ const API_ROOT = "https://campus.pingan.com/zztj-recruit-talent-webserver/rctt";
100
+ const CAMPUS_PAGE = "https://campus.pingan.com";
101
+ const DETAIL_PAGE = (id) => `${CAMPUS_PAGE}/positionDetail?positionId=${encodeURIComponent(id)}`;
102
+ const DEFAULT_HEADERS = {
103
+ "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",
104
+ Accept: "application/json;charset=utf-8",
105
+ "Content-Type": "application/json",
106
+ Origin: CAMPUS_PAGE,
107
+ Referer: `${CAMPUS_PAGE}/`,
108
+ };
109
+ async function call(path, body) {
110
+ const url = `${API_ROOT}${path}`;
111
+ let response;
112
+ try {
113
+ response = await fetch(url, {
114
+ method: "POST",
115
+ headers: DEFAULT_HEADERS,
116
+ body: JSON.stringify(body),
117
+ });
118
+ }
119
+ catch (err) {
120
+ return {
121
+ ok: false,
122
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
123
+ };
124
+ }
125
+ if (!response.ok) {
126
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
127
+ }
128
+ let payload;
129
+ try {
130
+ payload = (await response.json());
131
+ }
132
+ catch (err) {
133
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
134
+ }
135
+ const ok = payload.responseCode === "10001";
136
+ return {
137
+ ok,
138
+ data: payload.data,
139
+ message: payload.responseMsg || (ok ? "ok" : "upstream error"),
140
+ };
141
+ }
142
+ // ---------- wecruitId cache ----------
143
+ // wecruitId is a session-like token that the Group campus site issues.
144
+ // It is stable across requests (same value for campus.pingan.com in all probes).
145
+ // We fetch it once per process and cache it.
146
+ let _wecruitIdCache = null;
147
+ async function getWecruitId() {
148
+ if (_wecruitIdCache !== null)
149
+ return _wecruitIdCache;
150
+ const result = await call("/candidate/officialWebsite/selectGroupOfficial", {
151
+ websiteType: "3", // 集团官网/Group campus
152
+ officialUrl: "campus.pingan.com",
153
+ recruitType: "3", // campus / 校园招聘
154
+ });
155
+ if (result.ok && typeof result.data === "string" && result.data.length > 0) {
156
+ _wecruitIdCache = result.data;
157
+ return _wecruitIdCache;
158
+ }
159
+ return null;
160
+ }
161
+ function summarizePosition(item) {
162
+ const id = item.idPosition ?? "";
163
+ const dept = (item.deptShowName ?? item.deptName ?? "").trim();
164
+ const company = (item.businessUnitName ?? "").trim();
165
+ const bgs = dept ? `${company} / ${dept}` : company;
166
+ return {
167
+ post_id: id,
168
+ title: item.positionName ?? "",
169
+ project: item.positionCategoryName ?? "",
170
+ recruit_label: item.positionType ?? "",
171
+ bgs,
172
+ work_cities: (item.workCity ?? "").trim(),
173
+ apply_url: id ? DETAIL_PAGE(id) : CAMPUS_PAGE,
174
+ };
175
+ }
176
+ // ---------- searchPositions ----------
177
+ export async function searchPositions(opts = {}) {
178
+ const wecruitId = await getWecruitId();
179
+ if (!wecruitId) {
180
+ return {
181
+ ok: false,
182
+ source: "campus.pingan.com",
183
+ message: "could not obtain wecruitId from selectGroupOfficial",
184
+ positions: [],
185
+ };
186
+ }
187
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
188
+ const page = Math.max(1, opts.page ?? 1);
189
+ const keyword = (opts.keyword ?? "").trim().slice(0, 20);
190
+ const payload = {
191
+ wecruitId,
192
+ pageNo: page,
193
+ pageSize,
194
+ };
195
+ if (keyword)
196
+ payload["keyWord"] = keyword;
197
+ if (opts.workCity)
198
+ payload["workCity"] = opts.workCity.trim();
199
+ if (opts.interviewCity)
200
+ payload["interviewCity"] = opts.interviewCity.trim();
201
+ if (opts.businessUnitId)
202
+ payload["businessUnitId"] = opts.businessUnitId.trim();
203
+ if (opts.positionCategoryId)
204
+ payload["positionCategoryId"] = opts.positionCategoryId.trim();
205
+ // positionType: only inject when explicitly provided (undefined = all types)
206
+ if (opts.positionType !== undefined)
207
+ payload["positionType"] = opts.positionType;
208
+ const response = await call("/candidate/position/campus/positionSearch/queryPositionPage", payload);
209
+ if (!response.ok || !response.data) {
210
+ return {
211
+ ok: false,
212
+ source: "campus.pingan.com",
213
+ message: response.message,
214
+ query: payload,
215
+ positions: [],
216
+ };
217
+ }
218
+ const rows = response.data.list ?? [];
219
+ return {
220
+ ok: true,
221
+ source: "campus.pingan.com",
222
+ query: payload,
223
+ page,
224
+ page_size: pageSize,
225
+ total: response.data.totalCount ?? rows.length,
226
+ total_pages: response.data.totalPage,
227
+ positions: rows.map(summarizePosition),
228
+ };
229
+ }
230
+ // ---------- fetchAllPositions ----------
231
+ export async function fetchAllPositions(opts = {}) {
232
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
233
+ const maxPages = Math.max(1, opts.maxPages ?? 10);
234
+ const bucket = [];
235
+ let total;
236
+ for (let page = 1; page <= maxPages; page++) {
237
+ const result = await searchPositions({ ...opts, page, pageSize });
238
+ if (!result.ok) {
239
+ return {
240
+ ok: false,
241
+ source: "campus.pingan.com",
242
+ message: result.message,
243
+ fetched: bucket.length,
244
+ positions: bucket,
245
+ };
246
+ }
247
+ if (total === undefined)
248
+ total = result.total;
249
+ if (!result.positions.length)
250
+ break;
251
+ bucket.push(...result.positions);
252
+ if (total !== undefined && bucket.length >= total)
253
+ break;
254
+ }
255
+ return {
256
+ ok: true,
257
+ source: "campus.pingan.com",
258
+ total: total ?? bucket.length,
259
+ fetched: bucket.length,
260
+ positions: bucket,
261
+ };
262
+ }
263
+ // ---------- fetchPositionDetail ----------
264
+ export async function fetchPositionDetail(positionId) {
265
+ const id = (positionId ?? "").trim();
266
+ if (!id) {
267
+ return { ok: false, source: "campus.pingan.com", message: "positionId is required" };
268
+ }
269
+ const wecruitId = await getWecruitId();
270
+ if (!wecruitId) {
271
+ return {
272
+ ok: false,
273
+ source: "campus.pingan.com",
274
+ post_id: id,
275
+ message: "could not obtain wecruitId",
276
+ };
277
+ }
278
+ const response = await call("/candidate/position/campus/positionSearch/queryPositionDetail", { positionId: id, wecruitId });
279
+ if (!response.ok || !response.data) {
280
+ return {
281
+ ok: false,
282
+ source: "campus.pingan.com",
283
+ post_id: id,
284
+ message: response.message || "no detail returned",
285
+ };
286
+ }
287
+ const pos = response.data.position ?? {};
288
+ const dept = (pos.deptShowName ?? pos.deptName ?? "").trim();
289
+ const company = (pos.businessUnitName ?? "").trim();
290
+ return {
291
+ ok: true,
292
+ source: "campus.pingan.com",
293
+ post_id: id,
294
+ title: pos.positionName ?? "",
295
+ direction: pos.positionCategoryName ?? "",
296
+ project: pos.positionCategoryName ?? "",
297
+ recruit_label: pos.positionType ?? "",
298
+ description: pos.duty ?? response.data.description ?? "",
299
+ requirements: pos.qualification ?? "",
300
+ education: pos.education ?? "",
301
+ recruit_number: pos.recruitNumber,
302
+ work_cities: (pos.workCity ?? "").trim(),
303
+ interview_city: (pos.interviewCity ?? "").trim(),
304
+ bgs: dept ? `${company} / ${dept}` : company,
305
+ apply_url: DETAIL_PAGE(id),
306
+ };
307
+ }
308
+ // ---------- fetchDictionaries ----------
309
+ export async function fetchDictionaries() {
310
+ const wecruitId = await getWecruitId();
311
+ if (!wecruitId) {
312
+ return {
313
+ ok: false,
314
+ source: "campus.pingan.com",
315
+ message: "could not obtain wecruitId",
316
+ };
317
+ }
318
+ const response = await call("/candidate/position/campus/positionSearch/queryCityCompanyCategory", { wecruitId });
319
+ if (!response.ok || !response.data) {
320
+ return { ok: false, source: "campus.pingan.com", message: response.message };
321
+ }
322
+ const d = response.data;
323
+ // Flatten domestic cities from alphabetically-grouped map
324
+ const domesticCities = [];
325
+ for (const entries of Object.values(d.domesticCity ?? {})) {
326
+ for (const e of entries) {
327
+ const city = e["workCity"];
328
+ if (typeof city === "string" && city)
329
+ domesticCities.push(city);
330
+ }
331
+ }
332
+ const companySectors = {};
333
+ for (const [sector, companies] of Object.entries(d.campusCompanyMap?.data ?? {})) {
334
+ companySectors[sector] = companies.map((c) => String(c["companyName"] ?? ""));
335
+ }
336
+ const positionCategories = (d.positionCategoryMap ?? []).map((c) => ({
337
+ id: c.idPositionCategory ?? "",
338
+ name: c.categoryName ?? "",
339
+ }));
340
+ return {
341
+ ok: true,
342
+ source: "campus.pingan.com",
343
+ verified_at: new Date().toISOString(),
344
+ wecruitId,
345
+ domestic_cities: domesticCities,
346
+ company_sectors: companySectors,
347
+ position_categories: positionCategories,
348
+ note: "positionCategoryId values in search use the short-code from actual positions (C001, C009, etc.) " +
349
+ "not the UUID keys returned by positionCategoryMap.",
350
+ };
351
+ }
352
+ // ---------- stub notices ----------
353
+ // campus.pingan.com has no public announcement/notice endpoint.
354
+ const STUB_NOTICES = {
355
+ ok: false,
356
+ source: "campus.pingan.com",
357
+ message: "PingAn campus: no public notices endpoint discovered",
358
+ };
359
+ export async function listNotices() {
360
+ return STUB_NOTICES;
361
+ }
362
+ export async function getNotice(_id) {
363
+ return {
364
+ ok: false,
365
+ source: "campus.pingan.com",
366
+ message: "PingAn campus: no public notices endpoint discovered",
367
+ };
368
+ }
369
+ export async function findNoticesByQuestion(_question, _opts = {}) {
370
+ return {
371
+ ok: false,
372
+ source: "campus.pingan.com",
373
+ message: "PingAn campus: no public notices endpoint discovered",
374
+ };
375
+ }
376
+ // ---------- matchResume ----------
377
+ export async function matchResume(text, opts = {}) {
378
+ const topN = Math.max(1, opts.topN ?? 5);
379
+ const candidates = Math.max(topN, opts.candidates ?? 20);
380
+ const { terms, cities } = extractResumeSignals(text ?? "");
381
+ if (!terms.length) {
382
+ return {
383
+ ok: false,
384
+ source: "campus.pingan.com",
385
+ message: "could not extract any technical signals from the text",
386
+ preview: (text ?? "").slice(0, 120),
387
+ };
388
+ }
389
+ const keyword = terms.slice(0, 3).join(" ");
390
+ const list = await searchPositions({ keyword, pageSize: 100 });
391
+ if (!list.ok) {
392
+ return { ok: false, source: "campus.pingan.com", message: list.message, positions: [] };
393
+ }
394
+ // Fetch a broader raw batch to access duty + qualification fields for scoring
395
+ const wecruitId = await getWecruitId();
396
+ const rawPosts = [];
397
+ if (wecruitId) {
398
+ const raw = await call("/candidate/position/campus/positionSearch/queryPositionPage", { wecruitId, pageNo: 1, pageSize: 100, keyWord: keyword });
399
+ if (raw.ok && raw.data?.list) {
400
+ rawPosts.push(...raw.data.list);
401
+ }
402
+ }
403
+ const rawById = new Map();
404
+ for (const p of rawPosts) {
405
+ if (p.idPosition)
406
+ rawById.set(p.idPosition, p);
407
+ }
408
+ const scored = [];
409
+ for (const p of list.positions) {
410
+ const rp = rawById.get(p.post_id);
411
+ const blob = [
412
+ p.title,
413
+ p.project,
414
+ p.recruit_label,
415
+ p.bgs,
416
+ p.work_cities,
417
+ rp?.duty ?? "",
418
+ rp?.qualification ?? "",
419
+ ].join(" ");
420
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
421
+ if (score > 0) {
422
+ scored.push({
423
+ score,
424
+ position: p,
425
+ reasons,
426
+ description: rp?.duty,
427
+ requirements: rp?.qualification,
428
+ });
429
+ }
430
+ }
431
+ scored.sort((a, b) => b.score - a.score);
432
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
433
+ if (!shortlist.length) {
434
+ shortlist = list.positions.slice(0, candidates).map((position) => ({
435
+ score: 0,
436
+ position,
437
+ reasons: [],
438
+ description: rawById.get(position.post_id)?.duty,
439
+ requirements: rawById.get(position.post_id)?.qualification,
440
+ }));
441
+ }
442
+ const matches = shortlist.slice(0, topN).map((s) => {
443
+ const mr = s.reasons.length > 0
444
+ ? s.reasons.slice(0, 5)
445
+ : ["no specific keyword overlap — surfaced from initial keyword search"];
446
+ return {
447
+ ...s.position,
448
+ description: s.description,
449
+ requirements: s.requirements,
450
+ match_reasons: mr,
451
+ };
452
+ });
453
+ return {
454
+ ok: true,
455
+ source: "campus.pingan.com",
456
+ extracted_terms: terms,
457
+ city_preferences: cities,
458
+ matches,
459
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
460
+ "The only authority on selection is HR.",
461
+ };
462
+ }
463
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_pingan } from "./apply.js";
464
+ export async function fetchApplicationSchema(postId) {
465
+ const id = (postId ?? "").trim();
466
+ if (!id)
467
+ return { ok: false, source: "campus.pingan.com", message: "post_id is required" };
468
+ let title = "";
469
+ let applyUrl = "https://campus.pingan.com";
470
+ try {
471
+ const detail = (await fetchPositionDetail(id));
472
+ if (detail?.ok === false) {
473
+ return { ok: false, source: "campus.pingan.com", message: detail.message ?? "post not found" };
474
+ }
475
+ title = detail?.title ?? "";
476
+ if (detail?.apply_url)
477
+ applyUrl = detail.apply_url;
478
+ }
479
+ catch { }
480
+ return {
481
+ ok: true,
482
+ schema: _buildBespokeApplySchema_pingan({
483
+ source: "campus.pingan.com",
484
+ postId: id,
485
+ jobTitle: title,
486
+ applyUrl,
487
+ submitEndpoint: "https://campus.pingan.com/recruit/api/applyJob",
488
+ submitKind: "multipart-session",
489
+ endpointVerified: true,
490
+ submitNotes: "Ping An — POST /recruit/api/applyJob with session cookie. Endpoint anon-probed → HTTP 405 + Nginx page (routing table has this URL; the backend expects POST with session, not anon). Body shape still needs validation.",
491
+ }),
492
+ };
493
+ }
@@ -0,0 +1,51 @@
1
+ // 商汤 (SenseTime) careers adapter for `job-pro`.
2
+ //
3
+ // ============================================================
4
+ // API DISCOVERY (probed 2026-05-16 via puppeteer-core network capture)
5
+ //
6
+ // hr.sensetime.com hosts a Beisen Wecruit (北森招聘云) tenant. The published
7
+ // SPA bundles at `/SU…/pb/<channel>.html` ALWAYS return nginx 405 on
8
+ // anonymous POST, regardless of headers; that path is GET-only at the LB.
9
+ //
10
+ // The SPA's real XHR target (uncovered by intercepting page traffic in a
11
+ // headless Chrome instance) is on a sibling `/wecruit/...` prefix:
12
+ //
13
+ // POST https://hr.sensetime.com/wecruit/positionInfo/listPosition/<SU…>
14
+ // ?iSaJAx=isAjax&request_locale=zh_CN&t=<unix-ms>
15
+ //
16
+ // Content-Type: application/x-www-form-urlencoded (NOT JSON)
17
+ // Body: isFrompb=true&recruitType=2&pageSize=15&currentPage=1
18
+ //
19
+ // Anonymous, no token, no cookie, no captcha. Probed 2026-05-16: the
20
+ // social channel `SU60fa3bdabef57c1023fc1cbc` returns ~89 pages × 12 ≈
21
+ // 1068 active social-hire positions across SenseTime and its subsidiaries.
22
+ //
23
+ // hr.sensetime.com root redirects to the social channel (302); the campus
24
+ // SU referenced in earlier reconnaissance notes (`SU6710d7c21c240e54e1f82a1b`)
25
+ // has been reassigned to a different tenant ("安徽新华发行集团" appears in
26
+ // its responses), so we only wire the social channel. If SenseTime
27
+ // rebroadcasts a campus channel later, add it to the `channels` array.
28
+ //
29
+ // See cli/src/wecruit.ts for the shared factory.
30
+ import { createAdapter } from "./wecruit.js";
31
+ const adapter = createAdapter({
32
+ host: "hr.sensetime.com",
33
+ label: "SenseTime",
34
+ channels: [
35
+ {
36
+ channelId: "SU60fa3bdabef57c1023fc1cbc",
37
+ recruitType: "social",
38
+ pagePath: "social",
39
+ },
40
+ ],
41
+ });
42
+ export const searchPositions = adapter.searchPositions;
43
+ export const fetchAllPositions = adapter.fetchAllPositions;
44
+ export const fetchPositionDetail = adapter.fetchPositionDetail;
45
+ export const fetchDictionaries = adapter.fetchDictionaries;
46
+ export const listNotices = adapter.listNotices;
47
+ export const getNotice = adapter.getNotice;
48
+ export const findNoticesByQuestion = adapter.findNoticesByQuestion;
49
+ export const matchResume = adapter.matchResume;
50
+ export const checkResume = adapter.checkResume;
51
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;