job-pro 0.8.2 → 0.9.0
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 +166 -0
- package/dist/greenhouse.js +58 -0
- package/dist/hoyoverse.js +1 -0
- package/dist/index.js +96 -1
- package/dist/lever.js +75 -0
- package/dist/weride.js +1 -0
- package/dist/xpeng.js +1 -0
- package/package.json +1 -1
package/dist/apply.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Phase 2 — auto-apply infrastructure.
|
|
2
|
+
//
|
|
3
|
+
// This module is intentionally read-only (dry-run) right now. The user
|
|
4
|
+
// runs `job-pro <co> apply <postId>` and gets a fully-staged POST payload
|
|
5
|
+
// printed to stdout. Actually firing the submission ("--really-submit")
|
|
6
|
+
// is guarded: each adapter family must opt in by exporting an
|
|
7
|
+
// `executeApplication` function. Out of the 50 adapters, only a handful
|
|
8
|
+
// (Greenhouse boards / Lever boards) have well-documented public
|
|
9
|
+
// submission APIs; the rest need session capture (Phase 2.1, separate
|
|
10
|
+
// release).
|
|
11
|
+
//
|
|
12
|
+
// Profile shape — loaded from `~/.jobpro/profile.json` or via flags.
|
|
13
|
+
// Fields beyond first_name / last_name / email / phone / resume are
|
|
14
|
+
// passed through to whatever per-company custom question matches their
|
|
15
|
+
// `name` (e.g. `linkedin_url`, `nationality`).
|
|
16
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
const PROFILE_PATH = process.env.JOB_PRO_PROFILE_PATH ?? join(homedir(), ".jobpro", "profile.json");
|
|
20
|
+
const TEMPLATE = {
|
|
21
|
+
first_name: "",
|
|
22
|
+
last_name: "",
|
|
23
|
+
email: "",
|
|
24
|
+
phone: "",
|
|
25
|
+
resume_path: "",
|
|
26
|
+
cover_letter_text: "",
|
|
27
|
+
custom: {
|
|
28
|
+
// Common Greenhouse / Lever questions:
|
|
29
|
+
// question_<n>: "answer"
|
|
30
|
+
// linkedin_url: "https://www.linkedin.com/in/your-handle",
|
|
31
|
+
// nationality: "China",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export function loadProfile() {
|
|
35
|
+
if (!existsSync(PROFILE_PATH)) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
message: `profile not found at ${PROFILE_PATH}. Run \`job-pro profile init\` to create a template, ` +
|
|
39
|
+
`or set $JOB_PRO_PROFILE_PATH to override.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = readFileSync(PROFILE_PATH, "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return { ok: false, message: `could not read ${PROFILE_PATH}: ${err instanceof Error ? err.message : err}` };
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(raw);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return { ok: false, message: `${PROFILE_PATH} is not valid JSON: ${err instanceof Error ? err.message : err}` };
|
|
55
|
+
}
|
|
56
|
+
for (const required of ["first_name", "last_name", "email", "phone"]) {
|
|
57
|
+
if (!parsed[required]) {
|
|
58
|
+
return { ok: false, message: `${PROFILE_PATH}: missing required field "${required}"` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, profile: parsed };
|
|
62
|
+
}
|
|
63
|
+
export function profileTemplate() {
|
|
64
|
+
return { path: PROFILE_PATH, template: TEMPLATE };
|
|
65
|
+
}
|
|
66
|
+
/** Fill in known answers from the profile; flag any unanswered required fields. */
|
|
67
|
+
export function stageApplication(schema, profile) {
|
|
68
|
+
const staged = [];
|
|
69
|
+
const unanswered_required = [];
|
|
70
|
+
for (const q of schema.questions) {
|
|
71
|
+
// The "primary" field is the first one; secondary fields are alternate
|
|
72
|
+
// formats (e.g. resume has both `resume` file + `resume_text` textarea).
|
|
73
|
+
const primary = q.fields[0];
|
|
74
|
+
if (!primary)
|
|
75
|
+
continue;
|
|
76
|
+
const filled = resolveAnswer(primary, profile);
|
|
77
|
+
const reason = filled.value || !q.required ? undefined : filled.reason;
|
|
78
|
+
const sf = {
|
|
79
|
+
name: primary.name,
|
|
80
|
+
type: primary.type,
|
|
81
|
+
value: filled.value,
|
|
82
|
+
required: q.required,
|
|
83
|
+
unanswered_reason: reason,
|
|
84
|
+
};
|
|
85
|
+
staged.push(sf);
|
|
86
|
+
if (q.required && !filled.value)
|
|
87
|
+
unanswered_required.push(sf);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
source: schema.source,
|
|
91
|
+
post_id: schema.post_id,
|
|
92
|
+
job_title: schema.job_title,
|
|
93
|
+
apply_url: schema.apply_url,
|
|
94
|
+
submit_endpoint: schema.submit_endpoint,
|
|
95
|
+
submit_method: schema.submit_method,
|
|
96
|
+
staged,
|
|
97
|
+
unanswered_required,
|
|
98
|
+
ready: unanswered_required.length === 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function resolveAnswer(field, profile) {
|
|
102
|
+
// Hard-coded standard mappings — these names are the canonical
|
|
103
|
+
// Greenhouse field names and are reused by Lever's submission form.
|
|
104
|
+
switch (field.name) {
|
|
105
|
+
case "first_name":
|
|
106
|
+
return { value: profile.first_name ?? "", reason: "profile.first_name missing" };
|
|
107
|
+
case "last_name":
|
|
108
|
+
return { value: profile.last_name ?? "", reason: "profile.last_name missing" };
|
|
109
|
+
case "email":
|
|
110
|
+
return { value: profile.email ?? "", reason: "profile.email missing" };
|
|
111
|
+
case "phone":
|
|
112
|
+
return { value: profile.phone ?? "", reason: "profile.phone missing" };
|
|
113
|
+
case "resume":
|
|
114
|
+
return {
|
|
115
|
+
value: profile.resume_path ?? "",
|
|
116
|
+
reason: "profile.resume_path missing — set to an absolute PDF/DOCX path",
|
|
117
|
+
};
|
|
118
|
+
case "resume_text":
|
|
119
|
+
// Optional companion field — leave empty if user supplies a file.
|
|
120
|
+
return { value: "", reason: "" };
|
|
121
|
+
case "cover_letter":
|
|
122
|
+
return { value: "", reason: "" };
|
|
123
|
+
case "cover_letter_text":
|
|
124
|
+
return { value: profile.cover_letter_text ?? "", reason: "" };
|
|
125
|
+
default:
|
|
126
|
+
// Custom passthroughs — match by question name (e.g. "question_36528765002").
|
|
127
|
+
const v = profile.custom?.[field.name];
|
|
128
|
+
if (typeof v === "string" && v.length > 0)
|
|
129
|
+
return { value: v, reason: "" };
|
|
130
|
+
return {
|
|
131
|
+
value: "",
|
|
132
|
+
reason: `unknown field "${field.name}" — add to profile.custom.${field.name} to auto-fill`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ---------- pretty-print for dry-run ----------
|
|
137
|
+
export function formatStaged(s) {
|
|
138
|
+
const lines = [];
|
|
139
|
+
lines.push(`source: ${s.source}`);
|
|
140
|
+
lines.push(`job: ${s.post_id} — ${s.job_title}`);
|
|
141
|
+
lines.push(`apply_url: ${s.apply_url}`);
|
|
142
|
+
if (s.submit_endpoint) {
|
|
143
|
+
lines.push(`submit: ${s.submit_method ?? "POST"} ${s.submit_endpoint}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push(`ready: ${s.ready ? "✓ all required fields filled" : `✗ ${s.unanswered_required.length} required field(s) unfilled`}`);
|
|
147
|
+
lines.push("");
|
|
148
|
+
lines.push("Staged payload:");
|
|
149
|
+
const widthName = Math.max(...s.staged.map((f) => f.name.length));
|
|
150
|
+
const widthType = Math.max(...s.staged.map((f) => f.type.length));
|
|
151
|
+
for (const f of s.staged) {
|
|
152
|
+
const flag = f.required ? "•" : " ";
|
|
153
|
+
const value = f.value
|
|
154
|
+
? f.type === "input_file"
|
|
155
|
+
? `<file: ${f.value}>`
|
|
156
|
+
: truncate(f.value, 60)
|
|
157
|
+
: f.unanswered_reason
|
|
158
|
+
? `<unanswered: ${f.unanswered_reason}>`
|
|
159
|
+
: "<empty>";
|
|
160
|
+
lines.push(` ${flag} ${f.name.padEnd(widthName)} ${f.type.padEnd(widthType)} ${value}`);
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
function truncate(s, n) {
|
|
165
|
+
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
166
|
+
}
|
package/dist/greenhouse.js
CHANGED
|
@@ -356,6 +356,63 @@ export function createAdapter(cfg) {
|
|
|
356
356
|
"The only authority on selection is HR.",
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
|
+
async function fetchApplicationSchema(postId) {
|
|
360
|
+
const id = (postId ?? "").trim();
|
|
361
|
+
if (!id)
|
|
362
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
363
|
+
const url = `${API_ROOT}/jobs/${encodeURIComponent(id)}?questions=true`;
|
|
364
|
+
let response;
|
|
365
|
+
try {
|
|
366
|
+
response = await fetch(url, { headers: HEADERS });
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
source: SOURCE,
|
|
372
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (!response.ok) {
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
source: SOURCE,
|
|
379
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
let job;
|
|
383
|
+
try {
|
|
384
|
+
job = (await response.json());
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
source: SOURCE,
|
|
390
|
+
message: `bad JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const questions = (job.questions ?? []).map((q) => ({
|
|
394
|
+
label: q.label ?? "",
|
|
395
|
+
description: q.description ?? null,
|
|
396
|
+
required: q.required ?? false,
|
|
397
|
+
fields: (q.fields ?? []).map((f) => ({
|
|
398
|
+
name: f.name ?? "",
|
|
399
|
+
type: f.type ?? "input_text",
|
|
400
|
+
values: (f.values ?? []).map((v) => ({ value: v.value ?? "", label: v.label ?? "" })),
|
|
401
|
+
})),
|
|
402
|
+
}));
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
schema: {
|
|
406
|
+
source: SOURCE,
|
|
407
|
+
post_id: id,
|
|
408
|
+
job_title: job.title ?? "",
|
|
409
|
+
apply_url: job.absolute_url ?? `${BOARD_URL}/jobs/${id}`,
|
|
410
|
+
submit_endpoint: `${API_ROOT}/jobs/${encodeURIComponent(id)}`,
|
|
411
|
+
submit_method: "POST",
|
|
412
|
+
questions,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
359
416
|
return {
|
|
360
417
|
searchPositions,
|
|
361
418
|
fetchAllPositions,
|
|
@@ -366,5 +423,6 @@ export function createAdapter(cfg) {
|
|
|
366
423
|
findNoticesByQuestion,
|
|
367
424
|
matchResume,
|
|
368
425
|
checkResume,
|
|
426
|
+
fetchApplicationSchema,
|
|
369
427
|
};
|
|
370
428
|
}
|
package/dist/hoyoverse.js
CHANGED
|
@@ -23,3 +23,4 @@ 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
|
+
export const fetchApplicationSchema = adapter.fetchApplicationSchema;
|
package/dist/index.js
CHANGED
|
@@ -50,8 +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, profileTemplate, stageApplication, formatStaged, } from "./apply.js";
|
|
53
54
|
import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
|
|
54
|
-
|
|
55
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
56
|
+
import { dirname } from "node:path";
|
|
57
|
+
const VERSION = "0.9.0";
|
|
55
58
|
const COMPANIES = [
|
|
56
59
|
{ key: "tencent", family: "Bespoke", source: "join.qq.com", label: "Tencent / 腾讯" },
|
|
57
60
|
{ key: "bytedance", family: "Bespoke", source: "jobs.bytedance.com", label: "ByteDance / 字节跳动" },
|
|
@@ -111,6 +114,8 @@ job-pro — query Chinese big-tech campus recruiting from your terminal
|
|
|
111
114
|
USAGE
|
|
112
115
|
job-pro <company> <verb> [options]
|
|
113
116
|
job-pro list [--compact] list all 50 companies + source family
|
|
117
|
+
job-pro profile init [--force] write ~/.jobpro/profile.json template
|
|
118
|
+
job-pro profile show print the loaded profile
|
|
114
119
|
job-pro --version
|
|
115
120
|
job-pro help
|
|
116
121
|
|
|
@@ -118,6 +123,12 @@ USAGE
|
|
|
118
123
|
by ATS family (Bespoke / Feishu / Beisen Wecruit / Beisen iTalent / Moka
|
|
119
124
|
/ Greenhouse-Lever / Liepin). Coverage summary at job.ha7ch.com.
|
|
120
125
|
|
|
126
|
+
PHASE 2 (auto-apply) is in early access. \`job-pro <co> apply <postId>\`
|
|
127
|
+
prints the staged POST in dry-run mode. Today only Greenhouse +
|
|
128
|
+
Lever boards (xpeng / hoyoverse / weride) expose the application
|
|
129
|
+
schema; the rest return a "not yet wired" note. See docs/auto-apply.md
|
|
130
|
+
for the rollout plan.
|
|
131
|
+
|
|
121
132
|
VERBS (same surface for every company)
|
|
122
133
|
search <kw> search openings (free text)
|
|
123
134
|
detail <post_id> show full JD for one job
|
|
@@ -129,6 +140,9 @@ VERBS (same surface for every company)
|
|
|
129
140
|
match <resume-text-or--> rank jobs by overlap with resume text
|
|
130
141
|
pass "-" to read resume from stdin
|
|
131
142
|
resume-check <resume-text-or--> structural sanity check on a resume
|
|
143
|
+
apply <post_id> stage an application (Phase 2 dry-run)
|
|
144
|
+
--really-submit is intentionally disabled
|
|
145
|
+
until per-ATS flows are validated.
|
|
132
146
|
memory list | get <k> | set k=v | event <kind> [payload] | clear
|
|
133
147
|
|
|
134
148
|
OUTPUT
|
|
@@ -375,6 +389,63 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
375
389
|
const text = readResumeArg(args[0]);
|
|
376
390
|
return emit(adapter.checkResume(text), compact);
|
|
377
391
|
}
|
|
392
|
+
if (verb === "apply") {
|
|
393
|
+
const postId = args[0];
|
|
394
|
+
if (!postId)
|
|
395
|
+
die(`usage: job-pro ${company} apply <post_id> [--dry-run | --really-submit]`);
|
|
396
|
+
const reallySubmit = args.includes("--really-submit");
|
|
397
|
+
const fetchSchema = adapter.fetchApplicationSchema;
|
|
398
|
+
if (typeof fetchSchema !== "function") {
|
|
399
|
+
return emit({
|
|
400
|
+
ok: false,
|
|
401
|
+
source: company,
|
|
402
|
+
post_id: postId,
|
|
403
|
+
message: `apply: Phase 2 not yet wired for "${company}". Only Greenhouse + Lever ` +
|
|
404
|
+
`boards (xpeng / weride / hoyoverse) expose an application schema today. ` +
|
|
405
|
+
`See docs/auto-apply.md for the rollout plan.`,
|
|
406
|
+
}, compact);
|
|
407
|
+
}
|
|
408
|
+
if (reallySubmit) {
|
|
409
|
+
return emit({
|
|
410
|
+
ok: false,
|
|
411
|
+
source: company,
|
|
412
|
+
post_id: postId,
|
|
413
|
+
message: `--really-submit is intentionally not implemented yet. Phase 2 ships the ` +
|
|
414
|
+
`staging path first so you can see exactly what would be POSTed before any ` +
|
|
415
|
+
`submission actually fires. Re-run without --really-submit for the dry-run.`,
|
|
416
|
+
}, compact);
|
|
417
|
+
}
|
|
418
|
+
const schemaResult = await fetchSchema.call(adapter, postId);
|
|
419
|
+
const sr = schemaResult;
|
|
420
|
+
if (!sr.ok || !sr.schema) {
|
|
421
|
+
return emit({ ok: false, source: company, post_id: postId, message: sr.message ?? "unknown error" }, compact);
|
|
422
|
+
}
|
|
423
|
+
const prof = loadProfile();
|
|
424
|
+
if (!prof.ok) {
|
|
425
|
+
return emit({
|
|
426
|
+
ok: false,
|
|
427
|
+
source: company,
|
|
428
|
+
post_id: postId,
|
|
429
|
+
schema: sr.schema,
|
|
430
|
+
message: prof.message,
|
|
431
|
+
hint: `run \`job-pro profile init\` to create a template.`,
|
|
432
|
+
}, compact);
|
|
433
|
+
}
|
|
434
|
+
const staged = stageApplication(sr.schema, prof.profile);
|
|
435
|
+
if (compact) {
|
|
436
|
+
return emit({ ok: true, staged }, compact);
|
|
437
|
+
}
|
|
438
|
+
console.log(formatStaged(staged));
|
|
439
|
+
if (!staged.ready) {
|
|
440
|
+
console.log(`\nFill the unanswered required fields in ${profileTemplate().path} ` +
|
|
441
|
+
`(profile.custom.<name> for unknown fields), then re-run.`);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
console.log(`\nDry-run complete. --really-submit will be enabled in a future release ` +
|
|
445
|
+
`once per-ATS submission flows have been validated against live boards.`);
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
378
449
|
if (verb === "memory") {
|
|
379
450
|
const [sub, ...subArgs] = args;
|
|
380
451
|
if (!sub)
|
|
@@ -463,6 +534,30 @@ async function main() {
|
|
|
463
534
|
printCompanyList(compact);
|
|
464
535
|
return;
|
|
465
536
|
}
|
|
537
|
+
if (cmd === "profile") {
|
|
538
|
+
const sub = args[1];
|
|
539
|
+
if (sub === "init") {
|
|
540
|
+
const { path, template } = profileTemplate();
|
|
541
|
+
if (existsSync(path) && !args.includes("--force")) {
|
|
542
|
+
console.error(`profile already exists at ${path}; pass --force to overwrite.`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
546
|
+
writeFileSync(path, JSON.stringify(template, null, 2) + "\n", "utf8");
|
|
547
|
+
console.log(`Wrote ${path}. Fill in first_name / last_name / email / phone / resume_path before running \`job-pro <co> apply\`.`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (sub === "show") {
|
|
551
|
+
const r = loadProfile();
|
|
552
|
+
if (!r.ok) {
|
|
553
|
+
console.error(r.message);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
console.log(JSON.stringify(r.profile, null, 2));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
die(`usage: job-pro profile <init [--force] | show>`);
|
|
560
|
+
}
|
|
466
561
|
const adapter = ADAPTERS[cmd];
|
|
467
562
|
if (adapter) {
|
|
468
563
|
await runCompany(adapter, cmd, args.slice(1));
|
package/dist/lever.js
CHANGED
|
@@ -361,6 +361,80 @@ export function createAdapter(cfg) {
|
|
|
361
361
|
"The only authority on selection is HR.",
|
|
362
362
|
};
|
|
363
363
|
}
|
|
364
|
+
async function fetchApplicationSchema(postId) {
|
|
365
|
+
const id = (postId ?? "").trim();
|
|
366
|
+
if (!id)
|
|
367
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
368
|
+
let response;
|
|
369
|
+
try {
|
|
370
|
+
response = await fetch(API_DETAIL(id), { headers: HEADERS });
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
source: SOURCE,
|
|
376
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (!response.ok) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
source: SOURCE,
|
|
383
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
let job;
|
|
387
|
+
try {
|
|
388
|
+
job = (await response.json());
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
source: SOURCE,
|
|
394
|
+
message: `bad JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// Lever's standard contact-info block.
|
|
398
|
+
const standard = [
|
|
399
|
+
{ label: "First Name", required: true, fields: [{ name: "first_name", type: "input_text" }] },
|
|
400
|
+
{ label: "Last Name", required: true, fields: [{ name: "last_name", type: "input_text" }] },
|
|
401
|
+
{ label: "Email", required: true, fields: [{ name: "email", type: "input_text" }] },
|
|
402
|
+
{ label: "Phone", required: true, fields: [{ name: "phone", type: "input_text" }] },
|
|
403
|
+
{ label: "Resume", required: true, fields: [{ name: "resume", type: "input_file" }] },
|
|
404
|
+
];
|
|
405
|
+
// Custom-question fields keyed by their human label so the staging
|
|
406
|
+
// step can match them via profile.custom["…"].
|
|
407
|
+
const custom = (job.customQuestions ?? []).flatMap((cq) => (cq.fields ?? []).map((f) => ({
|
|
408
|
+
label: f.text ?? cq.text ?? "",
|
|
409
|
+
description: cq.description ?? null,
|
|
410
|
+
required: f.required ?? false,
|
|
411
|
+
fields: [
|
|
412
|
+
{
|
|
413
|
+
name: (f.text ?? cq.text ?? "").slice(0, 60).replace(/\s+/g, "_").toLowerCase(),
|
|
414
|
+
type: f.type === "multiple-choice"
|
|
415
|
+
? "single_select"
|
|
416
|
+
: f.type === "multi-choice"
|
|
417
|
+
? "multi_select"
|
|
418
|
+
: f.type === "textarea"
|
|
419
|
+
? "textarea"
|
|
420
|
+
: "input_text",
|
|
421
|
+
values: (f.options ?? []).map((o) => ({ value: o.text ?? "", label: o.text ?? "" })),
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
})));
|
|
425
|
+
return {
|
|
426
|
+
ok: true,
|
|
427
|
+
schema: {
|
|
428
|
+
source: SOURCE,
|
|
429
|
+
post_id: id,
|
|
430
|
+
job_title: job.text ?? "",
|
|
431
|
+
apply_url: job.applyUrl ?? job.hostedUrl ?? `${BOARD_URL}/${id}/apply`,
|
|
432
|
+
submit_endpoint: `${BOARD_URL}/${id}/apply`,
|
|
433
|
+
submit_method: "POST",
|
|
434
|
+
questions: [...standard, ...custom],
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
364
438
|
return {
|
|
365
439
|
searchPositions,
|
|
366
440
|
fetchAllPositions,
|
|
@@ -371,5 +445,6 @@ export function createAdapter(cfg) {
|
|
|
371
445
|
findNoticesByQuestion,
|
|
372
446
|
matchResume,
|
|
373
447
|
checkResume,
|
|
448
|
+
fetchApplicationSchema,
|
|
374
449
|
};
|
|
375
450
|
}
|
package/dist/weride.js
CHANGED
|
@@ -26,3 +26,4 @@ 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
|
+
export const fetchApplicationSchema = adapter.fetchApplicationSchema;
|
package/dist/xpeng.js
CHANGED
|
@@ -31,3 +31,4 @@ export const getNotice = adapter.getNotice;
|
|
|
31
31
|
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
32
32
|
export const matchResume = adapter.matchResume;
|
|
33
33
|
export const checkResume = adapter.checkResume;
|
|
34
|
+
export const fetchApplicationSchema = adapter.fetchApplicationSchema;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-pro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|