@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/lever.js ADDED
@@ -0,0 +1,455 @@
1
+ // Generic Lever (api.lever.co) postings adapter factory.
2
+ //
3
+ // Lever is a SaaS ATS used by many tech companies for public job boards. The
4
+ // unauthenticated REST surface is stable across tenants:
5
+ //
6
+ // GET https://api.lever.co/v0/postings/<slug>?mode=json
7
+ // → array of job objects (no pagination — entire board in one call)
8
+ //
9
+ // GET https://api.lever.co/v0/postings/<slug>/<id>?mode=json
10
+ // → full job object including the rendered description HTML
11
+ //
12
+ // All endpoints are GET-only, return JSON, and require no auth headers.
13
+ //
14
+ // ---- PositionSummary field mapping (Lever → canonical) ----
15
+ // post_id ← String(job.id)
16
+ // title ← job.text
17
+ // project ← job.categories.team or job.categories.department
18
+ // recruit_label ← job.categories.commitment (e.g. "Intern" / "Full-time")
19
+ // bgs ← "" (Lever has no BG dimension)
20
+ // work_cities ← job.categories.location (or join allLocations)
21
+ // apply_url ← job.hostedUrl (or applyUrl)
22
+ //
23
+ // ---- Discovery notes ----
24
+ // * Lever returns the full board in one ~200-400 KB JSON array. No pagination.
25
+ // * Some boards include both campus and experienced postings; we filter
26
+ // client-side by keyword / commitment / location.
27
+ // * `categories.allLocations` is an array; we join with " / " when len > 1.
28
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
29
+ export { checkResume };
30
+ // ---------- createAdapter ----------
31
+ export function createAdapter(cfg) {
32
+ const API_LIST = `https://api.lever.co/v0/postings/${encodeURIComponent(cfg.slug)}?mode=json`;
33
+ const API_DETAIL = (id) => `https://api.lever.co/v0/postings/${encodeURIComponent(cfg.slug)}/${encodeURIComponent(id)}?mode=json`;
34
+ const SOURCE = `api.lever.co/${cfg.slug}`;
35
+ const BOARD_URL = `https://jobs.lever.co/${encodeURIComponent(cfg.slug)}`;
36
+ const HEADERS = {
37
+ "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",
38
+ Accept: "application/json",
39
+ };
40
+ function summarize(job) {
41
+ const id = String(job.id ?? "");
42
+ const cats = job.categories ?? {};
43
+ const locs = cats.allLocations ?? [];
44
+ const work_cities = locs.length > 1
45
+ ? locs.filter(Boolean).join(" / ")
46
+ : cats.location ?? locs[0] ?? "";
47
+ return {
48
+ post_id: id,
49
+ title: job.text ?? "",
50
+ project: cats.team ?? cats.department ?? "",
51
+ recruit_label: cats.commitment ?? "",
52
+ bgs: "",
53
+ work_cities,
54
+ apply_url: job.hostedUrl ?? job.applyUrl ?? `${BOARD_URL}/${id}`,
55
+ };
56
+ }
57
+ let _allCache = null;
58
+ async function fetchAllRaw() {
59
+ const now = Date.now();
60
+ if (_allCache && now - _allCache.fetchedAt < 5 * 60 * 1000) {
61
+ return _allCache.ok ? { ok: true, jobs: _allCache.jobs } : { ok: false, message: _allCache.message };
62
+ }
63
+ let response;
64
+ try {
65
+ response = await fetch(API_LIST, { headers: HEADERS });
66
+ }
67
+ catch (err) {
68
+ const msg = `network error: ${err instanceof Error ? err.message : String(err)}`;
69
+ _allCache = { ok: false, message: msg, fetchedAt: now };
70
+ return { ok: false, message: msg };
71
+ }
72
+ if (!response.ok) {
73
+ const msg = `HTTP ${response.status}: ${response.statusText}`;
74
+ _allCache = { ok: false, message: msg, fetchedAt: now };
75
+ return { ok: false, message: msg };
76
+ }
77
+ let jobs;
78
+ try {
79
+ jobs = (await response.json());
80
+ }
81
+ catch (err) {
82
+ const msg = `bad JSON: ${err instanceof Error ? err.message : String(err)}`;
83
+ _allCache = { ok: false, message: msg, fetchedAt: now };
84
+ return { ok: false, message: msg };
85
+ }
86
+ if (!Array.isArray(jobs))
87
+ jobs = [];
88
+ _allCache = { ok: true, jobs, fetchedAt: now };
89
+ return { ok: true, jobs };
90
+ }
91
+ function applyFilters(jobs, opts) {
92
+ const kw = (opts.keyword ?? "").trim().toLowerCase();
93
+ const commitFilters = (opts.commitments ?? []).map((s) => String(s).toLowerCase());
94
+ const teamFilters = (opts.teams ?? []).map((s) => String(s).toLowerCase());
95
+ const cityFilters = (opts.cities ?? []).map((s) => String(s).toLowerCase());
96
+ return jobs.filter((job) => {
97
+ const cats = job.categories ?? {};
98
+ if (kw) {
99
+ const blob = [
100
+ job.text ?? "",
101
+ cats.team ?? "",
102
+ cats.department ?? "",
103
+ cats.location ?? "",
104
+ (cats.allLocations ?? []).join(" "),
105
+ cats.commitment ?? "",
106
+ ]
107
+ .join(" ")
108
+ .toLowerCase();
109
+ if (!blob.includes(kw))
110
+ return false;
111
+ }
112
+ if (commitFilters.length) {
113
+ const c = (cats.commitment ?? "").toLowerCase();
114
+ if (!commitFilters.some((f) => c.includes(f)))
115
+ return false;
116
+ }
117
+ if (teamFilters.length) {
118
+ const blob = `${cats.team ?? ""} ${cats.department ?? ""}`.toLowerCase();
119
+ if (!teamFilters.some((t) => blob.includes(t)))
120
+ return false;
121
+ }
122
+ if (cityFilters.length) {
123
+ const blob = [
124
+ cats.location ?? "",
125
+ ...(cats.allLocations ?? []),
126
+ ]
127
+ .join(" ")
128
+ .toLowerCase();
129
+ if (!cityFilters.some((c) => blob.includes(c)))
130
+ return false;
131
+ }
132
+ return true;
133
+ });
134
+ }
135
+ async function searchPositions(opts = {}) {
136
+ const pageSize = Math.max(1, Math.min(200, opts.pageSize ?? 20));
137
+ const page = Math.max(1, opts.page ?? 1);
138
+ const pool = await fetchAllRaw();
139
+ if (!pool.ok) {
140
+ return {
141
+ ok: false,
142
+ message: pool.message,
143
+ source: SOURCE,
144
+ apply_url: BOARD_URL,
145
+ positions: [],
146
+ };
147
+ }
148
+ const filtered = applyFilters(pool.jobs, opts);
149
+ const offset = (page - 1) * pageSize;
150
+ const paginated = filtered.slice(offset, offset + pageSize);
151
+ return {
152
+ ok: true,
153
+ source: SOURCE,
154
+ query: opts,
155
+ page,
156
+ page_size: pageSize,
157
+ total: filtered.length,
158
+ positions: paginated.map(summarize),
159
+ };
160
+ }
161
+ async function fetchAllPositions(opts = {}) {
162
+ const pool = await fetchAllRaw();
163
+ if (!pool.ok) {
164
+ return {
165
+ ok: false,
166
+ message: pool.message,
167
+ source: SOURCE,
168
+ apply_url: BOARD_URL,
169
+ fetched: 0,
170
+ positions: [],
171
+ };
172
+ }
173
+ const filtered = applyFilters(pool.jobs, opts);
174
+ return {
175
+ ok: true,
176
+ source: SOURCE,
177
+ total: filtered.length,
178
+ fetched: filtered.length,
179
+ positions: filtered.map(summarize),
180
+ };
181
+ }
182
+ async function fetchPositionDetail(postId) {
183
+ const id = (postId ?? "").trim();
184
+ if (!id) {
185
+ return { ok: false, source: SOURCE, message: "post_id is required" };
186
+ }
187
+ let response;
188
+ try {
189
+ response = await fetch(API_DETAIL(id), { headers: HEADERS });
190
+ }
191
+ catch (err) {
192
+ return {
193
+ ok: false,
194
+ source: SOURCE,
195
+ post_id: id,
196
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
197
+ };
198
+ }
199
+ if (!response.ok) {
200
+ return {
201
+ ok: false,
202
+ source: SOURCE,
203
+ post_id: id,
204
+ message: `HTTP ${response.status}: ${response.statusText}`,
205
+ };
206
+ }
207
+ let job;
208
+ try {
209
+ job = (await response.json());
210
+ }
211
+ catch (err) {
212
+ return {
213
+ ok: false,
214
+ source: SOURCE,
215
+ post_id: id,
216
+ message: `bad JSON: ${err instanceof Error ? err.message : String(err)}`,
217
+ };
218
+ }
219
+ const summary = summarize(job);
220
+ const sections = [job.descriptionPlain ?? ""];
221
+ for (const list of job.lists ?? []) {
222
+ const heading = list.text ?? "";
223
+ const body = (list.content ?? "")
224
+ .replace(/<[^>]+>/g, " ")
225
+ .replace(/&nbsp;/g, " ")
226
+ .replace(/&amp;/g, "&")
227
+ .replace(/\s+/g, " ")
228
+ .trim();
229
+ if (heading || body)
230
+ sections.push(`${heading}: ${body}`.trim());
231
+ }
232
+ sections.push(job.additionalPlain ?? "");
233
+ const description = sections.filter(Boolean).join("\n\n").trim();
234
+ return {
235
+ ok: true,
236
+ source: SOURCE,
237
+ post_id: id,
238
+ title: job.text ?? "",
239
+ project: summary.project,
240
+ recruit_label: summary.recruit_label,
241
+ workplace_type: job.workplaceType ?? "",
242
+ country: job.country ?? "",
243
+ created_at: job.createdAt ?? 0,
244
+ description,
245
+ work_cities: summary.work_cities,
246
+ apply_url: summary.apply_url,
247
+ };
248
+ }
249
+ // ---------- fetchDictionaries ----------
250
+ // Lever doesn't expose a filter catalog; synthesize from the live board.
251
+ let _dictCache = null;
252
+ async function fetchDictionaries() {
253
+ if (_dictCache !== null)
254
+ return _dictCache;
255
+ const pool = await fetchAllRaw();
256
+ if (!pool.ok) {
257
+ const r = { ok: false, source: SOURCE, message: pool.message };
258
+ _dictCache = r;
259
+ return r;
260
+ }
261
+ const teams = new Set();
262
+ const departments = new Set();
263
+ const commitments = new Set();
264
+ const cities = new Set();
265
+ for (const j of pool.jobs) {
266
+ const c = j.categories ?? {};
267
+ if (c.team)
268
+ teams.add(c.team);
269
+ if (c.department)
270
+ departments.add(c.department);
271
+ if (c.commitment)
272
+ commitments.add(c.commitment);
273
+ for (const loc of c.allLocations ?? [])
274
+ if (loc)
275
+ cities.add(loc);
276
+ if (c.location)
277
+ cities.add(c.location);
278
+ }
279
+ const result = {
280
+ ok: true,
281
+ source: SOURCE,
282
+ teams: [...teams].sort(),
283
+ departments: [...departments].sort(),
284
+ commitments: [...commitments].sort(),
285
+ cities: [...cities].sort(),
286
+ total: pool.jobs.length,
287
+ };
288
+ _dictCache = result;
289
+ return result;
290
+ }
291
+ // ---------- notices (stub) ----------
292
+ const NOTICES_STUB = {
293
+ ok: false,
294
+ source: SOURCE,
295
+ message: `${cfg.label}: Lever boards have no announcements endpoint`,
296
+ };
297
+ async function listNotices() {
298
+ return NOTICES_STUB;
299
+ }
300
+ async function getNotice(_id) {
301
+ return NOTICES_STUB;
302
+ }
303
+ async function findNoticesByQuestion(_question, _opts = {}) {
304
+ return NOTICES_STUB;
305
+ }
306
+ // ---------- matchResume ----------
307
+ async function matchResume(text, opts = {}) {
308
+ const topN = Math.max(1, opts.topN ?? 5);
309
+ const candidates = Math.max(topN, opts.candidates ?? 20);
310
+ const { terms, cities } = extractResumeSignals(text ?? "");
311
+ if (!terms.length) {
312
+ return {
313
+ ok: false,
314
+ source: SOURCE,
315
+ message: "could not extract any technical signals from the text",
316
+ preview: (text ?? "").slice(0, 120),
317
+ };
318
+ }
319
+ const pool = await fetchAllRaw();
320
+ if (!pool.ok) {
321
+ return { ok: false, source: SOURCE, message: pool.message, positions: [] };
322
+ }
323
+ const scored = [];
324
+ for (const job of pool.jobs) {
325
+ const c = job.categories ?? {};
326
+ const blob = [
327
+ job.text ?? "",
328
+ c.team ?? "",
329
+ c.department ?? "",
330
+ c.location ?? "",
331
+ (c.allLocations ?? []).join(" "),
332
+ c.commitment ?? "",
333
+ job.descriptionPlain ?? "",
334
+ ].join(" ");
335
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
336
+ if (score > 0)
337
+ scored.push({ score, raw: job, reasons });
338
+ }
339
+ scored.sort((a, b) => b.score - a.score);
340
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
341
+ if (!shortlist.length) {
342
+ shortlist = pool.jobs.slice(0, candidates).map((raw) => ({
343
+ score: 0,
344
+ raw,
345
+ reasons: [],
346
+ }));
347
+ }
348
+ const matches = shortlist.slice(0, topN).map((s) => {
349
+ const mr = s.reasons.length > 0
350
+ ? s.reasons.slice(0, 5)
351
+ : ["no specific keyword overlap — surfaced from full board listing"];
352
+ return { ...summarize(s.raw), match_reasons: mr };
353
+ });
354
+ return {
355
+ ok: true,
356
+ source: SOURCE,
357
+ extracted_terms: terms,
358
+ city_preferences: cities,
359
+ matches,
360
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
361
+ "The only authority on selection is HR.",
362
+ };
363
+ }
364
+ async function fetchApplicationSchema(postId) {
365
+ const id = (postId ?? "").trim();
366
+ if (!id)
367
+ return { ok: false, source: SOURCE, message: "post_id is required" };
368
+ let response;
369
+ try {
370
+ response = await fetch(API_DETAIL(id), { headers: HEADERS });
371
+ }
372
+ catch (err) {
373
+ return {
374
+ ok: false,
375
+ source: SOURCE,
376
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
377
+ };
378
+ }
379
+ if (!response.ok) {
380
+ return {
381
+ ok: false,
382
+ source: SOURCE,
383
+ message: `HTTP ${response.status}: ${response.statusText}`,
384
+ };
385
+ }
386
+ let job;
387
+ try {
388
+ job = (await response.json());
389
+ }
390
+ catch (err) {
391
+ return {
392
+ ok: false,
393
+ source: SOURCE,
394
+ message: `bad JSON: ${err instanceof Error ? err.message : String(err)}`,
395
+ };
396
+ }
397
+ // Lever's standard contact-info block.
398
+ const standard = [
399
+ { label: "First Name", required: true, fields: [{ name: "first_name", type: "input_text" }] },
400
+ { label: "Last Name", required: true, fields: [{ name: "last_name", type: "input_text" }] },
401
+ { label: "Email", required: true, fields: [{ name: "email", type: "input_text" }] },
402
+ { label: "Phone", required: true, fields: [{ name: "phone", type: "input_text" }] },
403
+ { label: "Resume", required: true, fields: [{ name: "resume", type: "input_file" }] },
404
+ ];
405
+ // Custom-question fields keyed by their human label so the staging
406
+ // step can match them via profile.custom["…"].
407
+ const custom = (job.customQuestions ?? []).flatMap((cq) => (cq.fields ?? []).map((f) => ({
408
+ label: f.text ?? cq.text ?? "",
409
+ description: cq.description ?? null,
410
+ required: f.required ?? false,
411
+ fields: [
412
+ {
413
+ name: (f.text ?? cq.text ?? "").slice(0, 60).replace(/\s+/g, "_").toLowerCase(),
414
+ type: f.type === "multiple-choice"
415
+ ? "single_select"
416
+ : f.type === "multi-choice"
417
+ ? "multi_select"
418
+ : f.type === "textarea"
419
+ ? "textarea"
420
+ : "input_text",
421
+ values: (f.options ?? []).map((o) => ({ value: o.text ?? "", label: o.text ?? "" })),
422
+ },
423
+ ],
424
+ })));
425
+ return {
426
+ ok: true,
427
+ schema: {
428
+ source: SOURCE,
429
+ post_id: id,
430
+ job_title: job.text ?? "",
431
+ apply_url: job.applyUrl ?? job.hostedUrl ?? `${BOARD_URL}/${id}/apply`,
432
+ submit_endpoint: `${BOARD_URL}/${id}/apply`,
433
+ submit_method: "POST",
434
+ submit_kind: "multipart-anon",
435
+ endpoint_verified: true,
436
+ submit_notes: "Lever apply-page accepts anonymous multipart/form-data POST whose field " +
437
+ "names match Lever's hosted apply form (standard contact-info + each " +
438
+ "customQuestion's auto-named field).",
439
+ questions: [...standard, ...custom],
440
+ },
441
+ };
442
+ }
443
+ return {
444
+ searchPositions,
445
+ fetchAllPositions,
446
+ fetchPositionDetail,
447
+ fetchDictionaries,
448
+ listNotices,
449
+ getNotice,
450
+ findNoticesByQuestion,
451
+ matchResume,
452
+ checkResume,
453
+ fetchApplicationSchema,
454
+ };
455
+ }