@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,49 @@
1
+ // Thin client for 百川智能 (Baichuan AI) recruiting portal.
2
+ //
3
+ // Portal: https://cq6qe6bvfr6.jobs.feishu.cn/baichuanzhaopin/
4
+ // Platform: Feishu Recruiting (ATSX) SaaS — same API surface as nio.ts / minimax.ts.
5
+ //
6
+ // ============================================================
7
+ // Discovery (2026-05):
8
+ //
9
+ // www.baichuan-ai.com/ → Next.js SPA with no /careers or /jobs route
10
+ // (the corporate site doesn't link to the portal)
11
+ // baichuan.jobs.feishu.cn/ → 400 (no portal configured on the obvious slug)
12
+ // baichuan-ai.jobs.feishu.cn/ → 404
13
+ //
14
+ // The real portal lives on a randomized Feishu tenant slug:
15
+ // cq6qe6bvfr6.jobs.feishu.cn/baichuanzhaopin/
16
+ //
17
+ // Tenant: 百川智能 / "【百川智能】社会招聘官方网站-欢迎你的加入!"
18
+ // Channel slug: "baichuanzhaopin" (the company PATH on the tenant)
19
+ //
20
+ // This is the same multi-tenant ATSX pattern that MiniMax (vrfi1sk8a0)
21
+ // uses — the company path is the portal-channel header.
22
+ //
23
+ // ============================================================
24
+ // PositionSummary field mapping (Feishu → canonical):
25
+ // post_id ← String(item.id)
26
+ // title ← item.title
27
+ // project ← item.job_category?.name ?? item.job_function?.name
28
+ // recruit_label ← item.recruit_type?.name
29
+ // bgs ← "" (not exposed in public search)
30
+ // work_cities ← city_list joined " / " (city_info used as fallback)
31
+ // apply_url ← https://cq6qe6bvfr6.jobs.feishu.cn/baichuanzhaopin/position/${id}/detail
32
+ import { createAdapter } from "./feishu.js";
33
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
34
+ export { extractResumeSignals, scoreOverlap, checkResume };
35
+ const _adapter = createAdapter({
36
+ host: "cq6qe6bvfr6.jobs.feishu.cn",
37
+ channel: "baichuanzhaopin",
38
+ label: "Baichuan (百川智能)",
39
+ applyUrlPrefix: "https://cq6qe6bvfr6.jobs.feishu.cn/baichuanzhaopin/position",
40
+ });
41
+ export const searchPositions = _adapter.searchPositions;
42
+ export const fetchAllPositions = _adapter.fetchAllPositions;
43
+ export const fetchPositionDetail = _adapter.fetchPositionDetail;
44
+ export const fetchDictionaries = _adapter.fetchDictionaries;
45
+ export const listNotices = _adapter.listNotices;
46
+ export const getNotice = _adapter.getNotice;
47
+ export const findNoticesByQuestion = _adapter.findNoticesByQuestion;
48
+ export const matchResume = _adapter.matchResume;
49
+ export const fetchApplicationSchema = _adapter.fetchApplicationSchema;
package/dist/baidu.js ADDED
@@ -0,0 +1,452 @@
1
+ // Thin client for Baidu's public campus-recruiting API at talent.baidu.com/jobs.
2
+ //
3
+ // ============================================================
4
+ // Discovery notes (probed 2026-05 — webpack bundle analysis):
5
+ //
6
+ // Portal URL: https://talent.baidu.com/jobs/list?recruitType=GRADUATE
7
+ // Old portal: https://talent.baidu.com/external/baidu/index.html (redirects)
8
+ // JS entry: talent-offical-static-prod.cdn.bcebos.com/hcm-recruitment/...
9
+ // Relevant chunks: 3085675597715898.0406a4cc.chunk.js (module 1093 = fetch layer)
10
+ // detail-fetch.9c2c2d3c.chunk.js (list + detail routing)
11
+ //
12
+ // ============================================================
13
+ // Endpoint inventory:
14
+ //
15
+ // POST https://talent.baidu.com/httservice/getPostListNew
16
+ // Content-Type: application/x-www-form-urlencoded ← CRITICAL: JSON body returns 400
17
+ // Params: recruitType, keyWord, pageNum, pageSize,
18
+ // workPlace (repeatable), postType (repeatable), projectType
19
+ // Response: { status:"ok", data:{ total:"<int>", pages:<int>, pageNum:<int>,
20
+ // pageSize:<int>, list:[...], hasNextPage:<bool> } }
21
+ // Note: total is a STRING in the response ("100", "416", …)
22
+ //
23
+ // GET https://talent.baidu.com/httservice/getSearchCompDicInfo?recruitType=GRADUATE
24
+ // Returns { status:"ok", data:{ postType:[{code,name,order}],
25
+ // workPlace:[{code,name,order}],
26
+ // internProjectType:[{code,name}], graduateProjectType:[{code,name}] } }
27
+ //
28
+ // GET https://talent.baidu.com/httservice/getPostDetail?postId=<uuid>&recruitType=<type>
29
+ // Returns { status:"ok", data:{ postId, name, postType, workPlace, projectType,
30
+ // serviceCondition (requirements), workContent (description), … } }
31
+ //
32
+ // ============================================================
33
+ // Filter taxonomy (from GET /httservice/getSearchCompDicInfo, probed 2026-05):
34
+ //
35
+ // DIMENSION 1 — postType (职位类别)
36
+ // "1" = 技术 "2" = 产品 "13" = 政企
37
+ // "14" = 销售 "15" = 综合
38
+ //
39
+ // DIMENSION 2 — workPlace (工作地点, city codes)
40
+ // "" = 不限 "1100" = 北京市 "3100" = 上海市 "4403" = 深圳市
41
+ // "4401"= 广州市 "5101" = 成都市 "2102" = 大连市 "1403" = 阳泉市
42
+ // "4201"= 武汉市 "3301" = 杭州市 "3501" = 福州市 "4419" = 东莞市
43
+ // "4601"= 海口市 "3701" = 济南市 "9000" = 全国
44
+ //
45
+ // DIMENSION 3 — recruitType (招聘类型)
46
+ // "GRADUATE" = 校园招聘 (new-grad, default) ~100 positions
47
+ // "INTERN" = 实习生招聘 ~778 positions (split by projectType)
48
+ // "SOCIAL" = 社招 (separate portal, not returned here)
49
+ //
50
+ // DIMENSION 4 — projectType (项目类型, varies by recruitType)
51
+ // For GRADUATE: "" = all (~100), "1" = 校招 (~89), "3" = AIDU项目, "4" = 管培生项目
52
+ // For INTERN: "" = 9 (social/misc), "-1" = 日常实习项目 (~416), "9" = 暑期实习项目 (~362)
53
+ //
54
+ // ============================================================
55
+ // Pagination gotcha:
56
+ // - total is returned as a STRING ("100"), not a number.
57
+ // - GRADUATE without filters: server caps total at 100 (UI shows 100 positions).
58
+ // - INTERN total depends on projectType: must set projectType to get realistic counts.
59
+ // - Server does NOT support offset-based pagination above the total cap;
60
+ // requesting pageNum > ceil(total/pageSize) silently resets to page 1.
61
+ //
62
+ // ============================================================
63
+ // ---- PositionSummary field mapping (Baidu → canonical) ----
64
+ // post_id ← item.postId (UUID string, e.g. "ab5ec82f-…")
65
+ // title ← item.name (e.g. "2027AIDU-大模型算法工程师(J99938)")
66
+ // project ← item.projectType (e.g. "AIDU项目", "校招", "日常实习项目")
67
+ // recruit_label ← item.postType (职位类别: "技术", "产品", "综合", …)
68
+ // bgs ← "" (Baidu does not expose BG/事业群 in public search)
69
+ // work_cities ← item.workPlace (comma-joined string "北京市,深圳市")
70
+ // apply_url ← https://talent.baidu.com/jobs/detail/<recruitType>/<postId>
71
+ //
72
+ // ============================================================
73
+ // Endpoints confirmed NOT to exist publicly:
74
+ // POST /httservice/getPostListNew with JSON body → 400 "Illegal argument : recruitType"
75
+ // /jobs/api/* → 404
76
+ // /httservice/notice* → (no public notice endpoint found)
77
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
78
+ export { checkResume };
79
+ const API_ROOT = "https://talent.baidu.com";
80
+ const LIST_PAGE = "https://talent.baidu.com/jobs/list";
81
+ const DETAIL_PAGE = (recruitType, postId) => `${API_ROOT}/jobs/detail/${encodeURIComponent(recruitType)}/${encodeURIComponent(postId)}`;
82
+ const DEFAULT_HEADERS = {
83
+ "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",
84
+ Accept: "application/json, text/plain, */*",
85
+ };
86
+ /** Build an application/x-www-form-urlencoded body string.
87
+ * The POST endpoint REQUIRES this content type — JSON bodies return 400.
88
+ * Multi-value keys (workPlace, postType) are handled via repeated keys. */
89
+ function buildForm(params) {
90
+ const parts = [];
91
+ for (const [key, val] of Object.entries(params)) {
92
+ const values = Array.isArray(val) ? val : [val];
93
+ for (const v of values) {
94
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
95
+ }
96
+ }
97
+ return parts.join("&");
98
+ }
99
+ async function postForm(path, params, referer) {
100
+ const url = `${API_ROOT}${path}`;
101
+ let response;
102
+ try {
103
+ response = await fetch(url, {
104
+ method: "POST",
105
+ headers: {
106
+ ...DEFAULT_HEADERS,
107
+ "Content-Type": "application/x-www-form-urlencoded",
108
+ Referer: referer,
109
+ },
110
+ body: buildForm(params),
111
+ });
112
+ }
113
+ catch (err) {
114
+ return {
115
+ ok: false,
116
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
117
+ };
118
+ }
119
+ if (!response.ok) {
120
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
121
+ }
122
+ let payload;
123
+ try {
124
+ payload = (await response.json());
125
+ }
126
+ catch (err) {
127
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : String(err)}` };
128
+ }
129
+ return {
130
+ ok: payload.status === "ok",
131
+ data: payload.data,
132
+ message: payload.message || (payload.status === "ok" ? "ok" : "upstream error"),
133
+ };
134
+ }
135
+ async function getJson(path, params, referer) {
136
+ const qs = new URLSearchParams(params).toString();
137
+ const url = `${API_ROOT}${path}${qs ? `?${qs}` : ""}`;
138
+ let response;
139
+ try {
140
+ response = await fetch(url, {
141
+ headers: { ...DEFAULT_HEADERS, Referer: referer },
142
+ });
143
+ }
144
+ catch (err) {
145
+ return {
146
+ ok: false,
147
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
148
+ };
149
+ }
150
+ if (!response.ok) {
151
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
152
+ }
153
+ let payload;
154
+ try {
155
+ payload = (await response.json());
156
+ }
157
+ catch (err) {
158
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : String(err)}` };
159
+ }
160
+ return {
161
+ ok: payload.status === "ok",
162
+ data: payload.data,
163
+ message: payload.message || (payload.status === "ok" ? "ok" : "upstream error"),
164
+ };
165
+ }
166
+ function summarizePosition(item, recruitType) {
167
+ const postId = item.postId ?? "";
168
+ return {
169
+ post_id: postId,
170
+ title: item.name ?? "",
171
+ project: item.projectType ?? "",
172
+ recruit_label: item.postType ?? "",
173
+ bgs: "",
174
+ work_cities: (item.workPlace ?? "").trim(),
175
+ apply_url: postId ? DETAIL_PAGE(recruitType, postId) : LIST_PAGE,
176
+ };
177
+ }
178
+ // ---------- searchPositions ----------
179
+ export async function searchPositions(opts = {}) {
180
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 10));
181
+ const page = Math.max(1, opts.page ?? 1);
182
+ const keyword = (opts.keyword ?? "").trim().slice(0, 60);
183
+ const recruitType = opts.recruitType ?? "GRADUATE";
184
+ const params = {
185
+ recruitType,
186
+ keyWord: keyword,
187
+ pageNum: String(page),
188
+ pageSize: String(pageSize),
189
+ };
190
+ if (opts.projectType !== undefined) {
191
+ params.projectType = opts.projectType;
192
+ }
193
+ const postTypes = opts.postTypes ?? [];
194
+ if (postTypes.length) {
195
+ params.postType = postTypes;
196
+ }
197
+ const workPlaces = opts.workPlaces ?? [];
198
+ if (workPlaces.length) {
199
+ params.workPlace = workPlaces;
200
+ }
201
+ const referer = `${LIST_PAGE}?recruitType=${recruitType}`;
202
+ const response = await postForm("/httservice/getPostListNew", params, referer);
203
+ if (!response.ok || !response.data) {
204
+ return {
205
+ ok: false,
206
+ message: response.message,
207
+ source: "talent.baidu.com",
208
+ query: params,
209
+ positions: [],
210
+ };
211
+ }
212
+ const rows = response.data.list ?? [];
213
+ const total = Number(response.data.total ?? rows.length);
214
+ return {
215
+ ok: true,
216
+ source: "talent.baidu.com",
217
+ query: params,
218
+ page,
219
+ page_size: pageSize,
220
+ total,
221
+ positions: rows.map((r) => summarizePosition(r, recruitType)),
222
+ };
223
+ }
224
+ // ---------- fetchAllPositions ----------
225
+ export async function fetchAllPositions(opts = {}) {
226
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
227
+ const maxPages = Math.max(1, opts.maxPages ?? 5);
228
+ const bucket = [];
229
+ let total;
230
+ for (let page = 1; page <= maxPages; page++) {
231
+ const result = await searchPositions({ ...opts, page, pageSize });
232
+ if (!result.ok) {
233
+ return {
234
+ ok: false,
235
+ message: result.message,
236
+ source: "talent.baidu.com",
237
+ fetched: bucket.length,
238
+ positions: bucket,
239
+ };
240
+ }
241
+ if (total === undefined)
242
+ total = result.total;
243
+ if (!result.positions.length)
244
+ break;
245
+ bucket.push(...result.positions);
246
+ if (total !== undefined && bucket.length >= total)
247
+ break;
248
+ }
249
+ return {
250
+ ok: true,
251
+ source: "talent.baidu.com",
252
+ total: total ?? bucket.length,
253
+ fetched: bucket.length,
254
+ positions: bucket,
255
+ };
256
+ }
257
+ // ---------- fetchPositionDetail ----------
258
+ export async function fetchPositionDetail(postId, recruitType = "GRADUATE") {
259
+ const id = (postId ?? "").trim();
260
+ if (!id) {
261
+ return { ok: false, source: "talent.baidu.com", message: "post_id is required" };
262
+ }
263
+ const rt = (recruitType ?? "GRADUATE").trim() || "GRADUATE";
264
+ const referer = DETAIL_PAGE(rt, id);
265
+ const response = await getJson("/httservice/getPostDetail", { postId: id, recruitType: rt }, referer);
266
+ if (!response.ok || !response.data) {
267
+ return {
268
+ ok: false,
269
+ source: "talent.baidu.com",
270
+ post_id: id,
271
+ message: response.message || "no detail returned",
272
+ };
273
+ }
274
+ const raw = response.data;
275
+ const summary = summarizePosition(raw, rt);
276
+ return {
277
+ ok: true,
278
+ source: "talent.baidu.com",
279
+ post_id: raw.postId ?? id,
280
+ title: raw.name ?? "",
281
+ direction: "",
282
+ project: raw.projectType ?? "",
283
+ recruit_label: raw.postType ?? "",
284
+ description: raw.workContent ?? "",
285
+ requirements: raw.serviceCondition ?? "",
286
+ work_cities: (raw.workPlace ?? "").trim(),
287
+ publish_date: raw.publishDate ?? "",
288
+ interview_date: raw.interviewDate ?? "",
289
+ exam_date: raw.writeExaminationDate ?? "",
290
+ apply_url: summary.apply_url,
291
+ };
292
+ }
293
+ // ---------- fetchDictionaries ----------
294
+ export async function fetchDictionaries() {
295
+ // Fetch for both recruit types in parallel; GRADUATE has the full filter set.
296
+ const [gradDic, internDic] = await Promise.all([
297
+ getJson("/httservice/getSearchCompDicInfo", { recruitType: "GRADUATE" }, `${LIST_PAGE}?recruitType=GRADUATE`),
298
+ getJson("/httservice/getSearchCompDicInfo", { recruitType: "INTERN" }, `${LIST_PAGE}?recruitType=INTERN`),
299
+ ]);
300
+ if (!gradDic.ok || !gradDic.data) {
301
+ return {
302
+ ok: false,
303
+ source: "talent.baidu.com",
304
+ message: gradDic.message,
305
+ };
306
+ }
307
+ const d = gradDic.data;
308
+ return {
309
+ ok: true,
310
+ source: "talent.baidu.com",
311
+ verified_at: new Date().toISOString(),
312
+ /** 职位类别 (job category). Use codes in SearchOptions.postTypes[]. */
313
+ postTypes: (d.postType ?? []).map((t) => ({
314
+ code: t.code,
315
+ name: t.name,
316
+ })),
317
+ /** 工作地点 (city codes). Use codes in SearchOptions.workPlaces[]. */
318
+ workPlaces: (d.workPlace ?? []).map((c) => ({
319
+ code: c.code,
320
+ name: c.name,
321
+ })),
322
+ /** 校园招聘 project types. Use code in SearchOptions.projectType. */
323
+ graduateProjectTypes: (d.graduateProjectType ?? []).map((p) => ({
324
+ code: p.code,
325
+ name: p.name,
326
+ })),
327
+ /** 实习生 project types (from INTERN-scoped call). Use code in SearchOptions.projectType. */
328
+ internProjectTypes: (internDic.ok ? (internDic.data?.internProjectType ?? []) : []).map((p) => ({
329
+ code: p.code,
330
+ name: p.name,
331
+ })),
332
+ recruitTypes: [
333
+ { code: "GRADUATE", name: "校园招聘", note: "new-grad campus hire (~100 positions shown)" },
334
+ { code: "INTERN", name: "实习生招聘", note: "intern (~416 日常 + ~362 暑期)" },
335
+ ],
336
+ };
337
+ }
338
+ // ---------- stub notices ----------
339
+ // talent.baidu.com has a 招聘动态 (news/trend) section but no public notice-list
340
+ // JSON endpoint was found — it is rendered server-side via httservice/config/article
341
+ // which returns HTML articles, not a structured notice API.
342
+ const STUB_SOURCE = "talent.baidu.com";
343
+ const STUB_MSG = "Baidu: no public structured notices endpoint (招聘动态 is HTML-only)";
344
+ export async function listNotices() {
345
+ return { ok: false, source: STUB_SOURCE, message: STUB_MSG };
346
+ }
347
+ export async function getNotice(_id) {
348
+ return { ok: false, source: STUB_SOURCE, message: STUB_MSG };
349
+ }
350
+ export async function findNoticesByQuestion(_question, _opts = {}) {
351
+ return { ok: false, source: STUB_SOURCE, message: STUB_MSG };
352
+ }
353
+ // ---------- matchResume ----------
354
+ export async function matchResume(text, opts = {}) {
355
+ const topN = Math.max(1, opts.topN ?? 5);
356
+ const candidates = Math.max(topN, opts.candidates ?? 20);
357
+ const recruitType = opts.recruitType ?? "GRADUATE";
358
+ const { terms, cities } = extractResumeSignals(text ?? "");
359
+ if (!terms.length) {
360
+ return {
361
+ ok: false,
362
+ source: STUB_SOURCE,
363
+ message: "could not extract any technical signals from the text",
364
+ preview: (text ?? "").slice(0, 120),
365
+ };
366
+ }
367
+ const keyword = terms.slice(0, 3).join(" ");
368
+ const list = await searchPositions({ keyword, pageSize: 100, recruitType });
369
+ if (!list.ok) {
370
+ return { ok: false, source: STUB_SOURCE, message: list.message, positions: [] };
371
+ }
372
+ // Also fetch without keyword to broaden the candidate pool if keyword returns few results
373
+ let allPositions = list.positions;
374
+ if (allPositions.length < candidates) {
375
+ const broad = await searchPositions({ pageSize: 100, recruitType });
376
+ if (broad.ok) {
377
+ const seen = new Set(allPositions.map((p) => p.post_id));
378
+ for (const p of broad.positions) {
379
+ if (!seen.has(p.post_id)) {
380
+ allPositions.push(p);
381
+ seen.add(p.post_id);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ const scored = [];
387
+ for (const p of allPositions) {
388
+ // The list response already includes workContent and serviceCondition inline —
389
+ // no extra detail fetch needed (unlike ByteDance/Tencent which omit description).
390
+ // We access them via the raw list response; for matchResume we use the summary fields.
391
+ const blob = [p.title, p.project, p.recruit_label, p.work_cities].join(" ");
392
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
393
+ if (score > 0) {
394
+ scored.push({ score, position: p, reasons });
395
+ }
396
+ }
397
+ scored.sort((a, b) => b.score - a.score);
398
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
399
+ if (!shortlist.length) {
400
+ shortlist = allPositions.slice(0, candidates).map((position) => ({
401
+ score: 0,
402
+ position,
403
+ reasons: [],
404
+ }));
405
+ }
406
+ const matches = shortlist.slice(0, topN).map((s) => {
407
+ const mr = s.reasons.length > 0
408
+ ? s.reasons.slice(0, 5)
409
+ : ["no specific keyword overlap — surfaced from initial keyword search"];
410
+ return { ...s.position, match_reasons: mr };
411
+ });
412
+ return {
413
+ ok: true,
414
+ source: STUB_SOURCE,
415
+ extracted_terms: terms,
416
+ city_preferences: cities,
417
+ matches,
418
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
419
+ "The only authority on selection is HR.",
420
+ };
421
+ }
422
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_baidu } from "./apply.js";
423
+ export async function fetchApplicationSchema(postId) {
424
+ const id = (postId ?? "").trim();
425
+ if (!id)
426
+ return { ok: false, source: "talent.baidu.com", message: "post_id is required" };
427
+ let title = "";
428
+ let applyUrl = "https://talent.baidu.com";
429
+ try {
430
+ const detail = (await fetchPositionDetail(id));
431
+ if (detail?.ok === false) {
432
+ return { ok: false, source: "talent.baidu.com", message: detail.message ?? "post not found" };
433
+ }
434
+ title = detail?.title ?? "";
435
+ if (detail?.apply_url)
436
+ applyUrl = detail.apply_url;
437
+ }
438
+ catch { }
439
+ return {
440
+ ok: true,
441
+ schema: _buildBespokeApplySchema_baidu({
442
+ source: "talent.baidu.com",
443
+ postId: id,
444
+ jobTitle: title,
445
+ applyUrl,
446
+ submitEndpoint: "https://talent.baidu.com/applyJob.json",
447
+ submitKind: "multipart-session",
448
+ endpointVerified: true,
449
+ submitNotes: "Baidu — POST /applyJob.json (host root) with session cookie. Endpoint anon-probed → HTTP 200 + {status:\"need-login\",message:\"need login!\"} (real auth-middleware gateway; the original /external/baidu/ prefix returns 404 HTML but the gateway lives at host root). Body shape still needs validation.",
450
+ }),
451
+ };
452
+ }