job-pro 0.9.2 → 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/agibot.js +9 -0
- package/dist/apply.js +10 -0
- package/dist/baichuan.js +1 -0
- package/dist/cambricon.js +1 -0
- package/dist/deepseek.js +1 -0
- package/dist/feishu.js +129 -0
- package/dist/galaxyuniversal.js +1 -0
- package/dist/geely.js +1 -0
- package/dist/greenhouse.js +3 -0
- package/dist/horizonrobotics.js +1 -0
- package/dist/iflytek.js +38 -0
- package/dist/index.js +34 -12
- package/dist/iqiyi.js +9 -0
- package/dist/lever.js +4 -0
- package/dist/lilith.js +10 -0
- package/dist/megvii.js +1 -0
- package/dist/minimax.js +1 -1
- package/dist/moka.js +58 -0
- package/dist/moonshot.js +1 -0
- package/dist/nio.js +1 -1
- package/dist/sensetime.js +1 -0
- package/dist/stepfun.js +1 -0
- package/dist/vivo.js +38 -0
- package/dist/wecruit.js +49 -0
- package/dist/xiaomi.js +9 -0
- package/dist/zerooneai.js +1 -0
- package/dist/zhipu.js +9 -0
- package/package.json +1 -1
package/dist/agibot.js
CHANGED
|
@@ -388,3 +388,12 @@ export async function matchResume(text, opts = {}) {
|
|
|
388
388
|
"The only authority on selection is HR.",
|
|
389
389
|
};
|
|
390
390
|
}
|
|
391
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
392
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
393
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
394
|
+
host: "agirobot.jobs.feishu.cn",
|
|
395
|
+
source: "agirobot.jobs.feishu.cn",
|
|
396
|
+
channel: "campus",
|
|
397
|
+
applyUrlPrefix: "https://agirobot.jobs.feishu.cn/campus/position",
|
|
398
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
399
|
+
});
|
package/dist/apply.js
CHANGED
|
@@ -120,6 +120,8 @@ export function stageApplication(schema, profile) {
|
|
|
120
120
|
apply_url: schema.apply_url,
|
|
121
121
|
submit_endpoint: schema.submit_endpoint,
|
|
122
122
|
submit_method: schema.submit_method,
|
|
123
|
+
submit_kind: schema.submit_kind,
|
|
124
|
+
submit_notes: schema.submit_notes,
|
|
123
125
|
staged,
|
|
124
126
|
unanswered_required,
|
|
125
127
|
ready: unanswered_required.length === 0,
|
|
@@ -133,6 +135,14 @@ function resolveAnswer(field, profile) {
|
|
|
133
135
|
return { value: profile.first_name ?? "", reason: "profile.first_name missing" };
|
|
134
136
|
case "last_name":
|
|
135
137
|
return { value: profile.last_name ?? "", reason: "profile.last_name missing" };
|
|
138
|
+
case "name":
|
|
139
|
+
// Feishu / Beisen / Moka often use a single `name` field. Compose
|
|
140
|
+
// first + last; gracefully degrade if only one is set.
|
|
141
|
+
const composed = [profile.first_name, profile.last_name].filter(Boolean).join(" ");
|
|
142
|
+
return {
|
|
143
|
+
value: composed || profile.first_name || profile.last_name || "",
|
|
144
|
+
reason: "profile.first_name and last_name both missing",
|
|
145
|
+
};
|
|
136
146
|
case "email":
|
|
137
147
|
return { value: profile.email ?? "", reason: "profile.email missing" };
|
|
138
148
|
case "phone":
|
package/dist/baichuan.js
CHANGED
|
@@ -46,3 +46,4 @@ export const listNotices = _adapter.listNotices;
|
|
|
46
46
|
export const getNotice = _adapter.getNotice;
|
|
47
47
|
export const findNoticesByQuestion = _adapter.findNoticesByQuestion;
|
|
48
48
|
export const matchResume = _adapter.matchResume;
|
|
49
|
+
export const fetchApplicationSchema = _adapter.fetchApplicationSchema;
|
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;
|
package/dist/feishu.js
CHANGED
|
@@ -42,6 +42,85 @@
|
|
|
42
42
|
// - Both: city_info is null; city_list always populated
|
|
43
43
|
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
44
44
|
export { checkResume };
|
|
45
|
+
// ---------- shared apply-schema helper (re-used by bespoke Feishu adapters) ----------
|
|
46
|
+
//
|
|
47
|
+
// xiaomi.ts / zhipu.ts / iqiyi.ts / agibot.ts / lilith.ts each predate the
|
|
48
|
+
// factory and have their own searchPositions implementations. To give them
|
|
49
|
+
// the same Phase-2 behaviour as factory-using adapters (nio / minimax /
|
|
50
|
+
// baichuan / zerooneai), each can call `buildFeishuApplySchema()` from
|
|
51
|
+
// its own fetchApplicationSchema function.
|
|
52
|
+
/**
|
|
53
|
+
* Wire fetchApplicationSchema for a bespoke Feishu adapter that doesn't use
|
|
54
|
+
* createAdapter. The callback `fetchTitle(id)` is the adapter's own
|
|
55
|
+
* fetchPositionDetail (or any function that returns `{ ok, title }`).
|
|
56
|
+
*
|
|
57
|
+
* Usage:
|
|
58
|
+
* export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
59
|
+
* host: HOST, source: SOURCE, channel: CHANNEL,
|
|
60
|
+
* applyUrlPrefix: APPLY_PREFIX,
|
|
61
|
+
* fetchTitle: (id) => fetchPositionDetail(id),
|
|
62
|
+
* submitKind: "feishu-3-step", // override for lilith → "cdp-real-browser"
|
|
63
|
+
* });
|
|
64
|
+
*/
|
|
65
|
+
export function makeFeishuApplyFn(opts) {
|
|
66
|
+
return async function fetchApplicationSchema(postId) {
|
|
67
|
+
const id = (postId ?? "").trim();
|
|
68
|
+
if (!id)
|
|
69
|
+
return { ok: false, source: opts.source, message: "post_id is required" };
|
|
70
|
+
let title = "";
|
|
71
|
+
try {
|
|
72
|
+
const detail = (await opts.fetchTitle(id));
|
|
73
|
+
if (detail?.ok === false) {
|
|
74
|
+
return { ok: false, source: opts.source, message: detail.message ?? "post not found" };
|
|
75
|
+
}
|
|
76
|
+
title = detail?.title ?? "";
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// detail call failures aren't fatal for the schema — we can still
|
|
80
|
+
// return what we know.
|
|
81
|
+
}
|
|
82
|
+
const schema = buildFeishuApplySchema({
|
|
83
|
+
host: opts.host,
|
|
84
|
+
source: opts.source,
|
|
85
|
+
channel: opts.channel,
|
|
86
|
+
applyUrlPrefix: opts.applyUrlPrefix,
|
|
87
|
+
postId: id,
|
|
88
|
+
jobTitle: title,
|
|
89
|
+
});
|
|
90
|
+
if (opts.submitKind === "cdp-real-browser") {
|
|
91
|
+
schema.submit_kind = "cdp-real-browser";
|
|
92
|
+
schema.submit_notes =
|
|
93
|
+
"Lilith's Feishu tenant requires a runtime-minted `_signature` token. " +
|
|
94
|
+
"Submission must drive a real browser (puppeteer-core) — staged dry-run " +
|
|
95
|
+
"only for now.";
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, schema };
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function buildFeishuApplySchema(args) {
|
|
101
|
+
const standard = [
|
|
102
|
+
{ label: "Name", required: true, fields: [{ name: "name", type: "input_text" }] },
|
|
103
|
+
{ label: "Email", required: true, fields: [{ name: "email", type: "input_text" }] },
|
|
104
|
+
{ label: "Phone", required: true, fields: [{ name: "phone", type: "input_text" }] },
|
|
105
|
+
{ label: "Resume", required: true, fields: [{ name: "resume", type: "input_file" }] },
|
|
106
|
+
];
|
|
107
|
+
return {
|
|
108
|
+
source: args.source,
|
|
109
|
+
post_id: args.postId,
|
|
110
|
+
job_title: args.jobTitle,
|
|
111
|
+
apply_url: `${args.applyUrlPrefix}/${encodeURIComponent(args.postId)}/detail`,
|
|
112
|
+
submit_endpoint: `https://${args.host}/api/v1/resume/apply`,
|
|
113
|
+
submit_method: "POST",
|
|
114
|
+
submit_kind: "feishu-3-step",
|
|
115
|
+
submit_notes: "Feishu apply is a 3-step token flow: POST /api/v1/attachment/upload/tokens → " +
|
|
116
|
+
"PUT presigned URL on lf-package-cn.feishucdn.com → POST /api/v1/attachment/exchange/tokens → " +
|
|
117
|
+
"POST /api/v1/resume/apply with { post_id, attachment_id, applicant_info }. " +
|
|
118
|
+
"Requires candidate session cookies (capture via extension/, drop under " +
|
|
119
|
+
"~/.jobpro/<adapter>.session.json). Multi-step submitter wires in next iteration; " +
|
|
120
|
+
"today this schema is dry-run only.",
|
|
121
|
+
questions: standard,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
45
124
|
// ---------- createAdapter ----------
|
|
46
125
|
export function createAdapter(cfg) {
|
|
47
126
|
const API_ROOT = `https://${cfg.host}/api/v1`;
|
|
@@ -432,6 +511,55 @@ export function createAdapter(cfg) {
|
|
|
432
511
|
"The only authority on selection is HR.",
|
|
433
512
|
};
|
|
434
513
|
}
|
|
514
|
+
// ---------- fetchApplicationSchema (Phase 2) ----------
|
|
515
|
+
//
|
|
516
|
+
// Feishu's apply funnel is a 3-step token flow, not a single multipart
|
|
517
|
+
// POST. Discovered via JS-bundle inspection of nio.jobs.feishu.cn
|
|
518
|
+
// (lf-package-cn.feishucdn.com/obj/atsx-throne/hire-fe-prod/portal/
|
|
519
|
+
// saas-career/static/js/*.js) — the routes baked into the bundle are:
|
|
520
|
+
//
|
|
521
|
+
// 1. POST {API_ROOT}/attachment/upload/tokens
|
|
522
|
+
// → returns short-lived presigned upload URL + attachment_id
|
|
523
|
+
// 2. PUT <presigned-URL on lf-package-cn.feishucdn.com>
|
|
524
|
+
// → uploads the resume PDF/DOCX bytes directly
|
|
525
|
+
// 3. POST {API_ROOT}/attachment/exchange/tokens
|
|
526
|
+
// → exchanges short-lived id for a permanent attachment_id
|
|
527
|
+
// 4. POST {API_ROOT}/user/delivery/check (pre-flight, optional)
|
|
528
|
+
// 5. POST {API_ROOT}/resume/apply
|
|
529
|
+
// body: { post_id, attachment_id, applicant_info: { name, email,
|
|
530
|
+
// phone, ... }, ... }
|
|
531
|
+
// → returns { code:0, data:{ application_id } } on success
|
|
532
|
+
//
|
|
533
|
+
// The whole flow requires the user to be logged in as a candidate; the
|
|
534
|
+
// session cookie set during login authorizes every call above. Capture
|
|
535
|
+
// via the browser extension (~/.jobpro/<co>.session.json), then a
|
|
536
|
+
// future iteration adds an `executeSubmission` hook that drives the
|
|
537
|
+
// 3-step flow with the captured cookies.
|
|
538
|
+
//
|
|
539
|
+
// For now `fetchApplicationSchema` returns the contact-info schema
|
|
540
|
+
// (sufficient for dry-run staging) plus `submit_kind: "feishu-3-step"`
|
|
541
|
+
// so the dispatcher refuses --really-submit with a useful pointer.
|
|
542
|
+
async function fetchApplicationSchema(postId) {
|
|
543
|
+
const id = (postId ?? "").trim();
|
|
544
|
+
if (!id)
|
|
545
|
+
return { ok: false, source, message: "post_id is required" };
|
|
546
|
+
const detail = await fetchPositionDetail(id);
|
|
547
|
+
const detailAny = detail;
|
|
548
|
+
if (!detailAny.ok) {
|
|
549
|
+
return { ok: false, source, message: detailAny.message ?? "post not found" };
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
ok: true,
|
|
553
|
+
schema: buildFeishuApplySchema({
|
|
554
|
+
host: cfg.host,
|
|
555
|
+
source,
|
|
556
|
+
channel: cfg.channel,
|
|
557
|
+
applyUrlPrefix: cfg.applyUrlPrefix,
|
|
558
|
+
postId: id,
|
|
559
|
+
jobTitle: detailAny.title ?? "",
|
|
560
|
+
}),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
435
563
|
return {
|
|
436
564
|
searchPositions,
|
|
437
565
|
fetchAllPositions,
|
|
@@ -442,5 +570,6 @@ export function createAdapter(cfg) {
|
|
|
442
570
|
findNoticesByQuestion,
|
|
443
571
|
matchResume,
|
|
444
572
|
checkResume,
|
|
573
|
+
fetchApplicationSchema,
|
|
445
574
|
};
|
|
446
575
|
}
|
package/dist/galaxyuniversal.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/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;
|
package/dist/greenhouse.js
CHANGED
|
@@ -409,6 +409,9 @@ export function createAdapter(cfg) {
|
|
|
409
409
|
apply_url: job.absolute_url ?? `${BOARD_URL}/jobs/${id}`,
|
|
410
410
|
submit_endpoint: `${API_ROOT}/jobs/${encodeURIComponent(id)}`,
|
|
411
411
|
submit_method: "POST",
|
|
412
|
+
submit_kind: "multipart-anon",
|
|
413
|
+
submit_notes: "Greenhouse Job Board API accepts anonymous multipart/form-data POSTs " +
|
|
414
|
+
"whose field names match the questions[].fields[].name returned here.",
|
|
412
415
|
questions,
|
|
413
416
|
},
|
|
414
417
|
};
|
package/dist/horizonrobotics.js
CHANGED
|
@@ -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.
|
|
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.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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)
|
|
@@ -460,12 +462,32 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
460
462
|
message: `${staged.unanswered_required.length} required field(s) still unanswered; refusing to submit incomplete application`,
|
|
461
463
|
}, compact);
|
|
462
464
|
}
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
465
|
+
// Submission flow selection by submit_kind. Only the generic
|
|
466
|
+
// multipart families are wired to actually fire today; everything
|
|
467
|
+
// else gets a useful refusal message.
|
|
468
|
+
const kind = (sr.schema.submit_kind ?? "multipart-anon");
|
|
469
|
+
const isAnonMultipart = kind === "multipart-anon";
|
|
470
|
+
const isSessionMultipart = kind === "multipart-session";
|
|
471
|
+
const isGenericMultipart = isAnonMultipart || isSessionMultipart;
|
|
472
|
+
if (!isGenericMultipart) {
|
|
473
|
+
return emit({
|
|
474
|
+
ok: false,
|
|
475
|
+
source: company,
|
|
476
|
+
post_id: postId,
|
|
477
|
+
mode: "really-submit-blocked",
|
|
478
|
+
staged,
|
|
479
|
+
submit_kind: kind,
|
|
480
|
+
submit_notes: sr.schema.submit_notes,
|
|
481
|
+
message: `submit_kind="${kind}" — this adapter family doesn't yet have an ` +
|
|
482
|
+
`executor wired. The application schema + submit endpoint are ` +
|
|
483
|
+
`documented (see submit_notes), but firing the submission needs a ` +
|
|
484
|
+
`family-specific multi-step flow (token exchange / AES envelope / ` +
|
|
485
|
+
`CDP / etc.). Landing per-family executors is the next iteration of ` +
|
|
486
|
+
`Phase 2. Use --debug-submit-to <url> to inspect what we have today.`,
|
|
487
|
+
}, compact);
|
|
488
|
+
}
|
|
489
|
+
// Non-anon multipart families need session.json.
|
|
490
|
+
if (!isAnonMultipart && !session) {
|
|
469
491
|
return emit({
|
|
470
492
|
ok: false,
|
|
471
493
|
source: company,
|
|
@@ -478,7 +500,7 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
478
500
|
}, compact);
|
|
479
501
|
}
|
|
480
502
|
const result = await submitApplication(staged, { kind: "upstream" }, { session });
|
|
481
|
-
return emit({ mode: "really-submit", staged, session_used: !!session, result }, compact);
|
|
503
|
+
return emit({ mode: "really-submit", staged, submit_kind: kind, session_used: !!session, result }, compact);
|
|
482
504
|
}
|
|
483
505
|
// Default: dry-run print, no network.
|
|
484
506
|
if (compact) {
|
package/dist/iqiyi.js
CHANGED
|
@@ -483,3 +483,12 @@ export async function matchResume(text, opts = {}) {
|
|
|
483
483
|
"The only authority on selection is HR.",
|
|
484
484
|
};
|
|
485
485
|
}
|
|
486
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
487
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
488
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
489
|
+
host: "careers.iqiyi.com",
|
|
490
|
+
source: "careers.iqiyi.com",
|
|
491
|
+
channel: "campus",
|
|
492
|
+
applyUrlPrefix: "https://careers.iqiyi.com/campus/position",
|
|
493
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
494
|
+
});
|
package/dist/lever.js
CHANGED
|
@@ -431,6 +431,10 @@ export function createAdapter(cfg) {
|
|
|
431
431
|
apply_url: job.applyUrl ?? job.hostedUrl ?? `${BOARD_URL}/${id}/apply`,
|
|
432
432
|
submit_endpoint: `${BOARD_URL}/${id}/apply`,
|
|
433
433
|
submit_method: "POST",
|
|
434
|
+
submit_kind: "multipart-anon",
|
|
435
|
+
submit_notes: "Lever apply-page accepts anonymous multipart/form-data POST whose field " +
|
|
436
|
+
"names match Lever's hosted apply form (standard contact-info + each " +
|
|
437
|
+
"customQuestion's auto-named field).",
|
|
434
438
|
questions: [...standard, ...custom],
|
|
435
439
|
},
|
|
436
440
|
};
|
package/dist/lilith.js
CHANGED
|
@@ -288,3 +288,13 @@ export async function matchResume(text, opts = {}) {
|
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
export { extractResumeSignals, scoreOverlap };
|
|
291
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
292
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
293
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
294
|
+
host: "lilithgames.jobs.feishu.cn",
|
|
295
|
+
source: "lilithgames.jobs.feishu.cn",
|
|
296
|
+
channel: "career",
|
|
297
|
+
applyUrlPrefix: "https://lilithgames.jobs.feishu.cn/career/position",
|
|
298
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
299
|
+
submitKind: "cdp-real-browser",
|
|
300
|
+
});
|
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/minimax.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
//
|
|
25
25
|
// apply_url pattern: https://vrfi1sk8a0.jobs.feishu.cn/379481/position/<id>/detail
|
|
26
26
|
import { createAdapter } from "./feishu.js";
|
|
27
|
-
export const { searchPositions, fetchAllPositions, fetchPositionDetail, fetchDictionaries, listNotices, getNotice, findNoticesByQuestion, matchResume, checkResume, } = createAdapter({
|
|
27
|
+
export const { searchPositions, fetchAllPositions, fetchPositionDetail, fetchDictionaries, listNotices, getNotice, findNoticesByQuestion, matchResume, checkResume, fetchApplicationSchema, } = createAdapter({
|
|
28
28
|
host: "vrfi1sk8a0.jobs.feishu.cn",
|
|
29
29
|
channel: "379481",
|
|
30
30
|
label: "MiniMax / MiniMax智能",
|
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/nio.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
//
|
|
17
17
|
// apply_url pattern: https://nio.jobs.feishu.cn/campus/position/<id>/detail
|
|
18
18
|
import { createAdapter } from "./feishu.js";
|
|
19
|
-
export const { searchPositions, fetchAllPositions, fetchPositionDetail, fetchDictionaries, listNotices, getNotice, findNoticesByQuestion, matchResume, checkResume, } = createAdapter({
|
|
19
|
+
export const { searchPositions, fetchAllPositions, fetchPositionDetail, fetchDictionaries, listNotices, getNotice, findNoticesByQuestion, matchResume, checkResume, fetchApplicationSchema, } = createAdapter({
|
|
20
20
|
host: "nio.jobs.feishu.cn",
|
|
21
21
|
channel: "campus",
|
|
22
22
|
label: "NIO / 蔚来",
|
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/dist/xiaomi.js
CHANGED
|
@@ -518,3 +518,12 @@ export async function matchResume(text, opts = {}) {
|
|
|
518
518
|
"The only authority on selection is HR.",
|
|
519
519
|
};
|
|
520
520
|
}
|
|
521
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
522
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
523
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
524
|
+
host: "xiaomi.jobs.f.mioffice.cn",
|
|
525
|
+
source: "xiaomi.jobs.f.mioffice.cn",
|
|
526
|
+
channel: "campus",
|
|
527
|
+
applyUrlPrefix: "https://xiaomi.jobs.f.mioffice.cn/campus/position",
|
|
528
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
529
|
+
});
|
package/dist/zerooneai.js
CHANGED
|
@@ -39,3 +39,4 @@ export const listNotices = _adapter.listNotices;
|
|
|
39
39
|
export const getNotice = _adapter.getNotice;
|
|
40
40
|
export const findNoticesByQuestion = _adapter.findNoticesByQuestion;
|
|
41
41
|
export const matchResume = _adapter.matchResume;
|
|
42
|
+
export const fetchApplicationSchema = _adapter.fetchApplicationSchema;
|
package/dist/zhipu.js
CHANGED
|
@@ -467,3 +467,12 @@ export async function matchResume(text, opts = {}) {
|
|
|
467
467
|
"The only authority on selection is HR.",
|
|
468
468
|
};
|
|
469
469
|
}
|
|
470
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
471
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
472
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
473
|
+
host: "zhipu-ai.jobs.feishu.cn",
|
|
474
|
+
source: "zhipu-ai.jobs.feishu.cn",
|
|
475
|
+
channel: "index",
|
|
476
|
+
applyUrlPrefix: "https://zhipu-ai.jobs.feishu.cn/index/position",
|
|
477
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
478
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-pro",
|
|
3
|
-
"version": "0.9.
|
|
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",
|