job-pro 1.0.4 → 1.0.6

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/apply.js CHANGED
@@ -437,21 +437,16 @@ export async function submitApplication(staged, target, options = {}) {
437
437
  if (options.extraHeaders) {
438
438
  Object.assign(headers, options.extraHeaders);
439
439
  }
440
- let response;
441
- try {
442
- response = await fetch(url, {
443
- method: staged.submit_method ?? "POST",
444
- headers,
445
- body: fd,
446
- });
447
- }
448
- catch (err) {
440
+ const r = await fetchWithRetry(url, { method: staged.submit_method ?? "POST", headers, body: fd }, "submit");
441
+ if (!r.ok) {
449
442
  return {
450
443
  ok: false,
451
444
  posted_to: url,
452
- message: `network error: ${err instanceof Error ? err.message : String(err)}`,
445
+ status: r.status,
446
+ message: r.message,
453
447
  };
454
448
  }
449
+ const response = r.response;
455
450
  let preview = "";
456
451
  try {
457
452
  preview = (await response.text()).slice(0, 4000);
@@ -581,33 +576,20 @@ export async function executeFeishu3Step(staged, session, target) {
581
576
  const sessionHeaders = sessionHeaderBag(session, host);
582
577
  // STEP 1 — upload tokens
583
578
  const step1Url = debug ? target.url : `${apiRoot}/attachment/upload/tokens`;
584
- let step1Resp;
585
- try {
586
- step1Resp = await fetch(step1Url, {
587
- method: "POST",
588
- headers: {
589
- ...sessionHeaders,
590
- "Content-Type": "application/json",
591
- Accept: "application/json, text/plain, */*",
592
- },
593
- body: JSON.stringify({ filename, file_size: fileSize }),
594
- });
595
- }
596
- catch (err) {
597
- steps.push({ step: "upload-tokens", url: step1Url, status: 0, ok: false, message: String(err) });
598
- return { ok: false, posted_to: step1Url, message: "step 1 network error", steps };
599
- }
600
- const step1Body = await step1Resp.text();
601
- steps.push({
602
- step: "upload-tokens",
603
- url: step1Url,
604
- status: step1Resp.status,
605
- ok: step1Resp.ok,
606
- message: step1Body.slice(0, 200),
607
- });
608
- if (!step1Resp.ok) {
609
- return { ok: false, posted_to: step1Url, status: step1Resp.status, message: "step 1 failed (upload tokens)", steps, response_preview: step1Body.slice(0, 4000) };
579
+ const s1 = await doStep("upload-tokens", step1Url, {
580
+ method: "POST",
581
+ headers: {
582
+ ...sessionHeaders,
583
+ "Content-Type": "application/json",
584
+ Accept: "application/json, text/plain, */*",
585
+ },
586
+ body: JSON.stringify({ filename, file_size: fileSize }),
587
+ }, steps);
588
+ if (!s1.ok) {
589
+ return { ok: false, posted_to: step1Url, status: s1.status, message: `step 1 failed: ${s1.message}`, steps };
610
590
  }
591
+ const step1Resp = s1.response;
592
+ const step1Body = s1.text;
611
593
  // In debug mode, we don't actually have a presigned URL — short-circuit.
612
594
  if (debug) {
613
595
  return {
@@ -645,23 +627,14 @@ export async function executeFeishu3Step(staged, session, target) {
645
627
  ? new FileCtor([new Uint8Array(resumeBytes)], filename, { type: "application/pdf" })
646
628
  : new Blob([new Uint8Array(resumeBytes)], { type: "application/pdf" });
647
629
  uploadFd.append("file", filePart, filename);
648
- let step2Resp;
649
- try {
650
- step2Resp = await fetch(upload_url, { method: "POST", body: uploadFd });
630
+ const s2 = await doStep("upload-file", upload_url, { method: "POST", body: uploadFd }, steps);
631
+ if (!s2.ok) {
632
+ return { ok: false, posted_to: upload_url, status: s2.status, message: `step 2 failed: ${s2.message}`, steps };
651
633
  }
652
- catch (err) {
653
- steps.push({ step: "upload-file", url: upload_url, status: 0, ok: false, message: String(err) });
654
- return { ok: false, posted_to: upload_url, message: "step 2 network error", steps };
655
- }
656
- steps.push({
657
- step: "upload-file",
658
- url: upload_url,
659
- status: step2Resp.status,
660
- ok: step2Resp.ok,
661
- message: `HTTP ${step2Resp.status}`,
662
- });
663
- if (!step2Resp.ok) {
664
- return { ok: false, posted_to: upload_url, status: step2Resp.status, message: "step 2 failed (upload to CDN)", steps };
634
+ // s2 already pushed to steps via doStep; if upstream returned non-2xx
635
+ // (after retries on 5xx), surface that.
636
+ if (!s2.response.ok) {
637
+ return { ok: false, posted_to: upload_url, status: s2.response.status, message: "step 2 failed (upload to CDN)", steps };
665
638
  }
666
639
  // STEP 3 — resume/apply
667
640
  const applicantInfo = {};
@@ -676,39 +649,107 @@ export async function executeFeishu3Step(staged, session, target) {
676
649
  applicant_info: applicantInfo,
677
650
  };
678
651
  const step3Url = `${apiRoot}/resume/apply`;
679
- let step3Resp;
680
- try {
681
- step3Resp = await fetch(step3Url, {
682
- method: "POST",
683
- headers: {
684
- ...sessionHeaders,
685
- "Content-Type": "application/json",
686
- Accept: "application/json, text/plain, */*",
687
- },
688
- body: JSON.stringify(step3Body),
689
- });
690
- }
691
- catch (err) {
692
- steps.push({ step: "resume-apply", url: step3Url, status: 0, ok: false, message: String(err) });
693
- return { ok: false, posted_to: step3Url, message: "step 3 network error", steps };
652
+ const s3 = await doStep("resume-apply", step3Url, {
653
+ method: "POST",
654
+ headers: {
655
+ ...sessionHeaders,
656
+ "Content-Type": "application/json",
657
+ Accept: "application/json, text/plain, */*",
658
+ },
659
+ body: JSON.stringify(step3Body),
660
+ }, steps);
661
+ if (!s3.ok) {
662
+ return { ok: false, posted_to: step3Url, status: s3.status, message: `step 3 failed: ${s3.message}`, steps };
694
663
  }
695
- const step3Text = await step3Resp.text();
696
- steps.push({
697
- step: "resume-apply",
698
- url: step3Url,
699
- status: step3Resp.status,
700
- ok: step3Resp.ok,
701
- message: step3Text.slice(0, 200),
702
- });
703
664
  return {
704
- ok: step3Resp.ok,
705
- status: step3Resp.status,
665
+ ok: s3.response.ok,
666
+ status: s3.response.status,
706
667
  posted_to: step3Url,
707
- response_preview: step3Text.slice(0, 4000),
708
- message: step3Resp.ok ? "Feishu 3-step submission accepted" : `step 3 rejected: HTTP ${step3Resp.status}`,
668
+ response_preview: s3.text,
669
+ message: s3.response.ok ? "Feishu 3-step submission accepted" : `step 3 rejected: HTTP ${s3.response.status}`,
709
670
  steps,
710
671
  };
711
672
  }
673
+ async function fetchWithRetry(url, init, label, log) {
674
+ const maxRetries = Math.max(0, Math.min(5, Number.parseInt(process.env.JOB_PRO_RETRY ?? "2", 10) || 2));
675
+ let lastErr = "";
676
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
677
+ let response = null;
678
+ try {
679
+ response = await fetch(url, init);
680
+ }
681
+ catch (err) {
682
+ lastErr = `network error: ${err instanceof Error ? err.message : String(err)}`;
683
+ log?.push({ attempt: attempt + 1, ok: false, message: `${label}: ${lastErr}` });
684
+ // Network errors are retryable. Back off and try again.
685
+ if (attempt < maxRetries) {
686
+ await sleep(retryDelayMs(attempt));
687
+ continue;
688
+ }
689
+ return { ok: false, message: lastErr };
690
+ }
691
+ // 4xx → user error, don't retry.
692
+ if (response.status >= 400 && response.status < 500) {
693
+ log?.push({ attempt: attempt + 1, ok: false, status: response.status, message: `${label}: HTTP ${response.status} (no retry — 4xx)` });
694
+ return { ok: false, status: response.status, message: `HTTP ${response.status}: ${response.statusText}` };
695
+ }
696
+ // 5xx → server error, retry.
697
+ if (response.status >= 500 && attempt < maxRetries) {
698
+ lastErr = `HTTP ${response.status}: ${response.statusText}`;
699
+ log?.push({ attempt: attempt + 1, ok: false, status: response.status, message: `${label}: ${lastErr} (will retry)` });
700
+ await sleep(retryDelayMs(attempt));
701
+ continue;
702
+ }
703
+ log?.push({ attempt: attempt + 1, ok: response.ok, status: response.status, message: `${label}: HTTP ${response.status}` });
704
+ return { ok: true, response };
705
+ }
706
+ return { ok: false, message: lastErr || "exhausted retries" };
707
+ }
708
+ function retryDelayMs(attempt) {
709
+ // Exponential backoff with jitter: 250ms / 500ms / 1s / 2s / 4s, ±25%.
710
+ const base = 250 * Math.pow(2, attempt);
711
+ const jitter = base * (Math.random() * 0.5 - 0.25);
712
+ return Math.round(base + jitter);
713
+ }
714
+ function sleep(ms) {
715
+ return new Promise((resolve) => setTimeout(resolve, ms));
716
+ }
717
+ /**
718
+ * Family-executor convenience wrapper. Combines fetchWithRetry's
719
+ * transient-failure handling with the FeishuStepLog bookkeeping that
720
+ * each executor needs to push into result.steps. Returns the response
721
+ * + decoded text, or the error message; either way appends one entry
722
+ * to `steps[]`.
723
+ */
724
+ async function doStep(step, url, init, steps) {
725
+ const r = await fetchWithRetry(url, init, step);
726
+ if (!r.ok) {
727
+ steps.push({
728
+ step,
729
+ url,
730
+ status: r.status ?? 0,
731
+ ok: false,
732
+ message: r.message.slice(0, 200),
733
+ });
734
+ return { ok: false, status: r.status, message: r.message };
735
+ }
736
+ const response = r.response;
737
+ let text = "";
738
+ try {
739
+ text = (await response.text()).slice(0, 4000);
740
+ }
741
+ catch {
742
+ /* binary or stream */
743
+ }
744
+ steps.push({
745
+ step,
746
+ url,
747
+ status: response.status,
748
+ ok: response.ok,
749
+ message: text.slice(0, 200) || `HTTP ${response.status}`,
750
+ });
751
+ return { ok: true, response, text };
752
+ }
712
753
  /** Build the headers bag used by every Feishu/Beisen/Moka step. */
713
754
  function sessionHeaderBag(session, targetHost) {
714
755
  const out = {
@@ -799,42 +840,32 @@ export async function executeMokaApply(staged, session, target) {
799
840
  fd.append("resume", filePart, filename);
800
841
  const steps = [];
801
842
  const sessionHeaders = sessionHeaderBag(session, host);
802
- // Pre-flight limit check (optional — skip in debug since we'd redirect)
843
+ // Pre-flight limit check (optional — skip in debug since we'd redirect).
844
+ // Best-effort; we ignore failures here because the upstream submit will
845
+ // surface any blocker more authoritatively.
803
846
  if (!debug && session) {
804
847
  const lc = `${apiRoot}/api/outer/ats-apply/website/applicant-limit-check`;
805
- try {
806
- const resp = await fetch(lc, {
807
- method: "POST",
808
- headers: { ...sessionHeaders, "Content-Type": "application/json" },
809
- body: JSON.stringify({ orgId: slug, jobId: staged.post_id }),
810
- });
811
- const txt = (await resp.text()).slice(0, 200);
812
- steps.push({ step: "limit-check", url: lc, status: resp.status, ok: resp.ok, message: txt });
813
- }
814
- catch (err) {
815
- steps.push({ step: "limit-check", url: lc, status: 0, ok: false, message: String(err) });
816
- }
817
- }
818
- // Final submit
819
- let resp;
820
- try {
821
- resp = await fetch(targetUrl, {
848
+ await doStep("limit-check", lc, {
822
849
  method: "POST",
823
- headers: sessionHeaders, // Content-Type: multipart/form-data; boundary set by undici
824
- body: fd,
825
- });
826
- }
827
- catch (err) {
828
- steps.push({ step: "apply", url: targetUrl, status: 0, ok: false, message: String(err) });
829
- return { ok: false, posted_to: targetUrl, message: "apply step network error", steps };
850
+ headers: { ...sessionHeaders, "Content-Type": "application/json" },
851
+ body: JSON.stringify({ orgId: slug, jobId: staged.post_id }),
852
+ }, steps);
830
853
  }
831
- const text = (await resp.text()).slice(0, 4000);
832
- steps.push({ step: "apply", url: targetUrl, status: resp.status, ok: resp.ok, message: text.slice(0, 200) });
854
+ // Final submit
855
+ const sFinal = await doStep("apply", targetUrl, {
856
+ method: "POST",
857
+ headers: sessionHeaders, // Content-Type: multipart/form-data; boundary set by undici
858
+ body: fd,
859
+ }, steps);
860
+ if (!sFinal.ok) {
861
+ return { ok: false, posted_to: targetUrl, status: sFinal.status, message: `apply failed: ${sFinal.message}`, steps };
862
+ }
863
+ const resp = sFinal.response;
833
864
  return {
834
865
  ok: resp.ok,
835
866
  status: resp.status,
836
867
  posted_to: targetUrl,
837
- response_preview: text,
868
+ response_preview: sFinal.text,
838
869
  message: resp.ok ? "Moka apply submitted" : `Moka apply rejected: HTTP ${resp.status}`,
839
870
  steps,
840
871
  };
@@ -894,16 +925,12 @@ export async function executeBeisenWecruit(staged, session, target) {
894
925
  ? new FileCtor([new Uint8Array(resumeBytes)], filename, { type: "application/pdf" })
895
926
  : new Blob([new Uint8Array(resumeBytes)], { type: "application/pdf" });
896
927
  uploadFd.append("file", filePart, filename);
897
- let r1;
898
- try {
899
- r1 = await fetch(step1Url, { method: "POST", headers: sessionHeaders, body: uploadFd });
900
- }
901
- catch (err) {
902
- steps.push({ step: "upload-file", url: step1Url, status: 0, ok: false, message: String(err) });
903
- return { ok: false, posted_to: step1Url, message: "step 1 network error", steps };
928
+ const s1 = await doStep("upload-file", step1Url, { method: "POST", headers: sessionHeaders, body: uploadFd }, steps);
929
+ if (!s1.ok) {
930
+ return { ok: false, posted_to: step1Url, status: s1.status, message: `step 1 failed: ${s1.message}`, steps };
904
931
  }
905
- const text1 = (await r1.text()).slice(0, 2000);
906
- steps.push({ step: "upload-file", url: step1Url, status: r1.status, ok: r1.ok, message: text1.slice(0, 200) });
932
+ const r1 = s1.response;
933
+ const text1 = s1.text;
907
934
  if (debug) {
908
935
  return { ok: r1.ok, status: r1.status, posted_to: step1Url, message: "debug: step 1 fired, steps 2+3 skipped", steps, response_preview: text1 };
909
936
  }
@@ -918,36 +945,26 @@ export async function executeBeisenWecruit(staged, session, target) {
918
945
  catch { /* keep empty */ }
919
946
  // STEP 2 — profile info
920
947
  const step2Url = `${apiBase}/resume/info/add/${encodeURIComponent(su)}`;
921
- let r2;
922
- try {
923
- r2 = await fetch(step2Url, {
924
- method: "POST",
925
- headers: { ...sessionHeaders, "Content-Type": "application/json" },
926
- body: JSON.stringify({ name: applicant.name, email: applicant.email, phone: applicant.phone, attachmentId }),
927
- });
928
- }
929
- catch (err) {
930
- steps.push({ step: "profile-add", url: step2Url, status: 0, ok: false, message: String(err) });
931
- return { ok: false, posted_to: step2Url, message: "step 2 network error", steps };
948
+ const s2 = await doStep("profile-add", step2Url, {
949
+ method: "POST",
950
+ headers: { ...sessionHeaders, "Content-Type": "application/json" },
951
+ body: JSON.stringify({ name: applicant.name, email: applicant.email, phone: applicant.phone, attachmentId }),
952
+ }, steps);
953
+ if (!s2.ok) {
954
+ return { ok: false, posted_to: step2Url, status: s2.status, message: `step 2 failed: ${s2.message}`, steps };
932
955
  }
933
- const text2 = (await r2.text()).slice(0, 2000);
934
- steps.push({ step: "profile-add", url: step2Url, status: r2.status, ok: r2.ok, message: text2.slice(0, 200) });
935
956
  // STEP 3 — final delivery
936
957
  const step3Url = `${apiBase}/delivery/resume/${encodeURIComponent(su)}`;
937
- let r3;
938
- try {
939
- r3 = await fetch(step3Url, {
940
- method: "POST",
941
- headers: { ...sessionHeaders, "Content-Type": "application/json" },
942
- body: JSON.stringify({ postId: staged.post_id, attachmentId }),
943
- });
944
- }
945
- catch (err) {
946
- steps.push({ step: "deliver", url: step3Url, status: 0, ok: false, message: String(err) });
947
- return { ok: false, posted_to: step3Url, message: "step 3 network error", steps };
948
- }
949
- const text3 = (await r3.text()).slice(0, 4000);
950
- steps.push({ step: "deliver", url: step3Url, status: r3.status, ok: r3.ok, message: text3.slice(0, 200) });
958
+ const s3 = await doStep("deliver", step3Url, {
959
+ method: "POST",
960
+ headers: { ...sessionHeaders, "Content-Type": "application/json" },
961
+ body: JSON.stringify({ postId: staged.post_id, attachmentId }),
962
+ }, steps);
963
+ if (!s3.ok) {
964
+ return { ok: false, posted_to: step3Url, status: s3.status, message: `step 3 failed: ${s3.message}`, steps };
965
+ }
966
+ const r3 = s3.response;
967
+ const text3 = s3.text;
951
968
  return {
952
969
  ok: r3.ok,
953
970
  status: r3.status,
@@ -1011,16 +1028,12 @@ export async function executeBeisenITalent(staged, session, target) {
1011
1028
  ? new FileCtor([new Uint8Array(resumeBytes)], filename, { type: "application/pdf" })
1012
1029
  : new Blob([new Uint8Array(resumeBytes)], { type: "application/pdf" });
1013
1030
  uploadFd.append("file", filePart, filename);
1014
- let r1;
1015
- try {
1016
- r1 = await fetch(step1Url, { method: "POST", headers: sessionHeaders, body: uploadFd });
1017
- }
1018
- catch (err) {
1019
- steps.push({ step: "upload", url: step1Url, status: 0, ok: false, message: String(err) });
1020
- return { ok: false, posted_to: step1Url, message: "step 1 network error", steps };
1031
+ const s1 = await doStep("upload", step1Url, { method: "POST", headers: sessionHeaders, body: uploadFd }, steps);
1032
+ if (!s1.ok) {
1033
+ return { ok: false, posted_to: step1Url, status: s1.status, message: `step 1 failed: ${s1.message}`, steps };
1021
1034
  }
1022
- const text1 = (await r1.text()).slice(0, 2000);
1023
- steps.push({ step: "upload", url: step1Url, status: r1.status, ok: r1.ok, message: text1.slice(0, 200) });
1035
+ const r1 = s1.response;
1036
+ const text1 = s1.text;
1024
1037
  if (debug) {
1025
1038
  return { ok: r1.ok, status: r1.status, posted_to: step1Url, message: "debug: step 1 fired, step 2 skipped", steps, response_preview: text1 };
1026
1039
  }
@@ -1034,31 +1047,26 @@ export async function executeBeisenITalent(staged, session, target) {
1034
1047
  catch { /* keep empty */ }
1035
1048
  // STEP 2 — submit apply
1036
1049
  const step2Url = `${apiRoot}/api/Apply/SubmitResume`;
1037
- let r2;
1038
- try {
1039
- r2 = await fetch(step2Url, {
1040
- method: "POST",
1041
- headers: { ...sessionHeaders, "Content-Type": "application/json" },
1042
- body: JSON.stringify({
1043
- JobAdId: staged.post_id,
1044
- ResumeId: resumeId,
1045
- Name: applicant.name,
1046
- Email: applicant.email,
1047
- Mobile: applicant.phone,
1048
- }),
1049
- });
1050
- }
1051
- catch (err) {
1052
- steps.push({ step: "submit", url: step2Url, status: 0, ok: false, message: String(err) });
1053
- return { ok: false, posted_to: step2Url, message: "step 2 network error", steps };
1054
- }
1055
- const text2 = (await r2.text()).slice(0, 4000);
1056
- steps.push({ step: "submit", url: step2Url, status: r2.status, ok: r2.ok, message: text2.slice(0, 200) });
1050
+ const s2 = await doStep("submit", step2Url, {
1051
+ method: "POST",
1052
+ headers: { ...sessionHeaders, "Content-Type": "application/json" },
1053
+ body: JSON.stringify({
1054
+ JobAdId: staged.post_id,
1055
+ ResumeId: resumeId,
1056
+ Name: applicant.name,
1057
+ Email: applicant.email,
1058
+ Mobile: applicant.phone,
1059
+ }),
1060
+ }, steps);
1061
+ if (!s2.ok) {
1062
+ return { ok: false, posted_to: step2Url, status: s2.status, message: `step 2 failed: ${s2.message}`, steps };
1063
+ }
1064
+ const r2 = s2.response;
1057
1065
  return {
1058
1066
  ok: r2.ok,
1059
1067
  status: r2.status,
1060
1068
  posted_to: step2Url,
1061
- response_preview: text2,
1069
+ response_preview: s2.text,
1062
1070
  message: r2.ok ? "Beisen iTalent submission accepted" : `step 2 rejected: HTTP ${r2.status}`,
1063
1071
  steps,
1064
1072
  };
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ import { createRequire as require_createRequire } from "node:module";
60
60
  function require_module() {
61
61
  return { createRequire: require_createRequire };
62
62
  }
63
- const VERSION = "1.0.4";
63
+ const VERSION = "1.0.6";
64
64
  const COMPANIES = [
65
65
  { key: "tencent", family: "Bespoke", source: "join.qq.com", label: "Tencent / 腾讯" },
66
66
  { key: "bytedance", family: "Bespoke", source: "jobs.bytedance.com", label: "ByteDance / 字节跳动" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-pro",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",