job-pro 0.9.3 → 0.9.4

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.
package/dist/cambricon.js CHANGED
@@ -30,3 +30,4 @@ export const getNotice = adapter.getNotice;
30
30
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
31
31
  export const matchResume = adapter.matchResume;
32
32
  export const checkResume = adapter.checkResume;
33
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
package/dist/deepseek.js CHANGED
@@ -22,3 +22,4 @@ export const getNotice = adapter.getNotice;
22
22
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
23
23
  export const matchResume = adapter.matchResume;
24
24
  export const checkResume = adapter.checkResume;
25
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
@@ -21,3 +21,4 @@ export const getNotice = adapter.getNotice;
21
21
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
22
22
  export const matchResume = adapter.matchResume;
23
23
  export const checkResume = adapter.checkResume;
24
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
package/dist/geely.js CHANGED
@@ -32,3 +32,4 @@ export const getNotice = adapter.getNotice;
32
32
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
33
33
  export const matchResume = adapter.matchResume;
34
34
  export const checkResume = adapter.checkResume;
35
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
@@ -43,3 +43,4 @@ export const getNotice = adapter.getNotice;
43
43
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
44
44
  export const matchResume = adapter.matchResume;
45
45
  export const checkResume = adapter.checkResume;
46
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
package/dist/iflytek.js CHANGED
@@ -337,3 +337,41 @@ export { extractResumeSignals, scoreOverlap };
337
337
  // Silence unused warning for the GET helper — kept for future taxonomy/city
338
338
  // endpoints that return BeisenEnvelope JSON via GET.
339
339
  void get;
340
+ export async function fetchApplicationSchema(postId) {
341
+ const id = (postId ?? '').trim();
342
+ if (!id)
343
+ return { ok: false, source: SOURCE, message: 'post_id is required' };
344
+ let title = '';
345
+ try {
346
+ const detail = await fetchPositionDetail(id);
347
+ if (detail?.ok === false) {
348
+ return { ok: false, source: SOURCE, message: detail.message ?? 'post not found' };
349
+ }
350
+ title = detail?.title ?? '';
351
+ }
352
+ catch { }
353
+ const questions = [
354
+ { label: 'Name', required: true, fields: [{ name: 'name', type: 'input_text' }] },
355
+ { label: 'Email', required: true, fields: [{ name: 'email', type: 'input_text' }] },
356
+ { label: 'Phone', required: true, fields: [{ name: 'phone', type: 'input_text' }] },
357
+ { label: 'Resume', required: true, fields: [{ name: 'resume', type: 'input_file' }] },
358
+ ];
359
+ return {
360
+ ok: true,
361
+ schema: {
362
+ source: SOURCE,
363
+ post_id: id,
364
+ job_title: title,
365
+ apply_url: 'https://iflytek.zhiye.com/jobs',
366
+ submit_endpoint: 'https://iflytek.zhiye.com/api/Apply/SubmitResume',
367
+ submit_method: 'POST',
368
+ submit_kind: 'beisen-italent',
369
+ submit_notes: 'Beisen iTalent apply: POST /api/Resume/UploadResume (multipart) + ' +
370
+ 'POST /api/Apply/SubmitResume with { JobAdId, ResumeId, … }. ' +
371
+ 'Requires candidate session — Beisen iTalent uses email+phone+OTP login at ' +
372
+ '/login.html. Capture via extension/, drop session.json under ~/.jobpro/. ' +
373
+ 'Multi-step submitter lands in a future iteration.',
374
+ questions,
375
+ },
376
+ };
377
+ }
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ import { loadProfile, loadSession, profileTemplate, stageApplication, submitAppl
54
54
  import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
55
55
  import { writeFileSync, mkdirSync, existsSync } from "node:fs";
56
56
  import { dirname } from "node:path";
57
- const VERSION = "0.9.3";
57
+ const VERSION = "0.9.4";
58
58
  const COMPANIES = [
59
59
  { key: "tencent", family: "Bespoke", source: "join.qq.com", label: "Tencent / 腾讯" },
60
60
  { key: "bytedance", family: "Bespoke", source: "jobs.bytedance.com", label: "ByteDance / 字节跳动" },
@@ -124,10 +124,12 @@ by ATS family (Bespoke / Feishu / Beisen Wecruit / Beisen iTalent / Moka
124
124
  / Greenhouse-Lever / Liepin). Coverage summary at job.ha7ch.com.
125
125
 
126
126
  PHASE 2 (auto-apply) is in early access. \`job-pro <co> apply <postId>\`
127
- prints the staged POST in dry-run mode. Today only Greenhouse +
128
- Lever boards (xpeng / hoyoverse / weride) expose the application
129
- schema; the rest return a "not yet wired" note. See docs/auto-apply.md
130
- for the rollout plan.
127
+ prints the staged POST in dry-run mode. 23 / 50 adapters expose the
128
+ application schema today: 3 Greenhouse/Lever (auto-submit ready),
129
+ 9 🟡 Feishu, 7 🟡 Moka, 2 🟡 Beisen Wecruit, 2 🟡 Beisen iTalent (all
130
+ 🟡 need a captured session.json + the family-specific multi-step
131
+ submitter). The remaining 27 fall back to a "Phase 2 not yet wired"
132
+ note. See docs/auto-apply.md.
131
133
 
132
134
  VERBS (same surface for every company)
133
135
  search <kw> search openings (free text)
package/dist/megvii.js CHANGED
@@ -24,3 +24,4 @@ export const getNotice = adapter.getNotice;
24
24
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
25
25
  export const matchResume = adapter.matchResume;
26
26
  export const checkResume = adapter.checkResume;
27
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
package/dist/moka.js CHANGED
@@ -397,6 +397,63 @@ export function createAdapter(cfg) {
397
397
  matches: scored,
398
398
  };
399
399
  }
400
+ // ---------- Phase 2: fetchApplicationSchema ----------
401
+ //
402
+ // Moka apply endpoints discovered in
403
+ // static-ats.mokahr.com/recruitment-web-client/javascripts/recruitmentWeb-*.js
404
+ // (probed 2026-05-16, 4.2 MB bundle):
405
+ //
406
+ // GET /api/get_job_apply_form/?jobId=<uuid>&orgId=<slug>
407
+ // → returns the per-job questions array (subject to org config)
408
+ // POST /api/outer/ats-apply/website/applicant-limit-check
409
+ // → rate-limit / dedupe pre-flight
410
+ // POST /api/outer/ats-apply/website/getValidateConfig
411
+ // → returns whether SMS validation is required
412
+ // POST /api/outer/ats-apply/website/sendApplyValidateSmsCode
413
+ // → send the candidate's phone an SMS code
414
+ // POST /api/outer/ats-apply/website/apply
415
+ // → final submission. Body is AES-128-CBC encrypted with the
416
+ // per-response `necromancer` key + page-level aesIv (same
417
+ // envelope as our existing read-side cli/src/moka.ts decrypt).
418
+ //
419
+ // The whole flow requires the candidate to be logged in via Moka's
420
+ // candidate-portal (email + SMS verification). Cookies for that
421
+ // session are captured by the browser extension and dropped under
422
+ // ~/.jobpro/<adapter>.session.json — see docs/auto-apply.md.
423
+ async function fetchApplicationSchema(postId) {
424
+ const id = (postId ?? "").trim();
425
+ if (!id)
426
+ return { ok: false, source: SOURCE, message: "post_id is required" };
427
+ // Find the job title via our existing search infrastructure.
428
+ const r = await fetchPositionDetail(id);
429
+ const detailAny = r;
430
+ // Standard contact-info questions Moka tenants always require.
431
+ const questions = [
432
+ { label: "Name", required: true, fields: [{ name: "name", type: "input_text" }] },
433
+ { label: "Email", required: true, fields: [{ name: "email", type: "input_text" }] },
434
+ { label: "Phone", required: true, fields: [{ name: "phone", type: "input_text" }] },
435
+ { label: "Resume", required: true, fields: [{ name: "resume", type: "input_file" }] },
436
+ ];
437
+ return {
438
+ ok: true,
439
+ schema: {
440
+ source: SOURCE,
441
+ post_id: id,
442
+ job_title: detailAny.title ?? "",
443
+ apply_url: `${portalUrl(pickChannel())}#/jobs/${encodeURIComponent(id)}`,
444
+ submit_endpoint: "https://app.mokahr.com/api/outer/ats-apply/website/apply",
445
+ submit_method: "POST",
446
+ submit_kind: "moka-aes",
447
+ submit_notes: "Moka apply flow: GET /api/get_job_apply_form (questions) → " +
448
+ "POST /applicant-limit-check (rate-limit) → POST /getValidateConfig + " +
449
+ "/sendApplyValidateSmsCode (if SMS required) → POST /website/apply with " +
450
+ "AES-128-CBC envelope {data, necromancer} (same encryption as the read-side " +
451
+ "list endpoint). Requires candidate session — capture via extension/, drop " +
452
+ "session.json under ~/.jobpro/. Multi-step submitter wires in next iteration.",
453
+ questions,
454
+ },
455
+ };
456
+ }
400
457
  return {
401
458
  searchPositions,
402
459
  fetchAllPositions,
@@ -407,6 +464,7 @@ export function createAdapter(cfg) {
407
464
  findNoticesByQuestion,
408
465
  matchResume,
409
466
  checkResume,
467
+ fetchApplicationSchema,
410
468
  };
411
469
  }
412
470
  export { extractResumeSignals, scoreOverlap };
package/dist/moonshot.js CHANGED
@@ -21,3 +21,4 @@ export const getNotice = adapter.getNotice;
21
21
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
22
22
  export const matchResume = adapter.matchResume;
23
23
  export const checkResume = adapter.checkResume;
24
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
package/dist/sensetime.js CHANGED
@@ -48,3 +48,4 @@ export const getNotice = adapter.getNotice;
48
48
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
49
49
  export const matchResume = adapter.matchResume;
50
50
  export const checkResume = adapter.checkResume;
51
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
package/dist/stepfun.js CHANGED
@@ -21,3 +21,4 @@ export const getNotice = adapter.getNotice;
21
21
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
22
22
  export const matchResume = adapter.matchResume;
23
23
  export const checkResume = adapter.checkResume;
24
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;
package/dist/vivo.js CHANGED
@@ -318,3 +318,41 @@ export async function matchResume(text, opts = {}) {
318
318
  };
319
319
  }
320
320
  export { extractResumeSignals, scoreOverlap };
321
+ export async function fetchApplicationSchema(postId) {
322
+ const id = (postId ?? '').trim();
323
+ if (!id)
324
+ return { ok: false, source: SOURCE, message: 'post_id is required' };
325
+ let title = '';
326
+ try {
327
+ const detail = await fetchPositionDetail(id);
328
+ if (detail?.ok === false) {
329
+ return { ok: false, source: SOURCE, message: detail.message ?? 'post not found' };
330
+ }
331
+ title = detail?.title ?? '';
332
+ }
333
+ catch { }
334
+ const questions = [
335
+ { label: 'Name', required: true, fields: [{ name: 'name', type: 'input_text' }] },
336
+ { label: 'Email', required: true, fields: [{ name: 'email', type: 'input_text' }] },
337
+ { label: 'Phone', required: true, fields: [{ name: 'phone', type: 'input_text' }] },
338
+ { label: 'Resume', required: true, fields: [{ name: 'resume', type: 'input_file' }] },
339
+ ];
340
+ return {
341
+ ok: true,
342
+ schema: {
343
+ source: SOURCE,
344
+ post_id: id,
345
+ job_title: title,
346
+ apply_url: 'https://vivo.zhiye.com/jobs',
347
+ submit_endpoint: 'https://vivo.zhiye.com/api/Apply/SubmitResume',
348
+ submit_method: 'POST',
349
+ submit_kind: 'beisen-italent',
350
+ submit_notes: 'Beisen iTalent apply: POST /api/Resume/UploadResume (multipart) + ' +
351
+ 'POST /api/Apply/SubmitResume with { JobAdId, ResumeId, … }. ' +
352
+ 'Requires candidate session — Beisen iTalent uses email+phone+OTP login at ' +
353
+ '/login.html. Capture via extension/, drop session.json under ~/.jobpro/. ' +
354
+ 'Multi-step submitter lands in a future iteration.',
355
+ questions,
356
+ },
357
+ };
358
+ }
package/dist/wecruit.js CHANGED
@@ -371,6 +371,54 @@ export function createAdapter(cfg) {
371
371
  "The only authority on selection is HR.",
372
372
  };
373
373
  }
374
+ // ---------- Phase 2: fetchApplicationSchema ----------
375
+ //
376
+ // Beisen Wecruit apply endpoints discovered in
377
+ // hr.sensetime.com/pb/js/vendor.js (probed 2026-05-16, 3.8 MB bundle):
378
+ //
379
+ // POST /wecruit/resume/upload/file/save/<channelId> — upload resume PDF/DOCX
380
+ // POST /wecruit/resume/info/add/<channelId> — create/update profile
381
+ // POST /wecruit/resume/info/get/<channelId> — read existing profile
382
+ // POST /wecruit/delivery/resume/<channelId> — final submission
383
+ //
384
+ // The candidate session is established by Wecruit's WeChat-OAuth or
385
+ // phone-OTP login at /pb/<channel>/login.html. Cookies for that session
386
+ // are captured by the browser extension and dropped under
387
+ // ~/.jobpro/<adapter>.session.json.
388
+ async function fetchApplicationSchema(postId) {
389
+ const id = (postId ?? "").trim();
390
+ if (!id)
391
+ return { ok: false, source: SOURCE, message: "post_id is required" };
392
+ const ch = cfg.channels[0];
393
+ if (!ch)
394
+ return { ok: false, source: SOURCE, message: "no channels configured" };
395
+ const detail = await fetchPositionDetail(id);
396
+ const detailAny = detail;
397
+ const questions = [
398
+ { label: "Name", required: true, fields: [{ name: "name", type: "input_text" }] },
399
+ { label: "Email", required: true, fields: [{ name: "email", type: "input_text" }] },
400
+ { label: "Phone", required: true, fields: [{ name: "phone", type: "input_text" }] },
401
+ { label: "Resume", required: true, fields: [{ name: "resume", type: "input_file" }] },
402
+ ];
403
+ return {
404
+ ok: true,
405
+ schema: {
406
+ source: SOURCE,
407
+ post_id: id,
408
+ job_title: detailAny.title ?? "",
409
+ apply_url: `${SITE_ROOT}/${encodeURIComponent(ch.channelId)}/pb/${ch.pagePath}.html`,
410
+ submit_endpoint: `${SITE_ROOT}/wecruit/delivery/resume/${encodeURIComponent(ch.channelId)}`,
411
+ submit_method: "POST",
412
+ submit_kind: "beisen-wecruit",
413
+ submit_notes: "Beisen Wecruit apply flow: POST /wecruit/resume/upload/file/save/<SU> → " +
414
+ "POST /wecruit/resume/info/add/<SU> → POST /wecruit/delivery/resume/<SU> with " +
415
+ "{ post_id, resume_attachment_id, channel_id }. Requires candidate session " +
416
+ "(WeChat OAuth or phone OTP via /pb/<channel>/login.html). Capture via extension/, " +
417
+ "drop session.json under ~/.jobpro/. Multi-step submitter lands in a future iteration.",
418
+ questions,
419
+ },
420
+ };
421
+ }
374
422
  return {
375
423
  searchPositions,
376
424
  fetchAllPositions,
@@ -381,5 +429,6 @@ export function createAdapter(cfg) {
381
429
  findNoticesByQuestion,
382
430
  matchResume,
383
431
  checkResume,
432
+ fetchApplicationSchema,
384
433
  };
385
434
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-pro",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Query Chinese big-tech campus recruiting from your terminal. 50 companies, all 50 live. 46 via each company's own API; the 4 with no public canonical feed (Hikvision, CICC, Cainiao, WeBank) surfaced via Liepin as a clearly-labeled third-party fallback. No signup, no token, no server.",
5
5
  "homepage": "https://job.ha7ch.com",
6
6
  "repository": "https://github.com/HA7CH/job-pro",