@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/jd.js ADDED
@@ -0,0 +1,559 @@
1
+ // Thin client for JD (京东) campus-recruiting API at campus.jd.com.
2
+ //
3
+ // ============================================================
4
+ // Endpoint inventory (probed 2026-05, JS bundle umi.js 20260511224015):
5
+ //
6
+ // GET https://campus.jd.com/api/wx/position/getProjectList
7
+ // Unauthenticated. Returns recruit types (code: present/internship/talent),
8
+ // plan IDs, direction code lists, and BG names.
9
+ // Response: { success:true, body:{ projectList:[...], bgList:[...], bgbuConfig:[...] } }
10
+ //
11
+ // POST https://campus.jd.com/api/wx/position/page?type={{type}}
12
+ // Unauthenticated. type = "present" | "internship" | "talent"
13
+ // Payload: { pageSize:int, pageIndex:int,
14
+ // parameter:{ positionName:str, planIdList:int[], positionDeptList:[],
15
+ // jobDirectionCodeList:str[], workCityCodeList:str[] } }
16
+ // Response: { success:true, body:{ totalNumber:int, items:[...] } }
17
+ // - Each item has publishId (= post_id), positionName, jobDirection, jobDirectionCode,
18
+ // workContent, qualification, and requirementVoList (array per city/BG).
19
+ // - positionDeptList: the server accepts [] (no-op); no public dictionary for dept codes.
20
+ // - workCityCodeList: city codes from requirementVoList[].workCityCode (e.g. "00001"=北京).
21
+ // - jobDirectionCodeList: string codes from items[].jobDirectionCode.
22
+ // Observed direction codes (probed 2026-05):
23
+ // "01" 采销与物流方向 "02" 技术方向 "03" 产品方向 "04" 运营方向
24
+ // "05" 供应链方向 "06" 设计方向 "09" 保险及金融方向
25
+ // "10" 新锐之星方向 "13" 管理培训生方向 "14" TGT顶尖技术方向
26
+ // "16" 数据方向 "17" 市场方向 "18" 人力方向
27
+ // "19" 财务方向 "20" 法务方向 "30" 基层管理方向
28
+ // "31" 一线销售方向 "34" 职能方向
29
+ //
30
+ // GET https://campus.jd.com/api/wx/position/detail/{{publishId}}
31
+ // Unauthenticated. Returns the same fields as the list item but with full
32
+ // workContent + qualification text. requirementVoList carries positionBg and workCity.
33
+ // Response: { success:true, body:{ publishId, positionName, jobDirection, ... } }
34
+ //
35
+ // ============================================================
36
+ // Endpoints that require JD SSO auth (all redirect to /passport):
37
+ // POST /api/position/list, /api/social/position/list, /api/campus/position/list,
38
+ // /api/wx/position/page?type=... (GET variant), /api/wx/position/delivery/*,
39
+ // /api/wx/resume/*, /api/wx/favorites/*, /api/common/recruit/dict/*
40
+ //
41
+ // ============================================================
42
+ // Recruit types (from getProjectList, probed 2026-05):
43
+ // code "present" 应届生 ~23 positions
44
+ // planId 52 = JDS-新星计划 (directions 01-06)
45
+ // planId 53 = TET-管理培训生 (direction 13)
46
+ // planId 54 = 新锐之星 (direction 10)
47
+ // code "internship" 实习生 ~110 positions
48
+ // planId 45 = JD YOUNG-实习生计划 (directions 03,04,06,16-20)
49
+ // planId 51 = 新锐之星实习生 (direction 10)
50
+ // code "talent" TGT专项 ~155 positions
51
+ // planId 47 = TGT-顶尖青年技术天才计划 (direction 14)
52
+ // planId 55 = TGT-顶尖青年技术实习生 (direction 14)
53
+ //
54
+ // ============================================================
55
+ // BG names (from getProjectList bgList):
56
+ // 京东集团, 京东零售, 京东物流, 京东科技,
57
+ // 京东健康, 京东国际, 京东产发, 京东工业, 京东创新零售, CHO体系, CCO体系, CFO体系
58
+ //
59
+ // ============================================================
60
+ // ---- PositionSummary field mapping (JD → canonical) ----
61
+ // post_id ← String(item.publishId)
62
+ // title ← item.positionName
63
+ // project ← item.jobDirection (职位方向, e.g. "技术方向")
64
+ // recruit_label ← recruitType label (e.g. "应届生" / "实习生" / "TGT专项")
65
+ // bgs ← unique positionBg values from requirementVoList joined with " / "
66
+ // work_cities ← unique workCity values from requirementVoList joined with " / "
67
+ // apply_url ← https://campus.jd.com/#/newDetails?publishId=<id>
68
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
69
+ export { checkResume };
70
+ const API_ROOT = "https://campus.jd.com";
71
+ const CAMPUS_PAGE = "https://campus.jd.com/";
72
+ const DETAIL_PAGE = (publishId) => `${CAMPUS_PAGE}#/newDetails?publishId=${encodeURIComponent(publishId)}`;
73
+ const DEFAULT_HEADERS = {
74
+ "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",
75
+ Accept: "application/json, text/plain, */*",
76
+ "Content-Type": "application/json",
77
+ Referer: CAMPUS_PAGE,
78
+ };
79
+ // ---------- helpers ----------
80
+ async function getJson(url) {
81
+ let response;
82
+ try {
83
+ response = await fetch(url, { headers: DEFAULT_HEADERS });
84
+ }
85
+ catch (err) {
86
+ return {
87
+ ok: false,
88
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
89
+ };
90
+ }
91
+ if (!response.ok) {
92
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
93
+ }
94
+ let payload;
95
+ try {
96
+ payload = (await response.json());
97
+ }
98
+ catch (err) {
99
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : String(err)}` };
100
+ }
101
+ return { ok: true, data: payload, message: "ok" };
102
+ }
103
+ async function postJson(url, body) {
104
+ let response;
105
+ try {
106
+ response = await fetch(url, {
107
+ method: "POST",
108
+ headers: DEFAULT_HEADERS,
109
+ body: JSON.stringify(body),
110
+ });
111
+ }
112
+ catch (err) {
113
+ return {
114
+ ok: false,
115
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
116
+ };
117
+ }
118
+ if (!response.ok) {
119
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
120
+ }
121
+ let payload;
122
+ try {
123
+ payload = (await response.json());
124
+ }
125
+ catch (err) {
126
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : String(err)}` };
127
+ }
128
+ return { ok: true, data: payload, message: "ok" };
129
+ }
130
+ function summarizePosition(item, recruitLabel) {
131
+ const id = String(item.publishId ?? "");
132
+ const reqs = item.requirementVoList ?? [];
133
+ const seenCities = new Set();
134
+ const seenBgs = new Set();
135
+ for (const r of reqs) {
136
+ if (r.workCity)
137
+ seenCities.add(r.workCity);
138
+ if (r.positionBg)
139
+ seenBgs.add(r.positionBg);
140
+ }
141
+ return {
142
+ post_id: id,
143
+ title: item.positionName ?? "",
144
+ project: item.jobDirection ?? "",
145
+ recruit_label: recruitLabel,
146
+ bgs: [...seenBgs].join(" / "),
147
+ work_cities: [...seenCities].join(" / "),
148
+ apply_url: id ? DETAIL_PAGE(id) : CAMPUS_PAGE,
149
+ };
150
+ }
151
+ // Label mapping for the three recruit type codes
152
+ const TYPE_LABELS = {
153
+ present: "应届生",
154
+ internship: "实习生",
155
+ talent: "TGT专项",
156
+ };
157
+ // ---------- searchPositions ----------
158
+ export async function searchPositions(opts = {}) {
159
+ const pageSize = Math.max(1, Math.min(200, opts.pageSize ?? 20));
160
+ const page = Math.max(1, opts.page ?? 1);
161
+ const recruitType = opts.recruitType ?? "present";
162
+ const recruitLabel = TYPE_LABELS[recruitType] ?? recruitType;
163
+ const keyword = (opts.keyword ?? "").trim().slice(0, 60);
164
+ const payload = {
165
+ pageSize,
166
+ pageIndex: page,
167
+ parameter: {
168
+ positionName: keyword,
169
+ planIdList: opts.planIdList ?? [],
170
+ positionDeptList: [],
171
+ jobDirectionCodeList: opts.jobDirectionCodeList ?? [],
172
+ workCityCodeList: opts.workCityCodeList ?? [],
173
+ },
174
+ };
175
+ const url = `${API_ROOT}/api/wx/position/page?type=${encodeURIComponent(recruitType)}`;
176
+ const resp = await postJson(url, payload);
177
+ if (!resp.ok || !resp.data) {
178
+ return {
179
+ ok: false,
180
+ source: "campus.jd.com",
181
+ message: resp.message,
182
+ query: payload,
183
+ page,
184
+ page_size: pageSize,
185
+ total: 0,
186
+ positions: [],
187
+ };
188
+ }
189
+ const d = resp.data;
190
+ if (!d.success) {
191
+ return {
192
+ ok: false,
193
+ source: "campus.jd.com",
194
+ message: d.errorMessage ?? "upstream returned success=false",
195
+ query: payload,
196
+ page,
197
+ page_size: pageSize,
198
+ total: 0,
199
+ positions: [],
200
+ };
201
+ }
202
+ const items = d.body?.items ?? [];
203
+ return {
204
+ ok: true,
205
+ source: "campus.jd.com",
206
+ query: payload,
207
+ page,
208
+ page_size: pageSize,
209
+ total: d.body?.totalNumber ?? items.length,
210
+ positions: items.map((item) => summarizePosition(item, recruitLabel)),
211
+ };
212
+ }
213
+ // ---------- fetchAllPositions ----------
214
+ export async function fetchAllPositions(opts = {}) {
215
+ const pageSize = Math.max(1, Math.min(200, opts.pageSize ?? 100));
216
+ const maxPages = Math.max(1, opts.maxPages ?? 10);
217
+ const bucket = [];
218
+ let total;
219
+ for (let page = 1; page <= maxPages; page++) {
220
+ const result = await searchPositions({ ...opts, page, pageSize });
221
+ if (!result.ok) {
222
+ return {
223
+ ok: false,
224
+ source: "campus.jd.com",
225
+ message: result.message,
226
+ total: total ?? 0,
227
+ fetched: bucket.length,
228
+ positions: bucket,
229
+ };
230
+ }
231
+ if (total === undefined)
232
+ total = result.total;
233
+ if (!result.positions.length)
234
+ break;
235
+ bucket.push(...result.positions);
236
+ if (total !== undefined && bucket.length >= total)
237
+ break;
238
+ }
239
+ return {
240
+ ok: true,
241
+ source: "campus.jd.com",
242
+ total: total ?? bucket.length,
243
+ fetched: bucket.length,
244
+ positions: bucket,
245
+ };
246
+ }
247
+ // ---------- fetchPositionDetail ----------
248
+ export async function fetchPositionDetail(postId) {
249
+ const id = (postId ?? "").trim();
250
+ if (!id) {
251
+ return {
252
+ ok: false,
253
+ source: "campus.jd.com",
254
+ message: "post_id is required",
255
+ };
256
+ }
257
+ const url = `${API_ROOT}/api/wx/position/detail/${encodeURIComponent(id)}`;
258
+ const resp = await getJson(url);
259
+ if (!resp.ok || !resp.data) {
260
+ return {
261
+ ok: false,
262
+ source: "campus.jd.com",
263
+ post_id: id,
264
+ message: resp.message,
265
+ };
266
+ }
267
+ if (!resp.data.success) {
268
+ return {
269
+ ok: false,
270
+ source: "campus.jd.com",
271
+ post_id: id,
272
+ message: resp.data.errorMessage ?? "upstream returned success=false",
273
+ };
274
+ }
275
+ const raw = resp.data.body;
276
+ if (!raw) {
277
+ return {
278
+ ok: false,
279
+ source: "campus.jd.com",
280
+ post_id: id,
281
+ message: "empty body in detail response",
282
+ };
283
+ }
284
+ const reqs = raw.requirementVoList ?? [];
285
+ const seenCities = [];
286
+ const seenBgs = [];
287
+ const seenCitySet = new Set();
288
+ const seenBgSet = new Set();
289
+ for (const r of reqs) {
290
+ if (r.workCity && !seenCitySet.has(r.workCity)) {
291
+ seenCities.push(r.workCity);
292
+ seenCitySet.add(r.workCity);
293
+ }
294
+ if (r.positionBg && !seenBgSet.has(r.positionBg)) {
295
+ seenBgs.push(r.positionBg);
296
+ seenBgSet.add(r.positionBg);
297
+ }
298
+ }
299
+ return {
300
+ ok: true,
301
+ source: "campus.jd.com",
302
+ post_id: String(raw.publishId ?? id),
303
+ title: raw.positionName ?? "",
304
+ direction: raw.jobDirection ?? "",
305
+ description: raw.workContent ?? "",
306
+ requirements: raw.qualification ?? "",
307
+ work_cities: seenCities,
308
+ recruit_cities: reqs
309
+ .map((r) => r.interviewCity)
310
+ .filter((v) => Boolean(v))
311
+ .filter((v, i, arr) => arr.indexOf(v) === i),
312
+ bgs: seenBgs,
313
+ apply_url: DETAIL_PAGE(String(raw.publishId ?? id)),
314
+ };
315
+ }
316
+ // ---------- fetchDictionaries ----------
317
+ // GET /api/wx/position/getProjectList is unauthenticated and returns the full
318
+ // recruit-type × plan × direction taxonomy plus the BG name list.
319
+ // No city dictionary or department code dictionary exists publicly.
320
+ let _projectCache = null;
321
+ function _buildDictResult(data) {
322
+ const body = data.body ?? {};
323
+ const projectList = body.projectList ?? [];
324
+ const plans = [];
325
+ for (const p of projectList) {
326
+ for (const g of p.groupList ?? []) {
327
+ for (const pm of g.planMapList ?? []) {
328
+ plans.push({
329
+ id: pm.id ?? 0,
330
+ name: pm.planName ?? "",
331
+ recruitType: p.type ?? "",
332
+ recruitTypeCode: p.code ?? "",
333
+ directionCodes: pm.directionList ?? [],
334
+ });
335
+ }
336
+ }
337
+ }
338
+ const knownDirections = {
339
+ "01": "采销与物流方向",
340
+ "02": "技术方向",
341
+ "03": "产品方向",
342
+ "04": "运营方向",
343
+ "05": "供应链方向",
344
+ "06": "设计方向",
345
+ "09": "保险及金融方向",
346
+ "10": "新锐之星方向",
347
+ "13": "管理培训生方向",
348
+ "14": "TGT顶尖技术方向",
349
+ "16": "数据方向",
350
+ "17": "市场方向",
351
+ "18": "人力方向",
352
+ "19": "财务方向",
353
+ "20": "法务方向",
354
+ "30": "基层管理方向",
355
+ "31": "一线销售方向",
356
+ "34": "职能方向",
357
+ };
358
+ return {
359
+ ok: true,
360
+ source: "campus.jd.com",
361
+ verified_at: new Date().toISOString(),
362
+ recruit_types: projectList.map((p) => ({
363
+ code: p.code ?? "",
364
+ name: p.type ?? "",
365
+ label: TYPE_LABELS[p.code ?? ""] ?? p.type ?? "",
366
+ })),
367
+ plans,
368
+ job_directions: Object.entries(knownDirections).map(([code, name]) => ({ code, name })),
369
+ business_groups: body.bgList ?? [],
370
+ business_group_details: (body.bgbuConfig ?? []).map((b) => ({
371
+ name: b.name ?? "",
372
+ queryName: b.queryName ?? "",
373
+ description: b.descriptions ?? "",
374
+ })),
375
+ note: "City codes live in requirementVoList[].workCityCode on each position item — " +
376
+ "no public city dictionary endpoint exists. " +
377
+ "Department codes are not publicly exposed.",
378
+ };
379
+ }
380
+ export async function fetchDictionaries() {
381
+ if (_projectCache !== null)
382
+ return _projectCache;
383
+ const url = `${API_ROOT}/api/wx/position/getProjectList`;
384
+ const resp = await getJson(url);
385
+ if (!resp.ok || !resp.data) {
386
+ const r = {
387
+ ok: false,
388
+ source: "campus.jd.com",
389
+ message: `JD: getProjectList failed — ${resp.message}`,
390
+ };
391
+ return r;
392
+ }
393
+ if (!resp.data.success) {
394
+ const r = {
395
+ ok: false,
396
+ source: "campus.jd.com",
397
+ message: `JD: getProjectList returned success=false — ${resp.data.errorMessage ?? ""}`,
398
+ };
399
+ return r;
400
+ }
401
+ const result = _buildDictResult(resp.data);
402
+ _projectCache = result;
403
+ return result;
404
+ }
405
+ // ---------- stub notices ----------
406
+ // No public notice/announcement endpoint was found.
407
+ const STUB_NOTICES_RESULT = {
408
+ ok: false,
409
+ source: "campus.jd.com",
410
+ message: "JD: no public notices endpoint",
411
+ };
412
+ export async function listNotices() {
413
+ return STUB_NOTICES_RESULT;
414
+ }
415
+ export async function getNotice(_id) {
416
+ return {
417
+ ok: false,
418
+ source: "campus.jd.com",
419
+ message: "JD: no public notices endpoint",
420
+ };
421
+ }
422
+ export async function findNoticesByQuestion(_question, _opts = {}) {
423
+ return {
424
+ ok: false,
425
+ source: "campus.jd.com",
426
+ message: "JD: no public notices endpoint",
427
+ };
428
+ }
429
+ // ---------- matchResume ----------
430
+ // Mirror tencent's algorithm:
431
+ // 1. Extract signals from resume text.
432
+ // 2. Search with top-3 terms as keyword across recruitType="internship" (larger pool).
433
+ // 3. Score each position against title + direction + BG + cities + description blobs.
434
+ // 4. Enrich top candidates with full detail and re-score.
435
+ // 5. Return top N matches with reasons.
436
+ export async function matchResume(text, opts = {}) {
437
+ const topN = Math.max(1, opts.topN ?? 5);
438
+ const candidates = Math.max(topN, opts.candidates ?? 20);
439
+ const recruitType = opts.recruitType ?? "internship";
440
+ const { terms, cities } = extractResumeSignals(text ?? "");
441
+ if (!terms.length) {
442
+ return {
443
+ ok: false,
444
+ source: "campus.jd.com",
445
+ message: "could not extract any technical signals from the text",
446
+ preview: (text ?? "").slice(0, 120),
447
+ };
448
+ }
449
+ const keyword = terms.slice(0, 3).join(" ");
450
+ // Fetch up to 200 positions so we have a good candidate pool
451
+ const list = await searchPositions({ keyword, page: 1, pageSize: 200, recruitType });
452
+ if (!list.ok) {
453
+ return {
454
+ ok: false,
455
+ source: "campus.jd.com",
456
+ message: list.message,
457
+ positions: [],
458
+ };
459
+ }
460
+ // If keyword search returns few results, fall back to full list
461
+ const pool = list.positions.length < 10
462
+ ? (await searchPositions({ page: 1, pageSize: 200, recruitType })).positions
463
+ : list.positions;
464
+ const scored = [];
465
+ for (const p of pool) {
466
+ const blob = [p.title, p.project, p.recruit_label, p.bgs, p.work_cities].join(" ");
467
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
468
+ if (score > 0) {
469
+ scored.push({ score, position: p, reasons });
470
+ }
471
+ }
472
+ scored.sort((a, b) => b.score - a.score);
473
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
474
+ if (!shortlist.length) {
475
+ // Fallback: return first candidates from pool
476
+ shortlist = pool.slice(0, candidates).map((position) => ({
477
+ score: 0,
478
+ position,
479
+ reasons: [],
480
+ }));
481
+ }
482
+ const enriched = [];
483
+ for (const { score: baseScore, position, reasons: baseReasons } of shortlist.slice(0, candidates)) {
484
+ const detail = await fetchPositionDetail(position.post_id);
485
+ let extraScore = 0;
486
+ let extraReasons = [];
487
+ let description;
488
+ let requirements;
489
+ if (detail.ok) {
490
+ description = detail.description;
491
+ requirements = detail.requirements;
492
+ const detailBlob = [
493
+ detail.title,
494
+ detail.direction,
495
+ detail.description,
496
+ detail.requirements,
497
+ detail.bgs.join(" "),
498
+ detail.work_cities.join(" "),
499
+ ].join(" ");
500
+ const r = scoreOverlap(detailBlob, terms, cities);
501
+ extraScore = r.score;
502
+ extraReasons = r.reasons;
503
+ }
504
+ const combined = [...new Set([...baseReasons, ...extraReasons])].slice(0, 5);
505
+ if (!combined.length) {
506
+ combined.push("no specific keyword overlap — surfaced from initial keyword search");
507
+ }
508
+ enriched.push({
509
+ score: baseScore + extraScore,
510
+ row: {
511
+ ...position,
512
+ description,
513
+ requirements,
514
+ match_reasons: combined,
515
+ },
516
+ });
517
+ }
518
+ enriched.sort((a, b) => b.score - a.score);
519
+ return {
520
+ ok: true,
521
+ source: "campus.jd.com",
522
+ extracted_terms: terms,
523
+ city_preferences: cities,
524
+ matches: enriched.slice(0, topN).map((e) => e.row),
525
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
526
+ "The only authority on selection is HR.",
527
+ };
528
+ }
529
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_jd } from "./apply.js";
530
+ export async function fetchApplicationSchema(postId) {
531
+ const id = (postId ?? "").trim();
532
+ if (!id)
533
+ return { ok: false, source: "campus.jd.com", message: "post_id is required" };
534
+ let title = "";
535
+ let applyUrl = "https://campus.jd.com";
536
+ try {
537
+ const detail = (await fetchPositionDetail(id));
538
+ if (detail?.ok === false) {
539
+ return { ok: false, source: "campus.jd.com", message: detail.message ?? "post not found" };
540
+ }
541
+ title = detail?.title ?? "";
542
+ if (detail?.apply_url)
543
+ applyUrl = detail.apply_url;
544
+ }
545
+ catch { }
546
+ return {
547
+ ok: true,
548
+ schema: _buildBespokeApplySchema_jd({
549
+ source: "campus.jd.com",
550
+ postId: id,
551
+ jobTitle: title,
552
+ applyUrl,
553
+ submitEndpoint: "https://wutongzhaopin.jd.com/api/wx/delivery",
554
+ submitKind: "multipart-session",
555
+ endpointVerified: true,
556
+ submitNotes: "JD — POST /api/wx/delivery on wutongzhaopin.jd.com (JD's careers backend). Endpoint extracted from the campus.jd.com SPA's umi.js bundle (gzip-decompressed); also documented sibling routes /api/wx/activityDelivery/activityDelivery, /api/wx/favorites/add, /api/wx/delivery/list. The campus.jd.com frontend domain serves SPA HTML only; XHR traffic targets wutongzhaopin.jd.com. Requires session cookies + JD passport login. Probe from US IP returns ECONNRESET (geo-fenced); the URL is verified via static JS-bundle analysis, not live anonymous probe.",
557
+ }),
558
+ };
559
+ }