@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,509 @@
1
+ // Thin client for Alibaba's public campus-recruiting API at campus-talent.alibaba.com.
2
+ //
3
+ // CSRF flow: the server issues XSRF-TOKEN via a GET to the campus listing page.
4
+ // Every subsequent POST must echo it as both a Cookie and an X-XSRF-TOKEN header.
5
+ // The module-level singleton caches the token on first use and retries once on 403.
6
+ //
7
+ // Endpoint inventory (all under https://campus-talent.alibaba.com):
8
+ //
9
+ // GET /campus/position — HTML page; sets XSRF-TOKEN cookie
10
+ // POST /searchCondition/listBatch — list all active batches (no auth required)
11
+ // POST /searchCondition/list — filter taxonomy for a given batchId
12
+ // POST /position/search — paginated job search (filter params below)
13
+ // POST /position/detail — single job detail (body: {id: <number>})
14
+ // POST /position/queryCircleDept — dept tree for a circle (returns [] without login)
15
+ //
16
+ // ACTIVE BATCHES (as of 2026-05-14; refresh via fetchDictionaries()):
17
+ // batchId | batchName | type | totalCount
18
+ // 100000540002 | 阿里巴巴2027届实习生 | internship | 474
19
+ // 100000560002 | 阿里巴巴日常实习生 | project | 225
20
+ // 100000560001 | 阿里巴巴研究型实习生 | project | 188
21
+ // (graduate section is empty — no 校招正式 batch open as of 2026-05-14)
22
+ //
23
+ // FULL-TIME (校招正式) NOTE:
24
+ // Alibaba's full-time new-grad batch (graduate/trainee type) is NOT currently active.
25
+ // It historically opens in August–October. When it opens, /searchCondition/listBatch
26
+ // will populate the `graduate` array with a new batchId. Pass that batchId explicitly.
27
+ //
28
+ // BATCHID IS MANDATORY:
29
+ // POST /position/search with NO batchId returns totalCount=0. There is no "all batches"
30
+ // aggregate call — you must loop over each batchId to get the full picture.
31
+ //
32
+ // FILTER DIMENSIONS (passed as comma-joined strings in /position/search body):
33
+ // subCategories — category values from searchCondition/list (type="category")
34
+ // e.g. "11" (技术类), "1" (产品类), "11,1" (both) — comma-joined
35
+ // regions — city names from searchCondition/list (type="workCity")
36
+ // e.g. "北京", "北京,上海" — comma-joined city labels
37
+ // customDeptCode — child dept codes from searchCondition/list (type="customDept")
38
+ // Must use leaf-level codes (e.g. "JM3EV0" for 阿里云技术线),
39
+ // NOT parent codes (e.g. "60002" for 阿里云 returns 0 results).
40
+ // Comma-join multiple: "JM3EV0,5YTU0N"
41
+ //
42
+ // KEYWORD SEARCH:
43
+ // The correct field is `searchKey` (NOT `keyword`). searchKey works: passing "前端"
44
+ // returns 3 results, "算法" returns 87, "java" returns 2. The old `keyword` field is
45
+ // silently ignored by the server. This adapter now uses searchKey.
46
+ //
47
+ // CHANNEL NOTE:
48
+ // The live site uses "new_campus_group_official_site". Both channel values return
49
+ // identical counts for batchId 100000540002 (474 total), so either works.
50
+ //
51
+ // PositionSummary field mapping (Alibaba → canonical):
52
+ // post_id ← String(item.id) (numeric, e.g. 199903220038)
53
+ // title ← item.name
54
+ // project ← item.categoryName ?? "" (e.g. "技术类")
55
+ // recruit_label ← item.categoryType ?? "" (e.g. "internship")
56
+ // bgs ← item.circleNames?.[0] ?? "" (BU / group name)
57
+ // work_cities ← item.workLocations.join(" / ")
58
+ // apply_url ← https://campus-talent.alibaba.com/campus/positionDetail?positionId=<id>
59
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
60
+ export { extractResumeSignals, scoreOverlap, checkResume };
61
+ const API_ROOT = "https://campus-talent.alibaba.com";
62
+ const CAMPUS_PAGE = `${API_ROOT}/campus/position`;
63
+ const DETAIL_PAGE = (id) => `${API_ROOT}/campus/position/${encodeURIComponent(String(id))}`;
64
+ const DEFAULT_BATCH_ID = 100000540002;
65
+ const DEFAULT_CHANNEL = "new_campus_group_official_site";
66
+ const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
67
+ "(KHTML, like Gecko) Chrome/124.0 Safari/537.36";
68
+ let csrfCache = null;
69
+ async function acquireCsrf() {
70
+ let response;
71
+ try {
72
+ response = await fetch(CAMPUS_PAGE, {
73
+ method: "GET",
74
+ headers: {
75
+ "User-Agent": UA,
76
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
77
+ },
78
+ });
79
+ }
80
+ catch (err) {
81
+ return null;
82
+ }
83
+ if (!response.ok)
84
+ return null;
85
+ // Node fetch exposes Set-Cookie via getSetCookie() (Node 18+) or headers.raw()
86
+ let setCookieHeaders = [];
87
+ const rawHeaders = response.headers.raw;
88
+ if (typeof rawHeaders === "function") {
89
+ const raw = rawHeaders.call(response.headers);
90
+ setCookieHeaders = raw["set-cookie"] ?? [];
91
+ }
92
+ else if (typeof response.headers.getSetCookie === "function") {
93
+ setCookieHeaders = response.headers.getSetCookie();
94
+ }
95
+ let token = "";
96
+ let session = "";
97
+ for (const hdr of setCookieHeaders) {
98
+ const nameVal = hdr.split(";")[0].trim();
99
+ const [name, val] = nameVal.split("=").map((s) => s.trim());
100
+ if (name === "XSRF-TOKEN" && val)
101
+ token = val;
102
+ if (name === "SESSION" && val)
103
+ session = val;
104
+ }
105
+ if (!token)
106
+ return null;
107
+ return { token, session };
108
+ }
109
+ async function getCsrf(force = false) {
110
+ if (!force && csrfCache)
111
+ return csrfCache;
112
+ const state = await acquireCsrf();
113
+ if (state)
114
+ csrfCache = state;
115
+ return state ?? null;
116
+ }
117
+ async function call(path, body, retried = false) {
118
+ const csrf = await getCsrf();
119
+ if (!csrf) {
120
+ return { ok: false, message: "failed to acquire CSRF token from Alibaba" };
121
+ }
122
+ const cookieStr = `XSRF-TOKEN=${csrf.token}${csrf.session ? `; SESSION=${csrf.session}` : ""}`;
123
+ const headers = {
124
+ "User-Agent": UA,
125
+ "Content-Type": "application/json",
126
+ Accept: "application/json",
127
+ Referer: CAMPUS_PAGE,
128
+ "X-XSRF-TOKEN": csrf.token,
129
+ Cookie: cookieStr,
130
+ };
131
+ let response;
132
+ try {
133
+ response = await fetch(`${API_ROOT}${path}`, {
134
+ method: "POST",
135
+ headers,
136
+ body: JSON.stringify(body),
137
+ });
138
+ }
139
+ catch (err) {
140
+ return {
141
+ ok: false,
142
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
143
+ };
144
+ }
145
+ if (response.status === 403 && !retried) {
146
+ csrfCache = null;
147
+ return call(path, body, true);
148
+ }
149
+ if (!response.ok) {
150
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
151
+ }
152
+ let payload;
153
+ try {
154
+ payload = (await response.json());
155
+ }
156
+ catch (err) {
157
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
158
+ }
159
+ if (payload.success === false) {
160
+ return {
161
+ ok: false,
162
+ message: payload.errorMsg || payload.errorCode || "upstream returned success=false",
163
+ };
164
+ }
165
+ return {
166
+ ok: true,
167
+ data: payload.content,
168
+ message: "ok",
169
+ };
170
+ }
171
+ function summarizePosition(item) {
172
+ const id = String(item.id ?? "");
173
+ return {
174
+ post_id: id,
175
+ title: item.name ?? "",
176
+ project: item.categoryName ?? "",
177
+ recruit_label: item.categoryType ?? "",
178
+ bgs: (item.circleNames ?? [])[0] ?? "",
179
+ work_cities: (item.workLocations ?? []).join(" / "),
180
+ apply_url: id ? DETAIL_PAGE(id) : CAMPUS_PAGE,
181
+ };
182
+ }
183
+ function joinFilter(v) {
184
+ if (!v)
185
+ return undefined;
186
+ return Array.isArray(v) ? v.join(",") : v;
187
+ }
188
+ export async function searchPositions(opts = {}) {
189
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
190
+ const page = Math.max(1, opts.page ?? 1);
191
+ const searchKey = (opts.keyword ?? "").trim().slice(0, 60) || undefined;
192
+ const batchId = opts.batchId ?? DEFAULT_BATCH_ID;
193
+ const body = {
194
+ batchId,
195
+ pageIndex: page,
196
+ pageSize,
197
+ channel: DEFAULT_CHANNEL,
198
+ language: "zh",
199
+ };
200
+ if (searchKey)
201
+ body.searchKey = searchKey;
202
+ const subCategories = joinFilter(opts.subCategories);
203
+ if (subCategories)
204
+ body.subCategories = subCategories;
205
+ const regions = joinFilter(opts.regions);
206
+ if (regions)
207
+ body.regions = regions;
208
+ const customDeptCode = joinFilter(opts.customDeptCode);
209
+ if (customDeptCode)
210
+ body.customDeptCode = customDeptCode;
211
+ const response = await call("/position/search", body);
212
+ if (!response.ok || !response.data) {
213
+ return {
214
+ ok: false,
215
+ source: "campus-talent.alibaba.com",
216
+ message: response.message,
217
+ query: body,
218
+ page,
219
+ page_size: pageSize,
220
+ total: 0,
221
+ positions: [],
222
+ };
223
+ }
224
+ const rows = response.data.datas ?? [];
225
+ return {
226
+ ok: true,
227
+ source: "campus-talent.alibaba.com",
228
+ query: body,
229
+ page,
230
+ page_size: pageSize,
231
+ total: response.data.totalCount ?? rows.length,
232
+ positions: rows.map(summarizePosition),
233
+ };
234
+ }
235
+ // ---------- fetch all ----------
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 ?? 20);
239
+ const bucket = [];
240
+ let total;
241
+ for (let page = 1; page <= maxPages; page++) {
242
+ const result = await searchPositions({
243
+ keyword: opts.keyword,
244
+ page,
245
+ pageSize,
246
+ batchId: opts.batchId,
247
+ subCategories: opts.subCategories,
248
+ regions: opts.regions,
249
+ customDeptCode: opts.customDeptCode,
250
+ });
251
+ if (!result.ok) {
252
+ return {
253
+ ok: false,
254
+ source: "campus-talent.alibaba.com",
255
+ message: result.message ?? "search failed",
256
+ fetched: bucket.length,
257
+ positions: bucket,
258
+ };
259
+ }
260
+ if (total === undefined)
261
+ total = result.total;
262
+ if (!result.positions.length)
263
+ break;
264
+ bucket.push(...result.positions);
265
+ if (total !== undefined && bucket.length >= total)
266
+ break;
267
+ }
268
+ return {
269
+ ok: true,
270
+ source: "campus-talent.alibaba.com",
271
+ total: total ?? bucket.length,
272
+ fetched: bucket.length,
273
+ positions: bucket,
274
+ };
275
+ }
276
+ // ---------- position detail ----------
277
+ export async function fetchPositionDetail(postId) {
278
+ const id = String(postId ?? "").trim();
279
+ if (!id)
280
+ return { ok: false, message: "post_id is required" };
281
+ const numId = Number(id);
282
+ const body = { id: Number.isNaN(numId) ? id : numId };
283
+ const response = await call("/position/detail", body);
284
+ if (!response.ok || !response.data) {
285
+ return {
286
+ ok: false,
287
+ source: "campus-talent.alibaba.com",
288
+ message: response.message || "no detail returned",
289
+ post_id: id,
290
+ };
291
+ }
292
+ const raw = response.data;
293
+ return {
294
+ ok: true,
295
+ source: "campus-talent.alibaba.com",
296
+ post_id: String(raw.id ?? id),
297
+ title: raw.name ?? "",
298
+ direction: raw.categoryName ?? "",
299
+ description: (raw.description ?? "").trim(),
300
+ requirements: (raw.requirement ?? "").trim(),
301
+ work_cities: raw.workLocations ?? [],
302
+ recruit_cities: raw.interviewLocations ?? [],
303
+ bgs: (raw.circleNames ?? [])[0] ?? "",
304
+ batch_name: raw.batchName ?? "",
305
+ apply_url: DETAIL_PAGE(raw.id ?? id),
306
+ };
307
+ }
308
+ export async function fetchDictionaries() {
309
+ // Step 1: fetch all active batches
310
+ const batchRes = await call("/searchCondition/listBatch", {
311
+ channel: DEFAULT_CHANNEL,
312
+ language: "zh",
313
+ });
314
+ if (!batchRes.ok || !batchRes.data) {
315
+ return {
316
+ ok: false,
317
+ message: `Alibaba batch list failed: ${batchRes.message}`,
318
+ };
319
+ }
320
+ const rawBatchList = batchRes.data;
321
+ // Collect all batches across categories
322
+ const allRawBatches = [];
323
+ for (const cat of rawBatchList.sequence ?? ["graduate", "internship", "topTalentPlan"]) {
324
+ const list = rawBatchList[cat];
325
+ if (Array.isArray(list)) {
326
+ for (const b of list) {
327
+ allRawBatches.push({ batch: b, category: cat });
328
+ }
329
+ }
330
+ }
331
+ // Deduplicate by batchId (topTalentPlan reuses 100000540002)
332
+ const seen = new Set();
333
+ const uniqueBatches = allRawBatches.filter(({ batch }) => {
334
+ if (seen.has(batch.id))
335
+ return false;
336
+ seen.add(batch.id);
337
+ return true;
338
+ });
339
+ // Step 2: for each unique batch, fetch the filter taxonomy
340
+ const batches = await Promise.all(uniqueBatches.map(async ({ batch, category }) => {
341
+ const condRes = await call("/searchCondition/list", {
342
+ batchId: batch.id,
343
+ channel: DEFAULT_CHANNEL,
344
+ language: "zh",
345
+ });
346
+ const searchItems = condRes.data?.searchItems ?? [];
347
+ const filters = { categories: [], cities: [], customDepts: [] };
348
+ for (const si of searchItems) {
349
+ const items = si.items ?? [];
350
+ if (si.type === "category") {
351
+ filters.categories = items.map((x) => ({ label: x.label, value: x.value }));
352
+ }
353
+ else if (si.type === "workCity") {
354
+ filters.cities = items.map((x) => ({ label: x.label, value: x.value }));
355
+ }
356
+ else if (si.type === "customDept") {
357
+ filters.customDepts = items.map((x) => ({
358
+ label: x.label,
359
+ value: x.value,
360
+ children: x.children?.map((c) => ({ label: c.label, value: c.value })),
361
+ }));
362
+ }
363
+ }
364
+ return {
365
+ batchId: batch.id,
366
+ batchName: batch.name,
367
+ batchNameEn: batch.enName ?? "",
368
+ category, // "graduate" | "internship" | "topTalentPlan"
369
+ recruitType: batch.type ?? "", // "trainee" | "talent_plan" | "aliStar"
370
+ remark: batch.remark ?? "",
371
+ totalPositions: condRes.data?.totalPositions ?? null,
372
+ filters,
373
+ };
374
+ }));
375
+ return {
376
+ ok: true,
377
+ source: "campus-talent.alibaba.com",
378
+ note: "batchId is mandatory for /position/search — no cross-batch aggregate exists. " +
379
+ "Graduate (校招正式) batch is empty; it typically opens Aug–Oct. " +
380
+ "Use subCategories/regions/customDeptCode filters in searchPositions(). " +
381
+ "customDeptCode requires CHILD-level codes (leaf nodes), not parent group codes.",
382
+ batches,
383
+ };
384
+ }
385
+ export async function listNotices() {
386
+ return {
387
+ ok: false,
388
+ message: "Alibaba: no public notices endpoint",
389
+ };
390
+ }
391
+ export async function getNotice(_id) {
392
+ return {
393
+ ok: false,
394
+ message: "Alibaba: no public notice detail endpoint",
395
+ };
396
+ }
397
+ export async function findNoticesByQuestion(_question, _opts = {}) {
398
+ return {
399
+ ok: false,
400
+ message: "Alibaba: no public notices endpoint",
401
+ matches: [],
402
+ };
403
+ }
404
+ // ---------- resume matching ----------
405
+ export async function matchResume(text, opts = {}) {
406
+ const topN = Math.max(1, opts.topN ?? 5);
407
+ const candidates = Math.max(topN, opts.candidates ?? 20);
408
+ const { terms, cities } = extractResumeSignals(text ?? "");
409
+ if (!terms.length) {
410
+ return {
411
+ ok: false,
412
+ message: "could not extract any technical signals from the text",
413
+ preview: (text ?? "").slice(0, 120),
414
+ };
415
+ }
416
+ const keyword = terms.slice(0, 3).join(" ");
417
+ const list = await searchPositions({ keyword, page: 1, pageSize: 100 });
418
+ if (!list.ok) {
419
+ return {
420
+ ok: false,
421
+ message: list.message ?? "search failed",
422
+ positions: [],
423
+ };
424
+ }
425
+ const pre = [];
426
+ for (const p of list.positions) {
427
+ const blob = [p.title, p.project, p.recruit_label, p.bgs, p.work_cities].join(" ");
428
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
429
+ if (score > 0)
430
+ pre.push({ score, position: p, reasons });
431
+ }
432
+ pre.sort((a, b) => b.score - a.score);
433
+ let shortlist = pre.slice(0, Math.max(topN, candidates));
434
+ if (!shortlist.length) {
435
+ shortlist = list.positions.slice(0, candidates).map((position) => ({
436
+ score: 0,
437
+ position,
438
+ reasons: [],
439
+ }));
440
+ }
441
+ const enriched = [];
442
+ for (const { score: baseScore, position, reasons: baseReasons } of shortlist.slice(0, candidates)) {
443
+ const detail = await fetchPositionDetail(position.post_id);
444
+ if (!detail.ok)
445
+ continue;
446
+ const jdBlob = [
447
+ detail.title,
448
+ detail.direction,
449
+ detail.description,
450
+ detail.requirements,
451
+ (detail.work_cities ?? []).join(" "),
452
+ ].join(" ");
453
+ const { score: extraScore, reasons: extraReasons } = scoreOverlap(jdBlob, terms, cities);
454
+ const combined = [...new Set([...baseReasons, ...extraReasons])].slice(0, 5);
455
+ if (!combined.length)
456
+ combined.push("no specific keyword overlap — surfaced from initial keyword search");
457
+ enriched.push({
458
+ score: baseScore + extraScore,
459
+ row: {
460
+ ...position,
461
+ direction: detail.direction,
462
+ description: detail.description,
463
+ requirements: detail.requirements,
464
+ match_reasons: combined,
465
+ },
466
+ });
467
+ }
468
+ enriched.sort((a, b) => b.score - a.score);
469
+ return {
470
+ ok: true,
471
+ source: "campus-talent.alibaba.com",
472
+ extracted_terms: terms,
473
+ city_preferences: cities,
474
+ matches: enriched.slice(0, topN).map((e) => e.row),
475
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
476
+ "The only authority on selection is HR.",
477
+ };
478
+ }
479
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_alibaba } from "./apply.js";
480
+ export async function fetchApplicationSchema(postId) {
481
+ const id = (postId ?? "").trim();
482
+ if (!id)
483
+ return { ok: false, source: "campus-talent.alibaba.com", message: "post_id is required" };
484
+ let title = "";
485
+ let applyUrl = "https://campus-talent.alibaba.com";
486
+ try {
487
+ const detail = (await fetchPositionDetail(id));
488
+ if (detail?.ok === false) {
489
+ return { ok: false, source: "campus-talent.alibaba.com", message: detail.message ?? "post not found" };
490
+ }
491
+ title = detail?.title ?? "";
492
+ if (detail?.apply_url)
493
+ applyUrl = detail.apply_url;
494
+ }
495
+ catch { }
496
+ return {
497
+ ok: true,
498
+ schema: _buildBespokeApplySchema_alibaba({
499
+ source: "campus-talent.alibaba.com",
500
+ postId: id,
501
+ jobTitle: title,
502
+ applyUrl,
503
+ submitEndpoint: "https://campus-talent.alibaba.com/campus/applyPosition.json",
504
+ submitKind: "multipart-session",
505
+ endpointVerified: true,
506
+ submitNotes: "Alibaba — POST /campus/applyPosition.json with session cookie. Alipay OAuth gates the session. Endpoint anon-probed → HTTP 403 (auth gate, not 404 — confirms real route). Body shape still needs validation against a real candidate session.",
507
+ }),
508
+ };
509
+ }