@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/byd.js ADDED
@@ -0,0 +1,412 @@
1
+ // BYD (比亚迪) recruiting adapter — job.byd.com.
2
+ //
3
+ // ============================================================
4
+ // API DISCOVERY (probed 2026-05-15)
5
+ //
6
+ // The job.byd.com SPA exposes two distinct API namespaces:
7
+ //
8
+ // /portal/api/... → authenticated; every endpoint returns
9
+ // {"code":4001,"msg":"Token无效或已过期"}
10
+ // for unauthenticated requests.
11
+ // /portal/api/portal-api/... → ANONYMOUS public endpoints used by the SPA's
12
+ // home/experienced/campus landing flows. These
13
+ // return job listings, notices, materials, and
14
+ // recruit topics without any token.
15
+ //
16
+ // The working anonymous search endpoint is:
17
+ //
18
+ // POST /portal/api/portal-api/position/queryList
19
+ //
20
+ // Required headers: a normal Chrome User-Agent, Content-Type application/json,
21
+ // a job.byd.com Referer, and `lang: en_US` (vivo accepts both en_US and zh_CN).
22
+ //
23
+ // Body shape:
24
+ // {
25
+ // positionTypeArr: [], // 职位类型 codes
26
+ // positionProvinceArr: [], // 省 codes
27
+ // positionCityArr: [], // 市 codes
28
+ // positionOrgArr: [], // 事业群 codes
29
+ // vagueCondition: "", // free-text keyword (matches title)
30
+ // searchType: 1, // 1 = title search
31
+ // zpType: "00251", // 招聘类型 — see table below
32
+ // pageNum: 0, // zero-based
33
+ // pageSize: 20
34
+ // }
35
+ //
36
+ // `zpType` controls the recruit channel:
37
+ // "00251" 社招 (Experienced; 1647+ live postings)
38
+ // "00252" 技师 (Technician — empty as of probe)
39
+ // "00253" 操作工 (Operator / blue-collar — empty as of probe)
40
+ // (Campus 校招 listings live behind a separate `school/*` flow that is fully
41
+ // auth-gated; the public anon channel exposes social hire only.)
42
+ //
43
+ // Response envelope: {"code":0, "data":{"total":N, "data":[...]}}.
44
+ //
45
+ // Endpoint inventory (anonymous):
46
+ // POST /portal/api/portal-api/position/queryList → paginated jobs
47
+ // GET /portal/api/portal-api/material/getMaterial?ids=… → site materials
48
+ // POST /portal/api/portal-api/other-info/notice/query-list → notices
49
+ // POST /portal/api/portal-api/other-info/resource/query-list→ downloadables
50
+ // GET /portal/api/portal-api/common/queryCodeTree?ids=… → filter taxonomy
51
+ // POST /portal/api/portal-api/common/queryDeptTree → org tree
52
+ // POST /portal/api/portal-api/Recruitment/getMessageList → marketing msgs
53
+ // GET /portal/api/portal-api/resumeSend/school-topic/info?zpNature=…
54
+ // → campus topics
55
+ //
56
+ // ============================================================
57
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
58
+ export { checkResume };
59
+ const SOURCE = "job.byd.com";
60
+ const API_ROOT = "https://job.byd.com";
61
+ const SITE_ROOT = "https://job.byd.com/portal/pc/";
62
+ const DETAIL_PAGE = (id) => `https://job.byd.com/portal/pc/#/social/detail?positionCode=${encodeURIComponent(id)}`;
63
+ const DEFAULT_HEADERS = {
64
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
65
+ Accept: "application/json, text/plain, */*",
66
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
67
+ Referer: SITE_ROOT,
68
+ Origin: API_ROOT,
69
+ lang: "zh_CN",
70
+ };
71
+ async function call(method, path, opts = {}) {
72
+ let url = `${API_ROOT}${path}`;
73
+ if (opts.query) {
74
+ const params = new URLSearchParams();
75
+ for (const [k, v] of Object.entries(opts.query)) {
76
+ if (v !== undefined && v !== "")
77
+ params.set(k, String(v));
78
+ }
79
+ const qs = params.toString();
80
+ if (qs)
81
+ url += (path.includes("?") ? "&" : "?") + qs;
82
+ }
83
+ const headers = { ...DEFAULT_HEADERS };
84
+ let body;
85
+ if (opts.body !== undefined) {
86
+ body = JSON.stringify(opts.body);
87
+ headers["Content-Type"] = "application/json;charset=UTF-8";
88
+ }
89
+ let response;
90
+ try {
91
+ response = await fetch(url, { method, headers, body });
92
+ }
93
+ catch (err) {
94
+ return {
95
+ ok: false,
96
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
97
+ };
98
+ }
99
+ if (!response.ok) {
100
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
101
+ }
102
+ let payload;
103
+ try {
104
+ payload = (await response.json());
105
+ }
106
+ catch (err) {
107
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
108
+ }
109
+ return {
110
+ ok: payload.code === 0,
111
+ data: payload.data,
112
+ message: payload.msg || payload.message || (payload.code === 0 ? "ok" : "upstream error"),
113
+ };
114
+ }
115
+ function summarize(item) {
116
+ const id = String(item.positionCode ?? item.id ?? "");
117
+ const city = [item.province, item.city].filter(Boolean).join("·");
118
+ return {
119
+ post_id: id,
120
+ title: (item.positionName ?? "").trim(),
121
+ project: (item.fatherOrgAliasName ?? item.fatherOrgName ?? "").trim(),
122
+ recruit_label: "社招",
123
+ bgs: (item.orgAliasName ?? item.orgName ?? "").trim(),
124
+ work_cities: city,
125
+ apply_url: id ? DETAIL_PAGE(id) : SITE_ROOT,
126
+ };
127
+ }
128
+ // ---------- searchPositions ----------
129
+ export async function searchPositions(opts = {}) {
130
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 20));
131
+ const page = Math.max(1, opts.page ?? 1);
132
+ const body = {
133
+ positionTypeArr: opts.positionTypeIds ?? [],
134
+ positionProvinceArr: opts.provinceCodes ?? [],
135
+ positionCityArr: opts.cityCodes ?? [],
136
+ positionOrgArr: opts.orgCodes ?? [],
137
+ vagueCondition: (opts.keyword ?? "").trim().slice(0, 60),
138
+ searchType: 1,
139
+ zpType: opts.zpType ?? "00251",
140
+ pageNum: page - 1, // BYD uses 0-based
141
+ pageSize,
142
+ };
143
+ const r = await call("POST", "/portal/api/portal-api/position/queryList", { body });
144
+ if (!r.ok || !r.data) {
145
+ return {
146
+ ok: false,
147
+ source: SOURCE,
148
+ message: r.message,
149
+ query: body,
150
+ positions: [],
151
+ };
152
+ }
153
+ const rows = r.data.data ?? [];
154
+ return {
155
+ ok: true,
156
+ source: SOURCE,
157
+ query: body,
158
+ page,
159
+ page_size: pageSize,
160
+ total: r.data.total ?? rows.length,
161
+ positions: rows.map(summarize),
162
+ };
163
+ }
164
+ // ---------- fetchAllPositions ----------
165
+ export async function fetchAllPositions(opts = {}) {
166
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 50));
167
+ const maxPages = Math.max(1, opts.maxPages ?? 40);
168
+ const bucket = [];
169
+ let total;
170
+ for (let page = 1; page <= maxPages; page++) {
171
+ const r = await searchPositions({
172
+ keyword: opts.keyword,
173
+ page,
174
+ pageSize,
175
+ zpType: opts.zpType,
176
+ });
177
+ if (!r.ok) {
178
+ return {
179
+ ok: false,
180
+ source: SOURCE,
181
+ message: r.message,
182
+ total: 0,
183
+ fetched: bucket.length,
184
+ positions: bucket,
185
+ };
186
+ }
187
+ if (total === undefined)
188
+ total = r.total;
189
+ if (!r.positions.length)
190
+ break;
191
+ bucket.push(...r.positions);
192
+ if (total !== undefined && bucket.length >= total)
193
+ break;
194
+ }
195
+ return {
196
+ ok: true,
197
+ source: SOURCE,
198
+ total: total ?? bucket.length,
199
+ fetched: bucket.length,
200
+ positions: bucket,
201
+ };
202
+ }
203
+ // ---------- fetchPositionDetail ----------
204
+ //
205
+ // The detail endpoint /portal/api/position/queryDetail requires auth, but the
206
+ // public list endpoint returns enough info per row that we surface a "row+link"
207
+ // detail instead of a fully gated 4001 stub.
208
+ export async function fetchPositionDetail(postId) {
209
+ const id = (postId ?? "").trim();
210
+ if (!id)
211
+ return { ok: false, source: SOURCE, message: "post_id is required", post_id: id };
212
+ // Page through the social-hire list looking for the row. This is the best we
213
+ // can do without a logged-in JWT; in practice the row is usually within the
214
+ // first few hundred records and matchResume already pages through the full
215
+ // catalogue.
216
+ const r = await searchPositions({ keyword: id, pageSize: 5 });
217
+ const hit = r.ok ? r.positions.find((p) => p.post_id === id) : undefined;
218
+ if (!hit) {
219
+ return {
220
+ ok: false,
221
+ source: SOURCE,
222
+ message: "Position detail endpoint (POST /portal/api/position/queryDetail) requires a logged-in JWT. " +
223
+ "Public anon API can list positions but not return per-position bodies.",
224
+ post_id: id,
225
+ apply_url: DETAIL_PAGE(id),
226
+ };
227
+ }
228
+ return {
229
+ ok: true,
230
+ source: SOURCE,
231
+ post_id: hit.post_id,
232
+ title: hit.title,
233
+ project: hit.project,
234
+ bgs: hit.bgs,
235
+ recruit_label: hit.recruit_label,
236
+ work_cities: hit.work_cities,
237
+ description: "",
238
+ requirements: "",
239
+ apply_url: hit.apply_url,
240
+ note: "Description and requirements are not available without authentication; " +
241
+ "visit apply_url for the full posting after login.",
242
+ };
243
+ }
244
+ // ---------- fetchDictionaries ----------
245
+ export async function fetchDictionaries() {
246
+ const [codeTree, deptTree] = await Promise.all([
247
+ call("GET", "/portal/api/portal-api/common/queryCodeTree", {
248
+ query: { ids: "0009,0030" },
249
+ }),
250
+ call("POST", "/portal/api/portal-api/common/queryDeptTree", { body: {} }),
251
+ ]);
252
+ return {
253
+ ok: codeTree.ok || deptTree.ok,
254
+ source: SOURCE,
255
+ api_host: API_ROOT,
256
+ verified_at: new Date().toISOString(),
257
+ code_tree: codeTree.data ?? null,
258
+ dept_tree: deptTree.data ?? null,
259
+ zp_types: {
260
+ "00251": "社招 (Experienced)",
261
+ "00252": "技师 (Technician)",
262
+ "00253": "操作工 (Operator)",
263
+ },
264
+ note: "Campus (校招) jobs are not exposed by the anon public API — the school/* " +
265
+ "endpoints all require a JWT bearer token.",
266
+ };
267
+ }
268
+ export async function listNotices() {
269
+ const r = await call("POST", "/portal/api/portal-api/other-info/notice/query-list", { body: { pageNum: 0, pageSize: 30 } });
270
+ if (!r.ok)
271
+ return { ok: false, source: SOURCE, message: r.message, notices: [] };
272
+ const items = r.data?.data ?? r.data?.list ?? [];
273
+ return {
274
+ ok: true,
275
+ source: SOURCE,
276
+ count: items.length,
277
+ notices: items.map((n) => ({
278
+ id: String(n.id ?? ""),
279
+ title: n.title ?? n.noticeTitle ?? "",
280
+ publish_time: n.publishTime ?? n.createTime ?? "",
281
+ tag: n.noticeType ?? "",
282
+ detail_url: SITE_ROOT,
283
+ })),
284
+ };
285
+ }
286
+ export async function getNotice(noticeId) {
287
+ const id = (noticeId ?? "").trim();
288
+ if (!id)
289
+ return { ok: false, source: SOURCE, message: "notice_id is required" };
290
+ const all = await listNotices();
291
+ if (!all.ok)
292
+ return { ok: false, source: SOURCE, message: all.message };
293
+ const hit = all.notices.find((n) => n.id === id);
294
+ if (!hit)
295
+ return {
296
+ ok: false,
297
+ source: SOURCE,
298
+ message: `notice ${id} not in the latest /notice/query-list page`,
299
+ };
300
+ return { ok: true, source: SOURCE, ...hit, content_html: "" };
301
+ }
302
+ export async function findNoticesByQuestion(question, opts = {}) {
303
+ const listing = await listNotices();
304
+ if (!listing.ok)
305
+ return { ok: false, source: SOURCE, message: listing.message, matches: [] };
306
+ const tokens = [];
307
+ const seen = new Set();
308
+ const text = (question ?? "").trim();
309
+ for (const m of text.match(/[A-Za-z0-9]{2,}/g) ?? []) {
310
+ const k = m.toLowerCase();
311
+ if (!seen.has(k)) {
312
+ seen.add(k);
313
+ tokens.push(k);
314
+ }
315
+ }
316
+ for (const run of text.match(/[一-鿿]+/g) ?? []) {
317
+ for (let i = 0; i < run.length - 1; i++) {
318
+ const bigram = run.slice(i, i + 2);
319
+ if (!seen.has(bigram)) {
320
+ seen.add(bigram);
321
+ tokens.push(bigram);
322
+ }
323
+ if (tokens.length >= 40)
324
+ break;
325
+ }
326
+ }
327
+ const topK = Math.max(1, opts.topK ?? 3);
328
+ const scored = listing.notices
329
+ .map((n) => {
330
+ const hay = `${n.title} ${n.tag}`.toLowerCase();
331
+ const score = tokens.filter((t) => hay.includes(t)).length;
332
+ return { score, notice: n };
333
+ })
334
+ .filter((s) => s.score > 0)
335
+ .sort((a, b) => b.score - a.score);
336
+ return {
337
+ ok: true,
338
+ source: SOURCE,
339
+ question,
340
+ question_time: opts.questionTime,
341
+ matched_tokens: tokens,
342
+ matches: scored.slice(0, topK).map((s) => ({ ...s.notice, excerpt: "" })),
343
+ };
344
+ }
345
+ // ---------- matchResume ----------
346
+ export async function matchResume(text, opts = {}) {
347
+ const { terms, cities } = extractResumeSignals(text ?? "");
348
+ const topN = Math.max(1, opts.topN ?? 5);
349
+ const candidates = Math.max(topN, opts.candidates ?? 200);
350
+ const all = await fetchAllPositions({
351
+ pageSize: 50,
352
+ maxPages: Math.ceil(candidates / 50),
353
+ });
354
+ if (!all.ok) {
355
+ return {
356
+ ok: false,
357
+ source: SOURCE,
358
+ message: all.message,
359
+ extracted_terms: terms,
360
+ city_preferences: cities,
361
+ matches: [],
362
+ };
363
+ }
364
+ const scored = [];
365
+ for (const p of all.positions) {
366
+ const haystack = `${p.title} ${p.project} ${p.bgs} ${p.work_cities}`;
367
+ const score = scoreOverlap(haystack, terms, cities).score;
368
+ if (score > 0)
369
+ scored.push({ score, position: p });
370
+ }
371
+ scored.sort((a, b) => b.score - a.score);
372
+ return {
373
+ ok: true,
374
+ source: SOURCE,
375
+ extracted_terms: terms,
376
+ city_preferences: cities,
377
+ candidate_pool: all.positions.length,
378
+ matches: scored.slice(0, topN).map((s) => s.position),
379
+ };
380
+ }
381
+ export { extractResumeSignals, scoreOverlap };
382
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_byd } from "./apply.js";
383
+ export async function fetchApplicationSchema(postId) {
384
+ const id = (postId ?? "").trim();
385
+ if (!id)
386
+ return { ok: false, source: "job.byd.com", message: "post_id is required" };
387
+ let title = "";
388
+ let applyUrl = "https://job.byd.com";
389
+ try {
390
+ const detail = (await fetchPositionDetail(id));
391
+ if (detail?.ok === false) {
392
+ return { ok: false, source: "job.byd.com", message: detail.message ?? "post not found" };
393
+ }
394
+ title = detail?.title ?? "";
395
+ if (detail?.apply_url)
396
+ applyUrl = detail.apply_url;
397
+ }
398
+ catch { }
399
+ return {
400
+ ok: true,
401
+ schema: _buildBespokeApplySchema_byd({
402
+ source: "job.byd.com",
403
+ postId: id,
404
+ jobTitle: title,
405
+ applyUrl,
406
+ submitEndpoint: "https://job.byd.com/portal/api/portal-api/resume/apply",
407
+ submitKind: "multipart-session",
408
+ endpointVerified: true,
409
+ submitNotes: "BYD — POST /portal/api/portal-api/resume/apply with JWT bearer (Token). Endpoint anon-probed → HTTP 200 + {code:4001, msg:\"Token无效或已过期: Not Authenticated\"} (unified gateway token middleware; the originally-inferred /position/apply returns structured 404 from the Spring position service, but /resume/apply, /job/apply, /applicant/apply, /resume/submit, /career/apply all hit the auth gateway). Body shape still needs validation against a real candidate session.",
410
+ }),
411
+ };
412
+ }