@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/huawei.js ADDED
@@ -0,0 +1,537 @@
1
+ // Thin client for Huawei's public campus-recruiting portal at career.huawei.com.
2
+ //
3
+ // ============================================================
4
+ // Endpoint discovery (probed 2026-05, JS bundles HwPortalReccamp.js,
5
+ // portal5/campus-recruitment.html):
6
+ //
7
+ // Final URL after redirect chain:
8
+ // career.huawei.com → reccampportal/ → campus4_index.html
9
+ // → /reccampportal/portal5/index.html (SPA)
10
+ // → /reccampportal/portal5/campus-recruitment.html (job search page)
11
+ //
12
+ // Session:
13
+ // The portal sets JSESSIONID on the first GET to /reccampportal/ (no login
14
+ // required). All public endpoints accept the session or even work without it.
15
+ //
16
+ // ============================================================
17
+ // Endpoint inventory (unauthenticated, no CSRF token needed):
18
+ //
19
+ // GET https://career.huawei.com/reccampportal/services/portal/portalpub/
20
+ // getJob/newHr/page/{pageSize}/{curPage}
21
+ // Query params: jobType, jobTypes, searchText, jobFamClsCode, language,
22
+ // reqTime, orderBy, cityCode, countryCode, graduateItem
23
+ // Response: { pageVO:{totalRows,curPage,pageSize,totalPages,...}, result:[...] }
24
+ //
25
+ // jobType/jobTypes semantics (mapped from campus-recruitment.html Vue data):
26
+ // jobType=0, jobTypes=2 → 应届生 (new-grad / campus) — default zh_CN, ~60 posts
27
+ // jobType=0, jobTypes=1 → 留学生 (overseas students)
28
+ // jobType=0, jobTypes=0 → 实习生 (intern) — actually returns PhD 博士, ~30 posts
29
+ // jobType=2, jobTypes=null → 博士生 (PhD)
30
+ // jobType=0, jobTypes=-1 → 博士生 (PhD, same as jobType=2)
31
+ // jobType=0, jobTypes=-2 → 海外博士 (overseas PhD)
32
+ // jobType=0, jobTypes=-3 → 中方博士 (Chinese PhD)
33
+ // jobType=0, jobTypes=7 → 海外本地 (overseas local)
34
+ // jobType=3 → all types combined (~420 posts in 2026-05)
35
+ // Default (no filter): jobType=0 → ~328 posts (all campus types)
36
+ //
37
+ // GET https://career.huawei.com/reccampportal/services/portal/portalpub/
38
+ // getJobDetail/newHr?jobId={jobId}&dataSource={dataSource}&language=zh_CN
39
+ // The {jobId} is item.jobId from the search result (NOT advertisementsIntegrationId).
40
+ // Response: flat object with jobname, mainBusiness, jobRequire, jobAddress, jobFamilyName, etc.
41
+ // NOTE: many posts return generic placeholder text for mainBusiness/jobRequire;
42
+ // use findIntentListByJobRequirementId for the real JD.
43
+ //
44
+ // GET https://career.huawei.com/reccampportal/services/portal/portaluser/
45
+ // findIntentListByJobRequirementId/newHr/{language}/{jobRequirementId}/null
46
+ // Query params: dataSource, jobId
47
+ // Returns array of position intents; each intent has full jobResponsibilities,
48
+ // jobDemand, deptName, jobPlaceName, positionIntention (sub-position title).
49
+ // This is the real JD when the top-level description is a placeholder.
50
+ //
51
+ // ============================================================
52
+ // Endpoints that are NOT public (require user login):
53
+ // services/portal/portaluser/applyJob/newHr
54
+ // services/portal/portaluser/collectJob/newHr
55
+ // services/portal/portalpub/getJobAllCount (→ 404)
56
+ // services/portal/portalpub/findStatAddress/ (→ 404)
57
+ // services/portal/portalpub/list/lang/ (→ 403)
58
+ //
59
+ // ============================================================
60
+ // PositionSummary field mapping (Huawei → canonical):
61
+ // post_id ← String(item.jobId) (internal job ID, used for detail API)
62
+ // title ← item.jobname
63
+ // project ← item.jobFamilyName (职族, e.g. "研发族" / "销售族")
64
+ // recruit_label ← derived from jobTypes param (e.g. "应届生" / "博士生")
65
+ // bgs ← "" (not exposed in public search)
66
+ // work_cities ← item.jobArea (pre-formatted Chinese city list)
67
+ // apply_url ← https://career.huawei.com/reccampportal/portal5/campus-recruitment-detail.html
68
+ // ?jobId={item.jobId}&dataSource={item.dataSource}
69
+ //
70
+ // ============================================================
71
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
72
+ export { checkResume };
73
+ const PORTAL_ROOT = "https://career.huawei.com/reccampportal";
74
+ const API_ROOT = `${PORTAL_ROOT}/services/portal/portalpub`;
75
+ const USER_API_ROOT = `${PORTAL_ROOT}/services/portal/portaluser`;
76
+ const CAMPUS_PAGE = `${PORTAL_ROOT}/portal5/campus-recruitment.html`;
77
+ const DETAIL_PAGE = (jobId, dataSource) => `${PORTAL_ROOT}/portal5/campus-recruitment-detail.html?jobId=${encodeURIComponent(jobId)}&dataSource=${encodeURIComponent(dataSource)}`;
78
+ const DEFAULT_HEADERS = {
79
+ "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",
80
+ Accept: "application/json, text/plain, */*",
81
+ Referer: CAMPUS_PAGE,
82
+ };
83
+ // ---------- session bootstrap ----------
84
+ // Huawei sets JSESSIONID on first GET to /reccampportal/. The public endpoints
85
+ // work without a session, but passing one avoids cookie-challenged 403s.
86
+ // Cache the session token for the lifetime of the process.
87
+ let _session = null;
88
+ async function getSession() {
89
+ if (_session !== null)
90
+ return _session;
91
+ try {
92
+ const resp = await fetch(`${PORTAL_ROOT}/`, {
93
+ headers: DEFAULT_HEADERS,
94
+ redirect: "follow",
95
+ });
96
+ const setCookie = resp.headers.get("set-cookie") ?? "";
97
+ const match = setCookie.match(/JSESSIONID=([^;]+)/);
98
+ _session = match ? match[1] : "";
99
+ return _session;
100
+ }
101
+ catch {
102
+ _session = "";
103
+ return _session;
104
+ }
105
+ }
106
+ // ---------- low-level GET helper ----------
107
+ async function getJson(url, params = {}) {
108
+ const session = await getSession();
109
+ // Build query string from params
110
+ const qs = Object.entries(params)
111
+ .filter(([, v]) => v !== undefined && v !== "" && v !== null)
112
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
113
+ .join("&");
114
+ const fullUrl = qs ? `${url}?${qs}` : url;
115
+ const headers = { ...DEFAULT_HEADERS };
116
+ if (session)
117
+ headers["Cookie"] = `JSESSIONID=${session}`;
118
+ let response;
119
+ try {
120
+ response = await fetch(fullUrl, { headers });
121
+ }
122
+ catch (err) {
123
+ return {
124
+ ok: false,
125
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
126
+ };
127
+ }
128
+ if (!response.ok) {
129
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
130
+ }
131
+ let payload;
132
+ try {
133
+ payload = (await response.json());
134
+ }
135
+ catch (err) {
136
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : String(err)}` };
137
+ }
138
+ return { ok: true, data: payload, message: "ok" };
139
+ }
140
+ // Maps (jobType, jobTypes) param values → human-readable label
141
+ const RECRUIT_LABELS = {
142
+ "0/2": "应届生",
143
+ "0/1": "留学生",
144
+ "0/0": "实习生",
145
+ "2/": "博士生",
146
+ "0/-1": "博士生",
147
+ "0/-2": "海外博士",
148
+ "0/-3": "中方博士",
149
+ "0/7": "海外本地",
150
+ "0/": "应届生", // default
151
+ "3/": "校园全类型",
152
+ };
153
+ function getRecruitLabel(jobType, jobTypes) {
154
+ const key = `${jobType}/${jobTypes ?? ""}`;
155
+ return RECRUIT_LABELS[key] ?? `jobType=${jobType}`;
156
+ }
157
+ function summarizePosition(item, recruitLabel) {
158
+ const id = String(item.jobId ?? "");
159
+ const ds = String(item.dataSource ?? "1");
160
+ return {
161
+ post_id: id,
162
+ title: item.jobname ?? "",
163
+ project: item.jobFamilyName ?? "",
164
+ recruit_label: recruitLabel,
165
+ bgs: "",
166
+ work_cities: item.jobArea ?? item.jobAddress ?? "",
167
+ apply_url: id ? DETAIL_PAGE(id, ds) : CAMPUS_PAGE,
168
+ };
169
+ }
170
+ function resolveJobTypeParams(recruitType) {
171
+ switch (recruitType) {
172
+ case "overseas": return { jobType: "0", jobTypes: "1", label: "留学生" };
173
+ case "intern": return { jobType: "0", jobTypes: "0", label: "实习生" };
174
+ case "phd": return { jobType: "2", jobTypes: undefined, label: "博士生" };
175
+ case "all": return { jobType: "3", jobTypes: undefined, label: "校园全类型" };
176
+ case "newgrad":
177
+ default: return { jobType: "0", jobTypes: "2", label: "应届生" };
178
+ }
179
+ }
180
+ // ---------- searchPositions ----------
181
+ export async function searchPositions(opts = {}) {
182
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 15));
183
+ const page = Math.max(1, opts.page ?? 1);
184
+ const keyword = (opts.keyword ?? "").trim().slice(0, 60);
185
+ const { jobType, jobTypes, label } = resolveJobTypeParams(opts.recruitType);
186
+ const params = {
187
+ jobType,
188
+ language: "zh_CN",
189
+ reqTime: Date.now(),
190
+ orderBy: "ISS_STARTDATE_DESC_AND_IS_HOT_JOB",
191
+ pageSize,
192
+ curPage: page,
193
+ };
194
+ if (jobTypes !== undefined)
195
+ params.jobTypes = jobTypes;
196
+ if (keyword)
197
+ params.searchText = keyword;
198
+ if (opts.jobFamClsCode)
199
+ params.jobFamClsCode = opts.jobFamClsCode;
200
+ if (opts.cityCode)
201
+ params.cityCode = opts.cityCode;
202
+ const url = `${API_ROOT}/getJob/newHr/page/${pageSize}/${page}`;
203
+ const resp = await getJson(url, params);
204
+ if (!resp.ok || !resp.data) {
205
+ return {
206
+ ok: false,
207
+ source: "career.huawei.com",
208
+ message: resp.message,
209
+ query: params,
210
+ page,
211
+ page_size: pageSize,
212
+ total: 0,
213
+ positions: [],
214
+ };
215
+ }
216
+ const pv = resp.data.pageVO ?? {};
217
+ const items = resp.data.result ?? [];
218
+ const recruitLabel = getRecruitLabel(jobType, jobTypes);
219
+ return {
220
+ ok: true,
221
+ source: "career.huawei.com",
222
+ query: params,
223
+ page,
224
+ page_size: pageSize,
225
+ total: pv.totalRows ?? items.length,
226
+ positions: items.map((item) => summarizePosition(item, recruitLabel)),
227
+ _label: label,
228
+ };
229
+ }
230
+ // ---------- fetchAllPositions ----------
231
+ export async function fetchAllPositions(opts = {}) {
232
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 50));
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: "career.huawei.com",
242
+ message: result.message,
243
+ total: total ?? 0,
244
+ fetched: bucket.length,
245
+ positions: bucket,
246
+ };
247
+ }
248
+ if (total === undefined)
249
+ total = result.total;
250
+ if (!result.positions.length)
251
+ break;
252
+ bucket.push(...result.positions);
253
+ if (total !== undefined && bucket.length >= total)
254
+ break;
255
+ }
256
+ return {
257
+ ok: true,
258
+ source: "career.huawei.com",
259
+ total: total ?? bucket.length,
260
+ fetched: bucket.length,
261
+ positions: bucket,
262
+ };
263
+ }
264
+ // ---------- fetchPositionDetail ----------
265
+ // Uses two endpoints:
266
+ // 1. getJobDetail (flat metadata + possibly placeholder JD text)
267
+ // 2. findIntentListByJobRequirementId (full JD per sub-position intent)
268
+ //
269
+ // post_id = item.jobId from searchPositions results.
270
+ // If the caller has item.jobRequirementId it can be passed as opts.jobRequirementId
271
+ // for the intent-list fetch; otherwise it's extracted from the getJobDetail response.
272
+ export async function fetchPositionDetail(postId, opts = {}) {
273
+ const id = (postId ?? "").trim();
274
+ if (!id) {
275
+ return { ok: false, source: "career.huawei.com", message: "post_id is required" };
276
+ }
277
+ const ds = opts.dataSource ?? "1";
278
+ const detailUrl = `${API_ROOT}/getJobDetail/newHr`;
279
+ const detailResp = await getJson(detailUrl, {
280
+ jobId: id,
281
+ dataSource: ds,
282
+ language: "zh_CN",
283
+ });
284
+ if (!detailResp.ok || !detailResp.data) {
285
+ return {
286
+ ok: false,
287
+ source: "career.huawei.com",
288
+ post_id: id,
289
+ message: detailResp.message,
290
+ };
291
+ }
292
+ const raw = detailResp.data;
293
+ // Fetch intent list for the full JD (jobResponsibilities + jobDemand)
294
+ const reqId = opts.jobRequirementId ?? String(raw.jobRequirementId ?? "");
295
+ let intents = [];
296
+ if (reqId) {
297
+ const intentUrl = `${USER_API_ROOT}/findIntentListByJobRequirementId/newHr/zh_CN/${encodeURIComponent(reqId)}/null`;
298
+ const intentResp = await getJson(intentUrl, { dataSource: ds, jobId: id });
299
+ if (intentResp.ok && Array.isArray(intentResp.data)) {
300
+ intents = intentResp.data;
301
+ }
302
+ }
303
+ // Strip HTML tags from JD text
304
+ const stripHtml = (s) => (s ?? "")
305
+ .replace(/<br\s*\/?>/gi, "\n")
306
+ .replace(/<[^>]+>/g, "")
307
+ .replace(/&nbsp;/g, " ")
308
+ .replace(/&amp;/g, "&")
309
+ .replace(/&lt;/g, "<")
310
+ .replace(/&gt;/g, ">")
311
+ .replace(/&quot;/g, '"')
312
+ .trim();
313
+ // Determine description: prefer intent list if main description is a placeholder
314
+ const isPlaceholder = (s) => !s || s.includes("请您详见岗位意向");
315
+ const mainBusiness = raw.mainBusiness ?? "";
316
+ const jobRequire = raw.jobRequire ?? "";
317
+ let description = isPlaceholder(mainBusiness)
318
+ ? intents.map((i) => `【${i.positionIntention ?? ""}】\n${stripHtml(i.jobResponsibilities)}`).join("\n\n")
319
+ : mainBusiness;
320
+ let requirements = isPlaceholder(jobRequire)
321
+ ? intents.map((i) => `【${i.positionIntention ?? ""}】\n${stripHtml(i.jobDemand)}`).join("\n\n")
322
+ : jobRequire;
323
+ description = description.trim();
324
+ requirements = requirements.trim();
325
+ return {
326
+ ok: true,
327
+ source: "career.huawei.com",
328
+ post_id: id,
329
+ job_requirement_id: reqId,
330
+ title: raw.jobname ?? "",
331
+ direction: "",
332
+ project: raw.jobFamilyName ?? "",
333
+ description,
334
+ requirements,
335
+ work_cities: raw.jobArea ?? raw.jobAddress ?? "",
336
+ recruit_cities: [],
337
+ intents: intents.map((i) => ({
338
+ id: i.positionIntentionId ?? "",
339
+ title: i.positionIntention ?? "",
340
+ dept: i.deptName ?? "",
341
+ cities: i.jobPlaceName ?? i.jobPlace ?? "",
342
+ description: stripHtml(i.jobResponsibilities),
343
+ requirements: stripHtml(i.jobDemand),
344
+ })),
345
+ apply_url: DETAIL_PAGE(id, ds),
346
+ };
347
+ }
348
+ // ---------- fetchDictionaries ----------
349
+ // There is no public filter-taxonomy endpoint (list/lang returns 403; findStatAddress
350
+ // returns 404 without a login session). This stub documents the known static
351
+ // values as observed in the JS bundle and probed API responses (2026-05).
352
+ export async function fetchDictionaries() {
353
+ // Probe the portal to confirm it is reachable
354
+ const session = await getSession();
355
+ const reachable = session !== null;
356
+ return {
357
+ ok: true,
358
+ source: "career.huawei.com",
359
+ note: "Huawei: no public filter-taxonomy endpoint. " +
360
+ "/services/portal/portalpub/list/lang/ returns 403 without login; " +
361
+ "findStatAddress/ returns 404. Values below are static from JS bundle (2026-05).",
362
+ reachable,
363
+ // jobTypes semantics from campus-recruitment.html Vue component
364
+ recruit_types: [
365
+ { jobType: "0", jobTypes: "2", label: "应届生 (new-grad)", approx_count: 60 },
366
+ { jobType: "0", jobTypes: "1", label: "留学生 (overseas student)", approx_count: 40 },
367
+ { jobType: "0", jobTypes: "0", label: "实习生 (intern/博士)", approx_count: 30 },
368
+ { jobType: "2", jobTypes: null, label: "博士生 (PhD)", approx_count: 92 },
369
+ { jobType: "3", jobTypes: null, label: "全类型 (all campus)", approx_count: 420 },
370
+ ],
371
+ // Job family class codes observed in search results (2026-05)
372
+ job_family_class_codes: [
373
+ { code: "JFC1", approx: "研发族 (R&D)" },
374
+ { code: "JFC2", approx: "销售族 (Sales) / 其他 (Other)" },
375
+ ],
376
+ // Job family codes observed in results
377
+ job_families: [
378
+ { code: "J01", name: "软件工程族" },
379
+ { code: "J03", name: "销售族" },
380
+ { code: "J26", name: "研发族" },
381
+ ],
382
+ // Key work cities appear in item.jobArea as Chinese strings like "中国/深圳"
383
+ city_note: "Cities are returned in item.jobArea as Chinese strings (e.g. '中国/深圳,中国/上海'). " +
384
+ "Use cityCode param to filter; codes appear in item.cityCode when set.",
385
+ };
386
+ }
387
+ // ---------- stub notices ----------
388
+ // Huawei: no public notice/announcement endpoint found.
389
+ const STUB_NOTICE_RESULT = {
390
+ ok: false,
391
+ source: "career.huawei.com",
392
+ message: "Huawei: no public notices endpoint",
393
+ };
394
+ export async function listNotices() {
395
+ return STUB_NOTICE_RESULT;
396
+ }
397
+ export async function getNotice(_id) {
398
+ return {
399
+ ok: false,
400
+ source: "career.huawei.com",
401
+ message: "Huawei: no public notices endpoint",
402
+ };
403
+ }
404
+ export async function findNoticesByQuestion(_question, _opts = {}) {
405
+ return {
406
+ ok: false,
407
+ source: "career.huawei.com",
408
+ message: "Huawei: no public notices endpoint",
409
+ };
410
+ }
411
+ // ---------- matchResume ----------
412
+ // Mirror tencent/bytedance algorithm:
413
+ // 1. Extract signals from resume text.
414
+ // 2. Search with top-3 terms as keyword (all-types pool for breadth).
415
+ // 3. Score each position against title + project + work_cities blob.
416
+ // 4. Enrich top candidates with full detail + intent JD for deeper scoring.
417
+ // 5. Return top N matches with reasons.
418
+ export async function matchResume(text, opts = {}) {
419
+ const topN = Math.max(1, opts.topN ?? 5);
420
+ const candidates = Math.max(topN, opts.candidates ?? 20);
421
+ const recruitType = opts.recruitType ?? "all";
422
+ const { terms, cities } = extractResumeSignals(text ?? "");
423
+ if (!terms.length) {
424
+ return {
425
+ ok: false,
426
+ source: "career.huawei.com",
427
+ message: "could not extract any technical signals from the text",
428
+ preview: (text ?? "").slice(0, 120),
429
+ };
430
+ }
431
+ const keyword = terms.slice(0, 3).join(" ");
432
+ const list = await searchPositions({ keyword, page: 1, pageSize: 50, recruitType });
433
+ if (!list.ok) {
434
+ return {
435
+ ok: false,
436
+ source: "career.huawei.com",
437
+ message: list.message,
438
+ positions: [],
439
+ };
440
+ }
441
+ // If keyword search returns few results, fall back to no-keyword full list
442
+ const pool = list.positions.length < 5
443
+ ? (await searchPositions({ page: 1, pageSize: 50, recruitType })).positions
444
+ : list.positions;
445
+ const scored = [];
446
+ for (const p of pool) {
447
+ const blob = [p.title, p.project, p.recruit_label, p.work_cities].join(" ");
448
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
449
+ if (score > 0)
450
+ scored.push({ score, position: p, reasons });
451
+ }
452
+ scored.sort((a, b) => b.score - a.score);
453
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
454
+ if (!shortlist.length) {
455
+ shortlist = pool.slice(0, candidates).map((position) => ({
456
+ score: 0,
457
+ position,
458
+ reasons: [],
459
+ }));
460
+ }
461
+ const enriched = [];
462
+ for (const { score: baseScore, position, reasons: baseReasons } of shortlist.slice(0, candidates)) {
463
+ const detail = await fetchPositionDetail(position.post_id);
464
+ let extraScore = 0;
465
+ let extraReasons = [];
466
+ let description;
467
+ let requirements;
468
+ if (detail.ok) {
469
+ description = detail.description;
470
+ requirements = detail.requirements;
471
+ const detailBlob = [
472
+ detail.title,
473
+ detail.project,
474
+ detail.description,
475
+ detail.requirements,
476
+ detail.work_cities,
477
+ ].join(" ");
478
+ const r = scoreOverlap(detailBlob, terms, cities);
479
+ extraScore = r.score;
480
+ extraReasons = r.reasons;
481
+ }
482
+ const combined = [...new Set([...baseReasons, ...extraReasons])].slice(0, 5);
483
+ if (!combined.length) {
484
+ combined.push("no specific keyword overlap — surfaced from initial keyword search");
485
+ }
486
+ enriched.push({
487
+ score: baseScore + extraScore,
488
+ row: {
489
+ ...position,
490
+ description,
491
+ requirements,
492
+ match_reasons: combined,
493
+ },
494
+ });
495
+ }
496
+ enriched.sort((a, b) => b.score - a.score);
497
+ return {
498
+ ok: true,
499
+ source: "career.huawei.com",
500
+ extracted_terms: terms,
501
+ city_preferences: cities,
502
+ matches: enriched.slice(0, topN).map((e) => e.row),
503
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
504
+ "The only authority on selection is HR.",
505
+ };
506
+ }
507
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_huawei } from "./apply.js";
508
+ export async function fetchApplicationSchema(postId) {
509
+ const id = (postId ?? "").trim();
510
+ if (!id)
511
+ return { ok: false, source: "career.huawei.com", message: "post_id is required" };
512
+ let title = "";
513
+ let applyUrl = "https://career.huawei.com";
514
+ try {
515
+ const detail = (await fetchPositionDetail(id));
516
+ if (detail?.ok === false) {
517
+ return { ok: false, source: "career.huawei.com", message: detail.message ?? "post not found" };
518
+ }
519
+ title = detail?.title ?? "";
520
+ if (detail?.apply_url)
521
+ applyUrl = detail.apply_url;
522
+ }
523
+ catch { }
524
+ return {
525
+ ok: true,
526
+ schema: _buildBespokeApplySchema_huawei({
527
+ source: "career.huawei.com",
528
+ postId: id,
529
+ jobTitle: title,
530
+ applyUrl,
531
+ submitEndpoint: "https://career.huawei.com/reccampportal/services/portal/portaluser/applyJob",
532
+ submitKind: "multipart-session",
533
+ endpointVerified: true,
534
+ submitNotes: "Huawei — POST under /reccampportal/services/portal/portaluser/. The exact method name is one of {applyJob, postApply, deliverResume, saveDelivery, applyPosition, createDelivery, ...} — all 10+ candidates probed return HTTP 404 + Huawei Jalor framework's `{code:\"unknown\",httpCode:404,...,问题编码:..Anonymous-..}` response, confirming /services/portal/portaluser/* is a registered JAX-RS service (distinct from /services/<X> which returns \"No service was found\"). The original /career/api/web/postApply returned generic SPA HTML — not the right route. Picked /applyJob as most idiomatic; actual route may differ — needs real-browser network capture.",
535
+ }),
536
+ };
537
+ }