job-pro 0.9.4 → 0.9.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/alibaba.js CHANGED
@@ -476,3 +476,33 @@ export async function matchResume(text, opts = {}) {
476
476
  "The only authority on selection is HR.",
477
477
  };
478
478
  }
479
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_alibaba } from "./apply.js";
480
+ export async function fetchApplicationSchema(postId) {
481
+ const id = (postId ?? "").trim();
482
+ if (!id)
483
+ return { ok: false, source: "campus-talent.alibaba.com", message: "post_id is required" };
484
+ let title = "";
485
+ let applyUrl = "https://campus-talent.alibaba.com";
486
+ try {
487
+ const detail = (await fetchPositionDetail(id));
488
+ if (detail?.ok === false) {
489
+ return { ok: false, source: "campus-talent.alibaba.com", message: detail.message ?? "post not found" };
490
+ }
491
+ title = detail?.title ?? "";
492
+ if (detail?.apply_url)
493
+ applyUrl = detail.apply_url;
494
+ }
495
+ catch { }
496
+ return {
497
+ ok: true,
498
+ schema: _buildBespokeApplySchema_alibaba({
499
+ source: "campus-talent.alibaba.com",
500
+ postId: id,
501
+ jobTitle: title,
502
+ applyUrl,
503
+ submitEndpoint: "https://campus-talent.alibaba.com/campus/applyPosition.json",
504
+ submitKind: "multipart-session",
505
+ submitNotes: "Alibaba — POST /campus/applyPosition.json with session cookie. Alipay OAuth gates the session. Endpoint inferred; needs validation.",
506
+ }),
507
+ };
508
+ }
package/dist/antgroup.js CHANGED
@@ -360,3 +360,33 @@ export async function matchResume(text, opts = {}) {
360
360
  };
361
361
  }
362
362
  export { extractResumeSignals, scoreOverlap };
363
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_antgroup } from "./apply.js";
364
+ export async function fetchApplicationSchema(postId) {
365
+ const id = (postId ?? "").trim();
366
+ if (!id)
367
+ return { ok: false, source: "hrcareersweb.antgroup.com", message: "post_id is required" };
368
+ let title = "";
369
+ let applyUrl = "https://hrcareersweb.antgroup.com";
370
+ try {
371
+ const detail = (await fetchPositionDetail(id));
372
+ if (detail?.ok === false) {
373
+ return { ok: false, source: "hrcareersweb.antgroup.com", message: detail.message ?? "post not found" };
374
+ }
375
+ title = detail?.title ?? "";
376
+ if (detail?.apply_url)
377
+ applyUrl = detail.apply_url;
378
+ }
379
+ catch { }
380
+ return {
381
+ ok: true,
382
+ schema: _buildBespokeApplySchema_antgroup({
383
+ source: "hrcareersweb.antgroup.com",
384
+ postId: id,
385
+ jobTitle: title,
386
+ applyUrl,
387
+ submitEndpoint: "https://hrcareersweb.antgroup.com/api/social/position/apply",
388
+ submitKind: "multipart-session",
389
+ submitNotes: "Ant Group — POST /api/social/position/apply (also campus variant). Alipay OAuth session required. Endpoint inferred from parallel read-side path; needs validation.",
390
+ }),
391
+ };
392
+ }
package/dist/apply.js CHANGED
@@ -201,6 +201,25 @@ export function formatStaged(s) {
201
201
  function truncate(s, n) {
202
202
  return s.length > n ? s.slice(0, n - 1) + "…" : s;
203
203
  }
204
+ export function buildBespokeApplySchema(cfg) {
205
+ const standard = [
206
+ { label: "Name", required: true, fields: [{ name: "name", type: "input_text" }] },
207
+ { label: "Email", required: true, fields: [{ name: "email", type: "input_text" }] },
208
+ { label: "Phone", required: true, fields: [{ name: "phone", type: "input_text" }] },
209
+ { label: "Resume", required: true, fields: [{ name: "resume", type: "input_file" }] },
210
+ ];
211
+ return {
212
+ source: cfg.source,
213
+ post_id: cfg.postId,
214
+ job_title: cfg.jobTitle,
215
+ apply_url: cfg.applyUrl,
216
+ submit_endpoint: cfg.submitEndpoint,
217
+ submit_method: cfg.submitEndpoint ? "POST" : undefined,
218
+ submit_kind: cfg.submitKind ?? "multipart-session",
219
+ submit_notes: cfg.submitNotes,
220
+ questions: [...standard, ...(cfg.extraQuestions ?? [])],
221
+ };
222
+ }
204
223
  export async function submitApplication(staged, target, options = {}) {
205
224
  if (!staged.submit_endpoint) {
206
225
  return {
@@ -336,3 +355,215 @@ async function buildMultipartForm(staged) {
336
355
  }
337
356
  return fd;
338
357
  }
358
+ /**
359
+ * Feishu Recruiting 3-step submission. Used by every 🟡 feishu-3-step
360
+ * adapter (xiaomi / nio / minimax / zhipu / iqiyi / agibot / zerooneai /
361
+ * baichuan, and moonshot when wired through the Feishu helper).
362
+ *
363
+ * Steps:
364
+ * 1. POST {host}/api/v1/attachment/upload/tokens
365
+ * body: { filename, file_size }
366
+ * → { code:0, data:{ upload_url, attachment_id, fields:{…} } }
367
+ * 2. POST/PUT to data.upload_url (lf-package-cn.feishucdn.com or similar)
368
+ * multipart/form-data with fields[…] + file bytes
369
+ * 3. POST {host}/api/v1/resume/apply
370
+ * body: { post_id, attachment_id, applicant_info:{ name, email, phone } }
371
+ * → { code:0, data:{ application_id } }
372
+ *
373
+ * Session.json must contain valid Feishu cookies (typically `_csrf_token`,
374
+ * `lark_oapi_session`, `passport_csrf_token`) for the host.
375
+ */
376
+ export async function executeFeishu3Step(staged, session, target) {
377
+ if (!staged.submit_endpoint) {
378
+ return { ok: false, posted_to: "", message: "no submit_endpoint", steps: [] };
379
+ }
380
+ if (target.kind === "dry-run") {
381
+ return {
382
+ ok: false,
383
+ posted_to: "dry-run (no network)",
384
+ message: "dry-run requested — no HTTP call fired",
385
+ steps: [],
386
+ };
387
+ }
388
+ if (target.kind === "upstream" && !session) {
389
+ return {
390
+ ok: false,
391
+ posted_to: staged.submit_endpoint,
392
+ message: "executeFeishu3Step requires a captured session (~/.jobpro/<adapter>.session.json) " +
393
+ "— Feishu apply endpoints all gate on candidate-session cookies. Install extension/ " +
394
+ "in Chrome, log in to the careers site, click Export.",
395
+ steps: [],
396
+ };
397
+ }
398
+ const submitUrl = new URL(staged.submit_endpoint);
399
+ const host = submitUrl.host;
400
+ const apiRoot = `${submitUrl.protocol}//${host}/api/v1`;
401
+ const debug = target.kind === "debug";
402
+ // Resolve the resume file from staged fields.
403
+ const resumeField = staged.staged.find((f) => f.name === "resume");
404
+ if (!resumeField || !resumeField.value) {
405
+ return { ok: false, posted_to: "", message: "staged.resume missing", steps: [] };
406
+ }
407
+ let resumeBytes;
408
+ try {
409
+ resumeBytes = readFileSync(resumeField.value);
410
+ }
411
+ catch (err) {
412
+ return {
413
+ ok: false,
414
+ posted_to: "",
415
+ message: `could not read resume ${resumeField.value}: ${err instanceof Error ? err.message : err}`,
416
+ steps: [],
417
+ };
418
+ }
419
+ const filename = resumeField.value.split("/").pop() ?? "resume.pdf";
420
+ const fileSize = resumeBytes.length;
421
+ const steps = [];
422
+ const sessionHeaders = sessionHeaderBag(session, host);
423
+ // STEP 1 — upload tokens
424
+ const step1Url = debug ? target.url : `${apiRoot}/attachment/upload/tokens`;
425
+ let step1Resp;
426
+ try {
427
+ step1Resp = await fetch(step1Url, {
428
+ method: "POST",
429
+ headers: {
430
+ ...sessionHeaders,
431
+ "Content-Type": "application/json",
432
+ Accept: "application/json, text/plain, */*",
433
+ },
434
+ body: JSON.stringify({ filename, file_size: fileSize }),
435
+ });
436
+ }
437
+ catch (err) {
438
+ steps.push({ step: "upload-tokens", url: step1Url, status: 0, ok: false, message: String(err) });
439
+ return { ok: false, posted_to: step1Url, message: "step 1 network error", steps };
440
+ }
441
+ const step1Body = await step1Resp.text();
442
+ steps.push({
443
+ step: "upload-tokens",
444
+ url: step1Url,
445
+ status: step1Resp.status,
446
+ ok: step1Resp.ok,
447
+ message: step1Body.slice(0, 200),
448
+ });
449
+ if (!step1Resp.ok) {
450
+ return { ok: false, posted_to: step1Url, status: step1Resp.status, message: "step 1 failed (upload tokens)", steps, response_preview: step1Body.slice(0, 4000) };
451
+ }
452
+ // In debug mode, we don't actually have a presigned URL — short-circuit.
453
+ if (debug) {
454
+ return {
455
+ ok: true,
456
+ posted_to: step1Url,
457
+ status: step1Resp.status,
458
+ message: "debug-submit-to: step 1 fired; steps 2+3 skipped (no real upload URL in echo response)",
459
+ steps,
460
+ response_preview: step1Body.slice(0, 4000),
461
+ };
462
+ }
463
+ let step1Parsed;
464
+ try {
465
+ step1Parsed = JSON.parse(step1Body);
466
+ }
467
+ catch {
468
+ return { ok: false, posted_to: step1Url, message: "step 1 returned non-JSON", steps };
469
+ }
470
+ if (step1Parsed.code !== 0 || !step1Parsed.data?.upload_url) {
471
+ return {
472
+ ok: false,
473
+ posted_to: step1Url,
474
+ message: `step 1 upstream error: ${step1Parsed.message ?? `code=${step1Parsed.code}`}`,
475
+ steps,
476
+ response_preview: step1Body.slice(0, 4000),
477
+ };
478
+ }
479
+ const { upload_url, attachment_id, fields } = step1Parsed.data;
480
+ // STEP 2 — upload resume to presigned URL
481
+ const uploadFd = new FormData();
482
+ for (const [k, v] of Object.entries(fields ?? {}))
483
+ uploadFd.append(k, v);
484
+ const FileCtor = globalThis.File;
485
+ const filePart = typeof FileCtor === "function"
486
+ ? new FileCtor([new Uint8Array(resumeBytes)], filename, { type: "application/pdf" })
487
+ : new Blob([new Uint8Array(resumeBytes)], { type: "application/pdf" });
488
+ uploadFd.append("file", filePart, filename);
489
+ let step2Resp;
490
+ try {
491
+ step2Resp = await fetch(upload_url, { method: "POST", body: uploadFd });
492
+ }
493
+ catch (err) {
494
+ steps.push({ step: "upload-file", url: upload_url, status: 0, ok: false, message: String(err) });
495
+ return { ok: false, posted_to: upload_url, message: "step 2 network error", steps };
496
+ }
497
+ steps.push({
498
+ step: "upload-file",
499
+ url: upload_url,
500
+ status: step2Resp.status,
501
+ ok: step2Resp.ok,
502
+ message: `HTTP ${step2Resp.status}`,
503
+ });
504
+ if (!step2Resp.ok) {
505
+ return { ok: false, posted_to: upload_url, status: step2Resp.status, message: "step 2 failed (upload to CDN)", steps };
506
+ }
507
+ // STEP 3 — resume/apply
508
+ const applicantInfo = {};
509
+ for (const f of staged.staged) {
510
+ if (f.name === "name" || f.name === "email" || f.name === "phone") {
511
+ applicantInfo[f.name] = f.value;
512
+ }
513
+ }
514
+ const step3Body = {
515
+ post_id: staged.post_id,
516
+ attachment_id,
517
+ applicant_info: applicantInfo,
518
+ };
519
+ const step3Url = `${apiRoot}/resume/apply`;
520
+ let step3Resp;
521
+ try {
522
+ step3Resp = await fetch(step3Url, {
523
+ method: "POST",
524
+ headers: {
525
+ ...sessionHeaders,
526
+ "Content-Type": "application/json",
527
+ Accept: "application/json, text/plain, */*",
528
+ },
529
+ body: JSON.stringify(step3Body),
530
+ });
531
+ }
532
+ catch (err) {
533
+ steps.push({ step: "resume-apply", url: step3Url, status: 0, ok: false, message: String(err) });
534
+ return { ok: false, posted_to: step3Url, message: "step 3 network error", steps };
535
+ }
536
+ const step3Text = await step3Resp.text();
537
+ steps.push({
538
+ step: "resume-apply",
539
+ url: step3Url,
540
+ status: step3Resp.status,
541
+ ok: step3Resp.ok,
542
+ message: step3Text.slice(0, 200),
543
+ });
544
+ return {
545
+ ok: step3Resp.ok,
546
+ status: step3Resp.status,
547
+ posted_to: step3Url,
548
+ response_preview: step3Text.slice(0, 4000),
549
+ message: step3Resp.ok ? "Feishu 3-step submission accepted" : `step 3 rejected: HTTP ${step3Resp.status}`,
550
+ steps,
551
+ };
552
+ }
553
+ /** Build the headers bag used by every Feishu/Beisen/Moka step. */
554
+ function sessionHeaderBag(session, targetHost) {
555
+ const out = {
556
+ "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",
557
+ };
558
+ if (!session)
559
+ return out;
560
+ const cookieHeader = serializeCookieHeader(session, targetHost);
561
+ if (cookieHeader)
562
+ out.Cookie = cookieHeader;
563
+ for (const [k, v] of Object.entries(session.headers ?? {})) {
564
+ if (k.toLowerCase() === "cookie" || k.toLowerCase() === "content-type")
565
+ continue;
566
+ out[k] = v;
567
+ }
568
+ return out;
569
+ }
package/dist/baidu.js CHANGED
@@ -419,3 +419,33 @@ export async function matchResume(text, opts = {}) {
419
419
  "The only authority on selection is HR.",
420
420
  };
421
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/external/baidu/applyJob.json",
447
+ submitKind: "multipart-session",
448
+ submitNotes: "Baidu — POST /external/baidu/applyJob.json with session cookie. Endpoint inferred; needs validation.",
449
+ }),
450
+ };
451
+ }
package/dist/bilibili.js CHANGED
@@ -422,3 +422,33 @@ export async function findNoticesByQuestion(_question, _opts = {}) {
422
422
  message: "Bilibili: no public notices endpoint",
423
423
  };
424
424
  }
425
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_bilibili } from "./apply.js";
426
+ export async function fetchApplicationSchema(postId) {
427
+ const id = (postId ?? "").trim();
428
+ if (!id)
429
+ return { ok: false, source: "jobs.bilibili.com", message: "post_id is required" };
430
+ let title = "";
431
+ let applyUrl = "https://jobs.bilibili.com";
432
+ try {
433
+ const detail = (await fetchPositionDetail(id));
434
+ if (detail?.ok === false) {
435
+ return { ok: false, source: "jobs.bilibili.com", message: detail.message ?? "post not found" };
436
+ }
437
+ title = detail?.title ?? "";
438
+ if (detail?.apply_url)
439
+ applyUrl = detail.apply_url;
440
+ }
441
+ catch { }
442
+ return {
443
+ ok: true,
444
+ schema: _buildBespokeApplySchema_bilibili({
445
+ source: "jobs.bilibili.com",
446
+ postId: id,
447
+ jobTitle: title,
448
+ applyUrl,
449
+ submitEndpoint: "https://jobs.bilibili.com/api/post/apply",
450
+ submitKind: "multipart-session",
451
+ submitNotes: "Bilibili — POST /api/post/apply with session cookie. Endpoint inferred; needs validation.",
452
+ }),
453
+ };
454
+ }
package/dist/byd.js CHANGED
@@ -379,3 +379,33 @@ export async function matchResume(text, opts = {}) {
379
379
  };
380
380
  }
381
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/position/apply",
407
+ submitKind: "multipart-session",
408
+ submitNotes: "BYD — POST /portal/api/portal-api/position/apply with JWT bearer + session. Endpoint inferred from job.byd.com bundle; needs validation.",
409
+ }),
410
+ };
411
+ }
package/dist/bytedance.js CHANGED
@@ -586,3 +586,33 @@ export async function matchResume(text, opts = {}) {
586
586
  "The only authority on selection is HR.",
587
587
  };
588
588
  }
589
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_bytedance } from "./apply.js";
590
+ export async function fetchApplicationSchema(postId) {
591
+ const id = (postId ?? "").trim();
592
+ if (!id)
593
+ return { ok: false, source: "jobs.bytedance.com", message: "post_id is required" };
594
+ let title = "";
595
+ let applyUrl = "https://jobs.bytedance.com";
596
+ try {
597
+ const detail = (await fetchPositionDetail(id));
598
+ if (detail?.ok === false) {
599
+ return { ok: false, source: "jobs.bytedance.com", message: detail.message ?? "post not found" };
600
+ }
601
+ title = detail?.title ?? "";
602
+ if (detail?.apply_url)
603
+ applyUrl = detail.apply_url;
604
+ }
605
+ catch { }
606
+ return {
607
+ ok: true,
608
+ schema: _buildBespokeApplySchema_bytedance({
609
+ source: "jobs.bytedance.com",
610
+ postId: id,
611
+ jobTitle: title,
612
+ applyUrl,
613
+ submitEndpoint: "https://jobs.bytedance.com/api/v1/user_apply",
614
+ submitKind: "multipart-session",
615
+ submitNotes: "ByteDance — POST /api/v1/user_apply with session cookie. CAPTCHA verification required for first-time applicants. Endpoint inferred; needs validation.",
616
+ }),
617
+ };
618
+ }
package/dist/cainiao.js CHANGED
@@ -24,3 +24,33 @@ 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
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_cainiao } from "./apply.js";
28
+ export async function fetchApplicationSchema(postId) {
29
+ const id = (postId ?? "").trim();
30
+ if (!id)
31
+ return { ok: false, source: "cainiao.com (via api-c.liepin.com)", message: "post_id is required" };
32
+ let title = "";
33
+ let applyUrl = "https://cainiao.com";
34
+ try {
35
+ const detail = (await fetchPositionDetail(id));
36
+ if (detail?.ok === false) {
37
+ return { ok: false, source: "cainiao.com (via api-c.liepin.com)", message: detail.message ?? "post not found" };
38
+ }
39
+ title = detail?.title ?? "";
40
+ if (detail?.apply_url)
41
+ applyUrl = detail.apply_url;
42
+ }
43
+ catch { }
44
+ return {
45
+ ok: true,
46
+ schema: _buildBespokeApplySchema_cainiao({
47
+ source: "cainiao.com (via api-c.liepin.com)",
48
+ postId: id,
49
+ jobTitle: title,
50
+ applyUrl,
51
+ submitEndpoint: undefined,
52
+ submitKind: "external",
53
+ submitNotes: "Cainiao (Liepin-backed) — submission is recruiter-IM-mediated through Liepin. Open the apply_url to start the chat.",
54
+ }),
55
+ };
56
+ }
package/dist/cicc.js CHANGED
@@ -24,3 +24,33 @@ 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
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_cicc } from "./apply.js";
28
+ export async function fetchApplicationSchema(postId) {
29
+ const id = (postId ?? "").trim();
30
+ if (!id)
31
+ return { ok: false, source: "cicc.com (via api-c.liepin.com)", message: "post_id is required" };
32
+ let title = "";
33
+ let applyUrl = "https://cicc.com";
34
+ try {
35
+ const detail = (await fetchPositionDetail(id));
36
+ if (detail?.ok === false) {
37
+ return { ok: false, source: "cicc.com (via api-c.liepin.com)", message: detail.message ?? "post not found" };
38
+ }
39
+ title = detail?.title ?? "";
40
+ if (detail?.apply_url)
41
+ applyUrl = detail.apply_url;
42
+ }
43
+ catch { }
44
+ return {
45
+ ok: true,
46
+ schema: _buildBespokeApplySchema_cicc({
47
+ source: "cicc.com (via api-c.liepin.com)",
48
+ postId: id,
49
+ jobTitle: title,
50
+ applyUrl,
51
+ submitEndpoint: undefined,
52
+ submitKind: "external",
53
+ submitNotes: "CICC (Liepin-backed) — submission is recruiter-IM-mediated through Liepin. Open the apply_url to start the chat.",
54
+ }),
55
+ };
56
+ }
package/dist/didi.js CHANGED
@@ -348,3 +348,33 @@ export async function matchResume(text, opts = {}) {
348
348
  "The only authority on selection is HR.",
349
349
  };
350
350
  }
351
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_didi } from "./apply.js";
352
+ export async function fetchApplicationSchema(postId) {
353
+ const id = (postId ?? "").trim();
354
+ if (!id)
355
+ return { ok: false, source: "talent.didiglobal.com", message: "post_id is required" };
356
+ let title = "";
357
+ let applyUrl = "https://talent.didiglobal.com";
358
+ try {
359
+ const detail = (await fetchPositionDetail(id));
360
+ if (detail?.ok === false) {
361
+ return { ok: false, source: "talent.didiglobal.com", message: detail.message ?? "post not found" };
362
+ }
363
+ title = detail?.title ?? "";
364
+ if (detail?.apply_url)
365
+ applyUrl = detail.apply_url;
366
+ }
367
+ catch { }
368
+ return {
369
+ ok: true,
370
+ schema: _buildBespokeApplySchema_didi({
371
+ source: "talent.didiglobal.com",
372
+ postId: id,
373
+ jobTitle: title,
374
+ applyUrl,
375
+ submitEndpoint: "https://talent.didiglobal.com/talent-api/applyResume",
376
+ submitKind: "multipart-session",
377
+ submitNotes: "Didi — POST /talent-api/applyResume with session cookie. Endpoint inferred; needs validation.",
378
+ }),
379
+ };
380
+ }
package/dist/hikvision.js CHANGED
@@ -26,3 +26,33 @@ export const getNotice = adapter.getNotice;
26
26
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
27
27
  export const matchResume = adapter.matchResume;
28
28
  export const checkResume = adapter.checkResume;
29
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_hikvision } from "./apply.js";
30
+ export async function fetchApplicationSchema(postId) {
31
+ const id = (postId ?? "").trim();
32
+ if (!id)
33
+ return { ok: false, source: "hikvision.com (via api-c.liepin.com)", message: "post_id is required" };
34
+ let title = "";
35
+ let applyUrl = "https://hikvision.com";
36
+ try {
37
+ const detail = (await fetchPositionDetail(id));
38
+ if (detail?.ok === false) {
39
+ return { ok: false, source: "hikvision.com (via api-c.liepin.com)", message: detail.message ?? "post not found" };
40
+ }
41
+ title = detail?.title ?? "";
42
+ if (detail?.apply_url)
43
+ applyUrl = detail.apply_url;
44
+ }
45
+ catch { }
46
+ return {
47
+ ok: true,
48
+ schema: _buildBespokeApplySchema_hikvision({
49
+ source: "hikvision.com (via api-c.liepin.com)",
50
+ postId: id,
51
+ jobTitle: title,
52
+ applyUrl,
53
+ submitEndpoint: undefined,
54
+ submitKind: "external",
55
+ submitNotes: "Hikvision (Liepin-backed) — submission is recruiter-IM-mediated through Liepin. Open the apply_url to start the chat.",
56
+ }),
57
+ };
58
+ }
package/dist/huawei.js CHANGED
@@ -504,3 +504,33 @@ export async function matchResume(text, opts = {}) {
504
504
  "The only authority on selection is HR.",
505
505
  };
506
506
  }
507
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_huawei } from "./apply.js";
508
+ export async function fetchApplicationSchema(postId) {
509
+ const id = (postId ?? "").trim();
510
+ if (!id)
511
+ return { ok: false, source: "career.huawei.com", message: "post_id is required" };
512
+ let title = "";
513
+ let applyUrl = "https://career.huawei.com";
514
+ try {
515
+ const detail = (await fetchPositionDetail(id));
516
+ if (detail?.ok === false) {
517
+ return { ok: false, source: "career.huawei.com", message: detail.message ?? "post not found" };
518
+ }
519
+ title = detail?.title ?? "";
520
+ if (detail?.apply_url)
521
+ applyUrl = detail.apply_url;
522
+ }
523
+ catch { }
524
+ return {
525
+ ok: true,
526
+ schema: _buildBespokeApplySchema_huawei({
527
+ source: "career.huawei.com",
528
+ postId: id,
529
+ jobTitle: title,
530
+ applyUrl,
531
+ submitEndpoint: "https://career.huawei.com/career/api/web/postApply",
532
+ submitKind: "multipart-session",
533
+ submitNotes: "Huawei — POST /career/api/web/postApply with session cookie. Endpoint inferred; needs validation.",
534
+ }),
535
+ };
536
+ }
package/dist/index.js CHANGED
@@ -50,11 +50,11 @@ import * as geely from "./geely.js";
50
50
  import * as webank from "./webank.js";
51
51
  import * as horizonrobotics from "./horizonrobotics.js";
52
52
  import * as cambricon from "./cambricon.js";
53
- import { loadProfile, loadSession, profileTemplate, stageApplication, submitApplication, formatStaged, } from "./apply.js";
53
+ import { loadProfile, loadSession, profileTemplate, stageApplication, submitApplication, executeFeishu3Step, formatStaged, } from "./apply.js";
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.4";
57
+ const VERSION = "0.9.6";
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 / 字节跳动" },
@@ -123,13 +123,22 @@ USAGE
123
123
  by ATS family (Bespoke / Feishu / Beisen Wecruit / Beisen iTalent / Moka
124
124
  / Greenhouse-Lever / Liepin). Coverage summary at job.ha7ch.com.
125
125
 
126
- PHASE 2 (auto-apply) is in early access. \`job-pro <co> apply <postId>\`
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.
126
+ PHASE 2 (auto-apply) schema coverage is now 50/50:
127
+ 3 Greenhouse / Lever (xpeng / hoyoverse / weride auto-submit
128
+ ready, no session needed)
129
+ 🟡 22 bespoke session (tencent, bytedance, alibaba, …)
130
+ 🟡 9 Feishu (xiaomi, nio, minimax, moonshot, zhipu,
131
+ iqiyi, agibot, lilith, zerooneai, baichuan)
132
+ 🟡 7 Moka (megvii, deepseek, galaxyuniversal,
133
+ stepfun, cambricon, geely, moonshot)
134
+ 🟡 2 Beisen Wecruit (sensetime, horizonrobotics)
135
+ 🟡 2 Beisen iTalent (vivo, iflytek)
136
+ ⛔ 5 external (unitree WeChat, hikvision/cicc/cainiao/
137
+ webank — Liepin IM-mediated)
138
+ \`apply <postId>\` dry-runs the staged POST for any of them. The 🟡
139
+ families need a session.json (extension/) + a family-specific
140
+ multi-step submitter; --really-submit currently fires only for ✅.
141
+ See docs/auto-apply.md.
133
142
 
134
143
  VERBS (same surface for every company)
135
144
  search <kw> search openings (free text)
@@ -432,8 +441,15 @@ async function runCompany(adapter, company, rawArgs) {
432
441
  const session = loadSession(company);
433
442
  // Mode selection: --debug-submit-to <url> overrides everything.
434
443
  if (debugUrl) {
444
+ // Route through the family-specific executor where appropriate so the
445
+ // user can verify each step's wire format against their echo server.
446
+ const kindForDebug = sr.schema.submit_kind ?? "multipart-anon";
447
+ if (kindForDebug === "feishu-3-step") {
448
+ const result = await executeFeishu3Step(staged, session, { kind: "debug", url: debugUrl });
449
+ return emit({ mode: "debug-submit", staged, submit_kind: kindForDebug, result }, compact);
450
+ }
435
451
  const result = await submitApplication(staged, { kind: "debug", url: debugUrl });
436
- return emit({ mode: "debug-submit", staged, result }, compact);
452
+ return emit({ mode: "debug-submit", staged, submit_kind: kindForDebug, result }, compact);
437
453
  }
438
454
  // --really-submit: actually hit the upstream endpoint. Guarded by both
439
455
  // an env-var attestation and (for non-anon adapters) a session.json.
@@ -469,6 +485,36 @@ async function runCompany(adapter, company, rawArgs) {
469
485
  const isAnonMultipart = kind === "multipart-anon";
470
486
  const isSessionMultipart = kind === "multipart-session";
471
487
  const isGenericMultipart = isAnonMultipart || isSessionMultipart;
488
+ if (kind === "external") {
489
+ return emit({
490
+ ok: false,
491
+ source: company,
492
+ post_id: postId,
493
+ mode: "really-submit-external",
494
+ staged,
495
+ submit_kind: kind,
496
+ apply_url: staged.apply_url,
497
+ message: `${company} has no programmatic submit API — recruiting is mediated ` +
498
+ `via WeChat mini-program / Liepin recruiter chat / other IM channel. ` +
499
+ `Open apply_url in your browser to start the actual application flow.`,
500
+ }, compact);
501
+ }
502
+ if (kind === "feishu-3-step") {
503
+ if (!session) {
504
+ return emit({
505
+ ok: false,
506
+ source: company,
507
+ post_id: postId,
508
+ mode: "really-submit-blocked",
509
+ staged,
510
+ message: `Feishu 3-step submission requires a captured session at ` +
511
+ `~/.jobpro/${company}.session.json. Install extension/ in Chrome, ` +
512
+ `log in to the careers site, click Export.`,
513
+ }, compact);
514
+ }
515
+ const result = await executeFeishu3Step(staged, session, { kind: "upstream" });
516
+ return emit({ mode: "really-submit", staged, submit_kind: kind, session_used: true, result }, compact);
517
+ }
472
518
  if (!isGenericMultipart) {
473
519
  return emit({
474
520
  ok: false,
package/dist/jd.js CHANGED
@@ -526,3 +526,33 @@ export async function matchResume(text, opts = {}) {
526
526
  "The only authority on selection is HR.",
527
527
  };
528
528
  }
529
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_jd } from "./apply.js";
530
+ export async function fetchApplicationSchema(postId) {
531
+ const id = (postId ?? "").trim();
532
+ if (!id)
533
+ return { ok: false, source: "campus.jd.com", message: "post_id is required" };
534
+ let title = "";
535
+ let applyUrl = "https://campus.jd.com";
536
+ try {
537
+ const detail = (await fetchPositionDetail(id));
538
+ if (detail?.ok === false) {
539
+ return { ok: false, source: "campus.jd.com", message: detail.message ?? "post not found" };
540
+ }
541
+ title = detail?.title ?? "";
542
+ if (detail?.apply_url)
543
+ applyUrl = detail.apply_url;
544
+ }
545
+ catch { }
546
+ return {
547
+ ok: true,
548
+ schema: _buildBespokeApplySchema_jd({
549
+ source: "campus.jd.com",
550
+ postId: id,
551
+ jobTitle: title,
552
+ applyUrl,
553
+ submitEndpoint: "https://campus.jd.com/web/job/apply",
554
+ submitKind: "multipart-session",
555
+ submitNotes: "JD — POST /web/job/apply with session cookie. Endpoint inferred from campus.jd.com SPA; needs validation.",
556
+ }),
557
+ };
558
+ }
package/dist/kuaishou.js CHANGED
@@ -463,3 +463,33 @@ export async function matchResume(text, opts = {}) {
463
463
  "The only authority on selection is HR.",
464
464
  };
465
465
  }
466
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_kuaishou } from "./apply.js";
467
+ export async function fetchApplicationSchema(postId) {
468
+ const id = (postId ?? "").trim();
469
+ if (!id)
470
+ return { ok: false, source: "campus.kuaishou.cn", message: "post_id is required" };
471
+ let title = "";
472
+ let applyUrl = "https://campus.kuaishou.cn";
473
+ try {
474
+ const detail = (await fetchPositionDetail(id));
475
+ if (detail?.ok === false) {
476
+ return { ok: false, source: "campus.kuaishou.cn", message: detail.message ?? "post not found" };
477
+ }
478
+ title = detail?.title ?? "";
479
+ if (detail?.apply_url)
480
+ applyUrl = detail.apply_url;
481
+ }
482
+ catch { }
483
+ return {
484
+ ok: true,
485
+ schema: _buildBespokeApplySchema_kuaishou({
486
+ source: "campus.kuaishou.cn",
487
+ postId: id,
488
+ jobTitle: title,
489
+ applyUrl,
490
+ submitEndpoint: "https://campus.kuaishou.cn/rest/campus-recruit/post/deliver",
491
+ submitKind: "multipart-session",
492
+ submitNotes: "Kuaishou — POST /rest/campus-recruit/post/deliver with session cookie. Endpoint inferred; needs validation.",
493
+ }),
494
+ };
495
+ }
package/dist/liauto.js CHANGED
@@ -360,3 +360,33 @@ export async function matchResume(text, opts = {}) {
360
360
  "The only authority on selection is HR.",
361
361
  };
362
362
  }
363
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_liauto } from "./apply.js";
364
+ export async function fetchApplicationSchema(postId) {
365
+ const id = (postId ?? "").trim();
366
+ if (!id)
367
+ return { ok: false, source: "www.lixiang.com", message: "post_id is required" };
368
+ let title = "";
369
+ let applyUrl = "https://www.lixiang.com";
370
+ try {
371
+ const detail = (await fetchPositionDetail(id));
372
+ if (detail?.ok === false) {
373
+ return { ok: false, source: "www.lixiang.com", message: detail.message ?? "post not found" };
374
+ }
375
+ title = detail?.title ?? "";
376
+ if (detail?.apply_url)
377
+ applyUrl = detail.apply_url;
378
+ }
379
+ catch { }
380
+ return {
381
+ ok: true,
382
+ schema: _buildBespokeApplySchema_liauto({
383
+ source: "www.lixiang.com",
384
+ postId: id,
385
+ jobTitle: title,
386
+ applyUrl,
387
+ submitEndpoint: "https://www.lixiang.com/api/career/apply",
388
+ submitKind: "multipart-session",
389
+ submitNotes: "Li Auto — POST /api/career/apply with session cookie. Endpoint inferred; needs validation.",
390
+ }),
391
+ };
392
+ }
package/dist/meituan.js CHANGED
@@ -600,3 +600,33 @@ export async function matchResume(text, opts = {}) {
600
600
  "The only authority on selection is HR.",
601
601
  };
602
602
  }
603
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_meituan } from "./apply.js";
604
+ export async function fetchApplicationSchema(postId) {
605
+ const id = (postId ?? "").trim();
606
+ if (!id)
607
+ return { ok: false, source: "zhaopin.meituan.com", message: "post_id is required" };
608
+ let title = "";
609
+ let applyUrl = "https://zhaopin.meituan.com";
610
+ try {
611
+ const detail = (await fetchPositionDetail(id));
612
+ if (detail?.ok === false) {
613
+ return { ok: false, source: "zhaopin.meituan.com", message: detail.message ?? "post not found" };
614
+ }
615
+ title = detail?.title ?? "";
616
+ if (detail?.apply_url)
617
+ applyUrl = detail.apply_url;
618
+ }
619
+ catch { }
620
+ return {
621
+ ok: true,
622
+ schema: _buildBespokeApplySchema_meituan({
623
+ source: "zhaopin.meituan.com",
624
+ postId: id,
625
+ jobTitle: title,
626
+ applyUrl,
627
+ submitEndpoint: "https://zhaopin.meituan.com/api/job-apply",
628
+ submitKind: "multipart-session",
629
+ submitNotes: "Meituan — POST /api/job-apply with session cookie. Endpoint inferred; needs validation.",
630
+ }),
631
+ };
632
+ }
package/dist/mihoyo.js CHANGED
@@ -272,3 +272,33 @@ export async function matchResume(text, opts = {}) {
272
272
  matches: scored,
273
273
  };
274
274
  }
275
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_mihoyo } from "./apply.js";
276
+ export async function fetchApplicationSchema(postId) {
277
+ const id = (postId ?? "").trim();
278
+ if (!id)
279
+ return { ok: false, source: "ats.openout.mihoyo.com", message: "post_id is required" };
280
+ let title = "";
281
+ let applyUrl = "https://ats.openout.mihoyo.com";
282
+ try {
283
+ const detail = (await fetchPositionDetail(id));
284
+ if (detail?.ok === false) {
285
+ return { ok: false, source: "ats.openout.mihoyo.com", message: detail.message ?? "post not found" };
286
+ }
287
+ title = detail?.title ?? "";
288
+ if (detail?.apply_url)
289
+ applyUrl = detail.apply_url;
290
+ }
291
+ catch { }
292
+ return {
293
+ ok: true,
294
+ schema: _buildBespokeApplySchema_mihoyo({
295
+ source: "ats.openout.mihoyo.com",
296
+ postId: id,
297
+ jobTitle: title,
298
+ applyUrl,
299
+ submitEndpoint: "https://ats.openout.mihoyo.com/ats-portal/v1/application/create",
300
+ submitKind: "multipart-session",
301
+ submitNotes: "miHoYo — POST /ats-portal/v1/application/create with session cookie. Endpoint inferred from ats.openout.mihoyo.com SPA; needs validation.",
302
+ }),
303
+ };
304
+ }
package/dist/netease.js CHANGED
@@ -391,3 +391,33 @@ export async function matchResume(text, opts = {}) {
391
391
  "The only authority on selection is HR.",
392
392
  };
393
393
  }
394
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_netease } from "./apply.js";
395
+ export async function fetchApplicationSchema(postId) {
396
+ const id = (postId ?? "").trim();
397
+ if (!id)
398
+ return { ok: false, source: "hr.163.com", message: "post_id is required" };
399
+ let title = "";
400
+ let applyUrl = "https://hr.163.com";
401
+ try {
402
+ const detail = (await fetchPositionDetail(id));
403
+ if (detail?.ok === false) {
404
+ return { ok: false, source: "hr.163.com", message: detail.message ?? "post not found" };
405
+ }
406
+ title = detail?.title ?? "";
407
+ if (detail?.apply_url)
408
+ applyUrl = detail.apply_url;
409
+ }
410
+ catch { }
411
+ return {
412
+ ok: true,
413
+ schema: _buildBespokeApplySchema_netease({
414
+ source: "hr.163.com",
415
+ postId: id,
416
+ jobTitle: title,
417
+ applyUrl,
418
+ submitEndpoint: "https://hr.163.com/post-app/apply.do",
419
+ submitKind: "multipart-session",
420
+ submitNotes: "NetEase — POST /post-app/apply.do with session cookie. Endpoint inferred; needs validation.",
421
+ }),
422
+ };
423
+ }
package/dist/oppo.js CHANGED
@@ -248,3 +248,33 @@ export async function matchResume(text, opts = {}) {
248
248
  };
249
249
  }
250
250
  export { extractResumeSignals, scoreOverlap };
251
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_oppo } from "./apply.js";
252
+ export async function fetchApplicationSchema(postId) {
253
+ const id = (postId ?? "").trim();
254
+ if (!id)
255
+ return { ok: false, source: "careers.oppo.com", message: "post_id is required" };
256
+ let title = "";
257
+ let applyUrl = "https://careers.oppo.com";
258
+ try {
259
+ const detail = (await fetchPositionDetail(id));
260
+ if (detail?.ok === false) {
261
+ return { ok: false, source: "careers.oppo.com", message: detail.message ?? "post not found" };
262
+ }
263
+ title = detail?.title ?? "";
264
+ if (detail?.apply_url)
265
+ applyUrl = detail.apply_url;
266
+ }
267
+ catch { }
268
+ return {
269
+ ok: true,
270
+ schema: _buildBespokeApplySchema_oppo({
271
+ source: "careers.oppo.com",
272
+ postId: id,
273
+ jobTitle: title,
274
+ applyUrl,
275
+ submitEndpoint: "https://careers.oppo.com/openapi/position/apply",
276
+ submitKind: "multipart-session",
277
+ submitNotes: "OPPO — POST /openapi/position/apply with session cookie. Endpoint inferred from /openapi/position/pageNew read pattern; needs validation.",
278
+ }),
279
+ };
280
+ }
package/dist/pdd.js CHANGED
@@ -581,3 +581,33 @@ export async function matchResume(text, opts = {}) {
581
581
  };
582
582
  }
583
583
  export { extractResumeSignals, scoreOverlap };
584
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_pdd } from "./apply.js";
585
+ export async function fetchApplicationSchema(postId) {
586
+ const id = (postId ?? "").trim();
587
+ if (!id)
588
+ return { ok: false, source: "careers.pinduoduo.com", message: "post_id is required" };
589
+ let title = "";
590
+ let applyUrl = "https://careers.pinduoduo.com";
591
+ try {
592
+ const detail = (await fetchPositionDetail(id));
593
+ if (detail?.ok === false) {
594
+ return { ok: false, source: "careers.pinduoduo.com", message: detail.message ?? "post not found" };
595
+ }
596
+ title = detail?.title ?? "";
597
+ if (detail?.apply_url)
598
+ applyUrl = detail.apply_url;
599
+ }
600
+ catch { }
601
+ return {
602
+ ok: true,
603
+ schema: _buildBespokeApplySchema_pdd({
604
+ source: "careers.pinduoduo.com",
605
+ postId: id,
606
+ jobTitle: title,
607
+ applyUrl,
608
+ submitEndpoint: "https://careers.pinduoduo.com/api/recruit/v1/position/apply",
609
+ submitKind: "multipart-session",
610
+ submitNotes: "PDD — POST /api/recruit/v1/position/apply with session cookie. Endpoint inferred; needs validation.",
611
+ }),
612
+ };
613
+ }
package/dist/pingan.js CHANGED
@@ -460,3 +460,33 @@ export async function matchResume(text, opts = {}) {
460
460
  "The only authority on selection is HR.",
461
461
  };
462
462
  }
463
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_pingan } from "./apply.js";
464
+ export async function fetchApplicationSchema(postId) {
465
+ const id = (postId ?? "").trim();
466
+ if (!id)
467
+ return { ok: false, source: "campus.pingan.com", message: "post_id is required" };
468
+ let title = "";
469
+ let applyUrl = "https://campus.pingan.com";
470
+ try {
471
+ const detail = (await fetchPositionDetail(id));
472
+ if (detail?.ok === false) {
473
+ return { ok: false, source: "campus.pingan.com", message: detail.message ?? "post not found" };
474
+ }
475
+ title = detail?.title ?? "";
476
+ if (detail?.apply_url)
477
+ applyUrl = detail.apply_url;
478
+ }
479
+ catch { }
480
+ return {
481
+ ok: true,
482
+ schema: _buildBespokeApplySchema_pingan({
483
+ source: "campus.pingan.com",
484
+ postId: id,
485
+ jobTitle: title,
486
+ applyUrl,
487
+ submitEndpoint: "https://campus.pingan.com/recruit/api/applyJob",
488
+ submitKind: "multipart-session",
489
+ submitNotes: "Ping An — POST /recruit/api/applyJob with session cookie. Endpoint inferred; needs validation.",
490
+ }),
491
+ };
492
+ }
package/dist/sf.js CHANGED
@@ -276,3 +276,33 @@ export async function matchResume(text, opts = {}) {
276
276
  };
277
277
  }
278
278
  export { extractResumeSignals, scoreOverlap };
279
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_sf } from "./apply.js";
280
+ export async function fetchApplicationSchema(postId) {
281
+ const id = (postId ?? "").trim();
282
+ if (!id)
283
+ return { ok: false, source: "campus.sf-express.com", message: "post_id is required" };
284
+ let title = "";
285
+ let applyUrl = "https://campus.sf-express.com";
286
+ try {
287
+ const detail = (await fetchPositionDetail(id));
288
+ if (detail?.ok === false) {
289
+ return { ok: false, source: "campus.sf-express.com", message: detail.message ?? "post not found" };
290
+ }
291
+ title = detail?.title ?? "";
292
+ if (detail?.apply_url)
293
+ applyUrl = detail.apply_url;
294
+ }
295
+ catch { }
296
+ return {
297
+ ok: true,
298
+ schema: _buildBespokeApplySchema_sf({
299
+ source: "campus.sf-express.com",
300
+ postId: id,
301
+ jobTitle: title,
302
+ applyUrl,
303
+ submitEndpoint: "https://campus.sf-express.com/api/web/position/apply",
304
+ submitKind: "multipart-session",
305
+ submitNotes: "SF Express — POST /api/web/position/apply with cr-service header + GeeTest captcha + session cookie. Endpoint inferred; needs validation.",
306
+ }),
307
+ };
308
+ }
package/dist/tencent.js CHANGED
@@ -737,3 +737,33 @@ export function checkResume(text) {
737
737
  note: "Heuristics only — they don't judge content quality, just whether the skeleton is intact.",
738
738
  };
739
739
  }
740
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_tencent } from "./apply.js";
741
+ export async function fetchApplicationSchema(postId) {
742
+ const id = (postId ?? "").trim();
743
+ if (!id)
744
+ return { ok: false, source: "join.qq.com", message: "post_id is required" };
745
+ let title = "";
746
+ let applyUrl = "https://join.qq.com";
747
+ try {
748
+ const detail = (await fetchPositionDetail(id));
749
+ if (detail?.ok === false) {
750
+ return { ok: false, source: "join.qq.com", message: detail.message ?? "post not found" };
751
+ }
752
+ title = detail?.title ?? "";
753
+ if (detail?.apply_url)
754
+ applyUrl = detail.apply_url;
755
+ }
756
+ catch { }
757
+ return {
758
+ ok: true,
759
+ schema: _buildBespokeApplySchema_tencent({
760
+ source: "join.qq.com",
761
+ postId: id,
762
+ jobTitle: title,
763
+ applyUrl,
764
+ submitEndpoint: "https://join.qq.com/api/v1/position/applyResume",
765
+ submitKind: "multipart-session",
766
+ submitNotes: "Tencent join.qq.com — POST /api/v1/position/applyResume with session cookie + CSRF. Endpoint inferred from python-reference/tencent.py + WorkBuddy skill bundle; not yet verified against live submit.",
767
+ }),
768
+ };
769
+ }
package/dist/trip.js CHANGED
@@ -363,3 +363,33 @@ export async function matchResume(text, opts = {}) {
363
363
  }
364
364
  // Export helpers so other modules can import them from trip.js
365
365
  export { extractResumeSignals, scoreOverlap };
366
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_trip } from "./apply.js";
367
+ export async function fetchApplicationSchema(postId) {
368
+ const id = (postId ?? "").trim();
369
+ if (!id)
370
+ return { ok: false, source: "careers.ctrip.com", message: "post_id is required" };
371
+ let title = "";
372
+ let applyUrl = "https://careers.ctrip.com";
373
+ try {
374
+ const detail = (await fetchPositionDetail(id));
375
+ if (detail?.ok === false) {
376
+ return { ok: false, source: "careers.ctrip.com", message: detail.message ?? "post not found" };
377
+ }
378
+ title = detail?.title ?? "";
379
+ if (detail?.apply_url)
380
+ applyUrl = detail.apply_url;
381
+ }
382
+ catch { }
383
+ return {
384
+ ok: true,
385
+ schema: _buildBespokeApplySchema_trip({
386
+ source: "careers.ctrip.com",
387
+ postId: id,
388
+ jobTitle: title,
389
+ applyUrl,
390
+ submitEndpoint: "https://careers.ctrip.com/api/jobs/apply",
391
+ submitKind: "multipart-session",
392
+ submitNotes: "Trip.com — POST /api/jobs/apply with session cookie. Endpoint inferred; needs validation.",
393
+ }),
394
+ };
395
+ }
package/dist/unitree.js CHANGED
@@ -387,3 +387,33 @@ export async function matchResume(text, opts = {}) {
387
387
  };
388
388
  }
389
389
  export { extractResumeSignals, scoreOverlap };
390
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_unitree } from "./apply.js";
391
+ export async function fetchApplicationSchema(postId) {
392
+ const id = (postId ?? "").trim();
393
+ if (!id)
394
+ return { ok: false, source: "unitree.com", message: "post_id is required" };
395
+ let title = "";
396
+ let applyUrl = "https://unitree.com";
397
+ try {
398
+ const detail = (await fetchPositionDetail(id));
399
+ if (detail?.ok === false) {
400
+ return { ok: false, source: "unitree.com", message: detail.message ?? "post not found" };
401
+ }
402
+ title = detail?.title ?? "";
403
+ if (detail?.apply_url)
404
+ applyUrl = detail.apply_url;
405
+ }
406
+ catch { }
407
+ return {
408
+ ok: true,
409
+ schema: _buildBespokeApplySchema_unitree({
410
+ source: "unitree.com",
411
+ postId: id,
412
+ jobTitle: title,
413
+ applyUrl,
414
+ submitEndpoint: undefined,
415
+ submitKind: "external",
416
+ submitNotes: "Unitree — recruiting funnel runs through a WeChat mini-program; no public submit API. Open apply_url in browser to scan the WeChat QR.",
417
+ }),
418
+ };
419
+ }
package/dist/webank.js CHANGED
@@ -23,3 +23,33 @@ export const getNotice = adapter.getNotice;
23
23
  export const findNoticesByQuestion = adapter.findNoticesByQuestion;
24
24
  export const matchResume = adapter.matchResume;
25
25
  export const checkResume = adapter.checkResume;
26
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_webank } from "./apply.js";
27
+ export async function fetchApplicationSchema(postId) {
28
+ const id = (postId ?? "").trim();
29
+ if (!id)
30
+ return { ok: false, source: "webank.com (via api-c.liepin.com)", message: "post_id is required" };
31
+ let title = "";
32
+ let applyUrl = "https://webank.com";
33
+ try {
34
+ const detail = (await fetchPositionDetail(id));
35
+ if (detail?.ok === false) {
36
+ return { ok: false, source: "webank.com (via api-c.liepin.com)", message: detail.message ?? "post not found" };
37
+ }
38
+ title = detail?.title ?? "";
39
+ if (detail?.apply_url)
40
+ applyUrl = detail.apply_url;
41
+ }
42
+ catch { }
43
+ return {
44
+ ok: true,
45
+ schema: _buildBespokeApplySchema_webank({
46
+ source: "webank.com (via api-c.liepin.com)",
47
+ postId: id,
48
+ jobTitle: title,
49
+ applyUrl,
50
+ submitEndpoint: undefined,
51
+ submitKind: "external",
52
+ submitNotes: "WeBank (Liepin-backed) — submission is recruiter-IM-mediated through Liepin. Open the apply_url to start the chat.",
53
+ }),
54
+ };
55
+ }
package/dist/weibo.js CHANGED
@@ -304,3 +304,33 @@ export async function matchResume(text, opts = {}) {
304
304
  };
305
305
  }
306
306
  export { extractResumeSignals, scoreOverlap };
307
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_weibo } from "./apply.js";
308
+ export async function fetchApplicationSchema(postId) {
309
+ const id = (postId ?? "").trim();
310
+ if (!id)
311
+ return { ok: false, source: "career.sina.com.cn", message: "post_id is required" };
312
+ let title = "";
313
+ let applyUrl = "https://career.sina.com.cn";
314
+ try {
315
+ const detail = (await fetchPositionDetail(id));
316
+ if (detail?.ok === false) {
317
+ return { ok: false, source: "career.sina.com.cn", message: detail.message ?? "post not found" };
318
+ }
319
+ title = detail?.title ?? "";
320
+ if (detail?.apply_url)
321
+ applyUrl = detail.apply_url;
322
+ }
323
+ catch { }
324
+ return {
325
+ ok: true,
326
+ schema: _buildBespokeApplySchema_weibo({
327
+ source: "career.sina.com.cn",
328
+ postId: id,
329
+ jobTitle: title,
330
+ applyUrl,
331
+ submitEndpoint: "https://career.sina.com.cn/post/apply",
332
+ submitKind: "multipart-session",
333
+ submitNotes: "Weibo (Sina careers) — POST /post/apply with session cookie. Endpoint inferred; needs validation.",
334
+ }),
335
+ };
336
+ }
@@ -447,3 +447,33 @@ export async function matchResume(text, opts = {}) {
447
447
  "The only authority on selection is HR.",
448
448
  };
449
449
  }
450
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_xiaohongshu } from "./apply.js";
451
+ export async function fetchApplicationSchema(postId) {
452
+ const id = (postId ?? "").trim();
453
+ if (!id)
454
+ return { ok: false, source: "job.xiaohongshu.com", message: "post_id is required" };
455
+ let title = "";
456
+ let applyUrl = "https://job.xiaohongshu.com";
457
+ try {
458
+ const detail = (await fetchPositionDetail(id));
459
+ if (detail?.ok === false) {
460
+ return { ok: false, source: "job.xiaohongshu.com", message: detail.message ?? "post not found" };
461
+ }
462
+ title = detail?.title ?? "";
463
+ if (detail?.apply_url)
464
+ applyUrl = detail.apply_url;
465
+ }
466
+ catch { }
467
+ return {
468
+ ok: true,
469
+ schema: _buildBespokeApplySchema_xiaohongshu({
470
+ source: "job.xiaohongshu.com",
471
+ postId: id,
472
+ jobTitle: title,
473
+ applyUrl,
474
+ submitEndpoint: "https://job.xiaohongshu.com/api/recruit/apply",
475
+ submitKind: "multipart-session",
476
+ submitNotes: "Xiaohongshu — POST /api/recruit/apply with session cookie. Endpoint inferred; needs validation.",
477
+ }),
478
+ };
479
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-pro",
3
- "version": "0.9.4",
3
+ "version": "0.9.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",