job-pro 0.8.1 → 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 +205 -66
- 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,82 +50,84 @@ 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";
|
|
58
|
+
const COMPANIES = [
|
|
59
|
+
{ key: "tencent", family: "Bespoke", source: "join.qq.com", label: "Tencent / 腾讯" },
|
|
60
|
+
{ key: "bytedance", family: "Bespoke", source: "jobs.bytedance.com", label: "ByteDance / 字节跳动" },
|
|
61
|
+
{ key: "alibaba", family: "Bespoke", source: "campus-talent.alibaba.com", label: "Alibaba / 阿里巴巴" },
|
|
62
|
+
{ key: "meituan", family: "Bespoke", source: "zhaopin.meituan.com", label: "Meituan / 美团" },
|
|
63
|
+
{ key: "xiaohongshu", family: "Bespoke", source: "job.xiaohongshu.com", label: "Xiaohongshu / 小红书" },
|
|
64
|
+
{ key: "jd", family: "Bespoke", source: "campus.jd.com", label: "JD / 京东" },
|
|
65
|
+
{ key: "kuaishou", family: "Bespoke", source: "campus.kuaishou.cn", label: "Kuaishou / 快手" },
|
|
66
|
+
{ key: "baidu", family: "Bespoke", source: "talent.baidu.com", label: "Baidu / 百度" },
|
|
67
|
+
{ key: "netease", family: "Bespoke", source: "hr.163.com", label: "NetEase / 网易" },
|
|
68
|
+
{ key: "didi", family: "Bespoke", source: "talent.didiglobal.com", label: "Didi / 滴滴" },
|
|
69
|
+
{ key: "bilibili", family: "Bespoke", source: "jobs.bilibili.com", label: "Bilibili / 哔哩哔哩" },
|
|
70
|
+
{ key: "pdd", family: "Bespoke", source: "careers.pinduoduo.com", label: "PDD / 拼多多" },
|
|
71
|
+
{ key: "huawei", family: "Bespoke", source: "career.huawei.com", label: "Huawei / 华为" },
|
|
72
|
+
{ key: "weibo", family: "Bespoke", source: "career.sina.com.cn", label: "Weibo / 微博" },
|
|
73
|
+
{ key: "mihoyo", family: "Bespoke", source: "ats.openout.mihoyo.com", label: "miHoYo / 米哈游" },
|
|
74
|
+
{ key: "pingan", family: "Bespoke", source: "campus.pingan.com", label: "Ping An / 平安" },
|
|
75
|
+
{ key: "trip", family: "Bespoke", source: "careers.ctrip.com", label: "Trip.com / 携程" },
|
|
76
|
+
{ key: "unitree", family: "Bespoke", source: "www.unitree.com", label: "Unitree / 宇树科技" },
|
|
77
|
+
{ key: "byd", family: "Bespoke", source: "job.byd.com", label: "BYD / 比亚迪" },
|
|
78
|
+
{ key: "antgroup", family: "Bespoke", source: "hrcareersweb.antgroup.com", label: "Ant Group / 蚂蚁集团" },
|
|
79
|
+
{ key: "liauto", family: "Bespoke", source: "www.lixiang.com", label: "Li Auto / 理想汽车" },
|
|
80
|
+
{ key: "sf", family: "Bespoke", source: "campus.sf-express.com", label: "SF Express / 顺丰" },
|
|
81
|
+
{ key: "oppo", family: "Bespoke", source: "careers.oppo.com", label: "OPPO" },
|
|
82
|
+
{ key: "xiaomi", family: "Feishu", source: "xiaomi.jobs.f.mioffice.cn", label: "Xiaomi / 小米" },
|
|
83
|
+
{ key: "nio", family: "Feishu", source: "nio.jobs.feishu.cn", label: "NIO / 蔚来" },
|
|
84
|
+
{ key: "minimax", family: "Feishu", source: "vrfi1sk8a0.jobs.feishu.cn", label: "MiniMax" },
|
|
85
|
+
{ key: "moonshot", family: "Moka", source: "app.mokahr.com/moonshot", label: "Moonshot / 月之暗面" },
|
|
86
|
+
{ key: "zhipu", family: "Feishu", source: "zhipu-ai.jobs.feishu.cn", label: "Zhipu / 智谱AI" },
|
|
87
|
+
{ key: "iqiyi", family: "Feishu", source: "careers.iqiyi.com", label: "iQIYI / 爱奇艺" },
|
|
88
|
+
{ key: "agibot", family: "Feishu", source: "agirobot.jobs.feishu.cn", label: "Agibot / 智元机器人" },
|
|
89
|
+
{ key: "lilith", family: "Feishu", source: "lilithgames.jobs.feishu.cn", label: "Lilith Games / 莉莉丝 — needs local Chrome" },
|
|
90
|
+
{ key: "zerooneai", family: "Feishu", source: "01ai.jobs.feishu.cn", label: "01.AI / 零一万物" },
|
|
91
|
+
{ key: "baichuan", family: "Feishu", source: "cq6qe6bvfr6.jobs.feishu.cn", label: "Baichuan / 百川智能" },
|
|
92
|
+
{ key: "sensetime", family: "Beisen Wecruit", source: "hr.sensetime.com", label: "SenseTime / 商汤" },
|
|
93
|
+
{ key: "horizonrobotics", family: "Beisen Wecruit", source: "wecruit.hotjob.cn", label: "Horizon Robotics / 地平线" },
|
|
94
|
+
{ key: "vivo", family: "Beisen iTalent", source: "vivo.zhiye.com", label: "vivo" },
|
|
95
|
+
{ key: "iflytek", family: "Beisen iTalent", source: "iflytek.zhiye.com", label: "iFlytek / 科大讯飞" },
|
|
96
|
+
{ key: "megvii", family: "Moka", source: "app.mokahr.com/megviihr", label: "Megvii / 旷视" },
|
|
97
|
+
{ key: "deepseek", family: "Moka", source: "app.mokahr.com/high-flyer", label: "DeepSeek / 深度求索" },
|
|
98
|
+
{ key: "galaxyuniversal", family: "Moka", source: "app.mokahr.com/yinhetongyong", label: "Galaxy Universal / 银河通用" },
|
|
99
|
+
{ key: "stepfun", family: "Moka", source: "app.mokahr.com/step", label: "StepFun / 阶跃星辰" },
|
|
100
|
+
{ key: "cambricon", family: "Moka", source: "app.mokahr.com/cambricon", label: "Cambricon / 寒武纪" },
|
|
101
|
+
{ key: "geely", family: "Moka", source: "app.mokahr.com/geely", label: "Geely / 吉利" },
|
|
102
|
+
{ key: "xpeng", family: "Greenhouse / Lever (intl arm)", source: "boards.greenhouse.io/xpengmotors", label: "XPeng / 小鹏汽车 — US AI" },
|
|
103
|
+
{ key: "weride", family: "Greenhouse / Lever (intl arm)", source: "jobs.lever.co/weride", label: "WeRide / 文远知行 — US / 广州" },
|
|
104
|
+
{ key: "hoyoverse", family: "Greenhouse / Lever (intl arm)", source: "boards.greenhouse.io/hoyoverse", label: "HoYoverse / 米哈游国际" },
|
|
105
|
+
{ key: "hikvision", family: "Liepin (third-party)", source: "api-c.liepin.com", label: "Hikvision / 海康威视" },
|
|
106
|
+
{ key: "cicc", family: "Liepin (third-party)", source: "api-c.liepin.com", label: "CICC / 中金" },
|
|
107
|
+
{ key: "cainiao", family: "Liepin (third-party)", source: "api-c.liepin.com", label: "Cainiao / 菜鸟" },
|
|
108
|
+
{ key: "webank", family: "Liepin (third-party)", source: "api-c.liepin.com", label: "WeBank / 微众银行" },
|
|
109
|
+
];
|
|
55
110
|
const HELP = `
|
|
56
111
|
job-pro — query Chinese big-tech campus recruiting from your terminal
|
|
57
112
|
(job.ha7ch.com)
|
|
58
113
|
|
|
59
114
|
USAGE
|
|
60
115
|
job-pro <company> <verb> [options]
|
|
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
|
|
61
119
|
job-pro --version
|
|
62
120
|
job-pro help
|
|
63
121
|
|
|
64
|
-
|
|
122
|
+
50 companies, all live. Run \`job-pro list\` for the full table grouped
|
|
123
|
+
by ATS family (Bespoke / Feishu / Beisen Wecruit / Beisen iTalent / Moka
|
|
124
|
+
/ Greenhouse-Lever / Liepin). Coverage summary at job.ha7ch.com.
|
|
65
125
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
xiaohongshu job.xiaohongshu.com (Xiaohongshu / 小红书)
|
|
72
|
-
jd campus.jd.com (JD / 京东)
|
|
73
|
-
kuaishou campus.kuaishou.cn (Kuaishou / 快手)
|
|
74
|
-
baidu talent.baidu.com (Baidu / 百度)
|
|
75
|
-
netease hr.163.com (NetEase / 网易)
|
|
76
|
-
didi talent.didiglobal.com (Didi / 滴滴)
|
|
77
|
-
bilibili jobs.bilibili.com (Bilibili / 哔哩哔哩)
|
|
78
|
-
pdd careers.pinduoduo.com (PDD / 拼多多)
|
|
79
|
-
huawei career.huawei.com (Huawei / 华为)
|
|
80
|
-
weibo career.sina.com.cn (Weibo / 微博)
|
|
81
|
-
mihoyo ats.openout.mihoyo.com (miHoYo / 米哈游)
|
|
82
|
-
pingan campus.pingan.com (Ping An / 平安)
|
|
83
|
-
trip careers.ctrip.com (Trip.com / 携程)
|
|
84
|
-
unitree www.unitree.com (Unitree / 宇树科技)
|
|
85
|
-
byd job.byd.com (BYD / 比亚迪)
|
|
86
|
-
antgroup hrcareersweb.antgroup.com (Ant Group / 蚂蚁集团)
|
|
87
|
-
liauto www.lixiang.com (Li Auto / 理想汽车)
|
|
88
|
-
zerooneai 01ai.jobs.feishu.cn (01.AI / 零一万物)
|
|
89
|
-
baichuan cq6qe6bvfr6.jobs.feishu.cn (Baichuan / 百川智能)
|
|
90
|
-
sf campus.sf-express.com (SF Express / 顺丰)
|
|
91
|
-
oppo careers.oppo.com (OPPO)
|
|
92
|
-
|
|
93
|
-
Feishu Recruiting (ATSX)
|
|
94
|
-
xiaomi xiaomi.jobs.f.mioffice.cn (Xiaomi / 小米)
|
|
95
|
-
nio nio.jobs.feishu.cn (NIO / 蔚来)
|
|
96
|
-
minimax vrfi1sk8a0.jobs.feishu.cn (MiniMax)
|
|
97
|
-
moonshot moonshot.jobs.feishu.cn (Moonshot / 月之暗面)
|
|
98
|
-
zhipu zhipu-ai.jobs.feishu.cn (Zhipu / 智谱AI)
|
|
99
|
-
iqiyi careers.iqiyi.com (iQIYI / 爱奇艺)
|
|
100
|
-
agibot agirobot.jobs.feishu.cn (Agibot / 智元机器人)
|
|
101
|
-
lilith lilithgames.jobs.feishu.cn (Lilith Games / 莉莉丝 — needs local Chrome)
|
|
102
|
-
|
|
103
|
-
Beisen Wecruit
|
|
104
|
-
sensetime hr.sensetime.com (SenseTime / 商汤)
|
|
105
|
-
horizonrobotics wecruit.hotjob.cn (Horizon Robotics / 地平线)
|
|
106
|
-
|
|
107
|
-
Beisen iTalent (zhiye)
|
|
108
|
-
vivo vivo.zhiye.com (vivo)
|
|
109
|
-
iflytek iflytek.zhiye.com (iFlytek / 科大讯飞)
|
|
110
|
-
|
|
111
|
-
Moka (app.mokahr.com)
|
|
112
|
-
megvii app.mokahr.com/megviihr (Megvii / 旷视)
|
|
113
|
-
deepseek app.mokahr.com/high-flyer (DeepSeek / 深度求索)
|
|
114
|
-
galaxyuniversal app.mokahr.com/yinhetongyong (Galaxy Universal / 银河通用)
|
|
115
|
-
stepfun app.mokahr.com/step (StepFun / 阶跃星辰)
|
|
116
|
-
cambricon app.mokahr.com/cambricon (Cambricon / 寒武纪)
|
|
117
|
-
geely app.mokahr.com/geely (Geely / 吉利)
|
|
118
|
-
|
|
119
|
-
Greenhouse / Lever (international arm only)
|
|
120
|
-
xpeng boards.greenhouse.io/xpengmotors (XPeng / 小鹏汽车 — US AI)
|
|
121
|
-
weride jobs.lever.co/weride (WeRide / 文远知行 — US / 广州)
|
|
122
|
-
hoyoverse boards.greenhouse.io/hoyoverse (HoYoverse / 米哈游国际)
|
|
123
|
-
|
|
124
|
-
Third-party (Liepin aggregator — no canonical public feed exists)
|
|
125
|
-
hikvision api-c.liepin.com (Hikvision / 海康威视)
|
|
126
|
-
cicc api-c.liepin.com (CICC / 中金)
|
|
127
|
-
cainiao api-c.liepin.com (Cainiao / 菜鸟)
|
|
128
|
-
webank api-c.liepin.com (WeBank / 微众银行)
|
|
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.
|
|
129
131
|
|
|
130
132
|
VERBS (same surface for every company)
|
|
131
133
|
search <kw> search openings (free text)
|
|
@@ -138,6 +140,9 @@ VERBS (same surface for every company)
|
|
|
138
140
|
match <resume-text-or--> rank jobs by overlap with resume text
|
|
139
141
|
pass "-" to read resume from stdin
|
|
140
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.
|
|
141
146
|
memory list | get <k> | set k=v | event <kind> [payload] | clear
|
|
142
147
|
|
|
143
148
|
OUTPUT
|
|
@@ -384,6 +389,63 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
384
389
|
const text = readResumeArg(args[0]);
|
|
385
390
|
return emit(adapter.checkResume(text), compact);
|
|
386
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
|
+
}
|
|
387
449
|
if (verb === "memory") {
|
|
388
450
|
const [sub, ...subArgs] = args;
|
|
389
451
|
if (!sub)
|
|
@@ -409,6 +471,53 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
409
471
|
}
|
|
410
472
|
die(`unknown verb: ${verb}. Try \`job-pro help\`.`);
|
|
411
473
|
}
|
|
474
|
+
function printCompanyList(compact) {
|
|
475
|
+
// Validate the directory still matches the ADAPTERS map. If a company
|
|
476
|
+
// appears in only one place, treat it as a bug.
|
|
477
|
+
const adapterKeys = new Set(Object.keys(ADAPTERS));
|
|
478
|
+
const dirKeys = new Set(COMPANIES.map((c) => c.key));
|
|
479
|
+
const missingInDir = [...adapterKeys].filter((k) => !dirKeys.has(k));
|
|
480
|
+
const missingInAdapters = [...dirKeys].filter((k) => !adapterKeys.has(k));
|
|
481
|
+
if (missingInDir.length || missingInAdapters.length) {
|
|
482
|
+
console.error("INTERNAL: COMPANIES directory diverged from ADAPTERS map.\n" +
|
|
483
|
+
(missingInDir.length ? ` missing from directory: ${missingInDir.join(", ")}\n` : "") +
|
|
484
|
+
(missingInAdapters.length ? ` missing from adapters: ${missingInAdapters.join(", ")}\n` : ""));
|
|
485
|
+
}
|
|
486
|
+
if (compact) {
|
|
487
|
+
// Machine-readable: emit a JSON array of { key, family, source, label }.
|
|
488
|
+
console.log(JSON.stringify(COMPANIES));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
// Human-readable: group by family, fixed-width left column.
|
|
492
|
+
const byFamily = new Map();
|
|
493
|
+
for (const c of COMPANIES) {
|
|
494
|
+
if (!byFamily.has(c.family))
|
|
495
|
+
byFamily.set(c.family, []);
|
|
496
|
+
byFamily.get(c.family).push(c);
|
|
497
|
+
}
|
|
498
|
+
const order = [
|
|
499
|
+
"Bespoke",
|
|
500
|
+
"Feishu",
|
|
501
|
+
"Beisen Wecruit",
|
|
502
|
+
"Beisen iTalent",
|
|
503
|
+
"Moka",
|
|
504
|
+
"Greenhouse / Lever (intl arm)",
|
|
505
|
+
"Liepin (third-party)",
|
|
506
|
+
];
|
|
507
|
+
const keyWidth = Math.max(...COMPANIES.map((c) => c.key.length));
|
|
508
|
+
const srcWidth = Math.max(...COMPANIES.map((c) => c.source.length));
|
|
509
|
+
console.log(`job-pro — 50 companies, all live. ATS-family breakdown:`);
|
|
510
|
+
for (const family of order) {
|
|
511
|
+
const entries = byFamily.get(family);
|
|
512
|
+
if (!entries)
|
|
513
|
+
continue;
|
|
514
|
+
console.log(`\n${family} (${entries.length})`);
|
|
515
|
+
for (const c of entries) {
|
|
516
|
+
console.log(` ${c.key.padEnd(keyWidth)} ${c.source.padEnd(srcWidth)} ${c.label}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
console.log(`\nTotal: ${COMPANIES.length}. Run \`job-pro <key> search "…"\` against any of them.`);
|
|
520
|
+
}
|
|
412
521
|
async function main() {
|
|
413
522
|
const args = process.argv.slice(2);
|
|
414
523
|
const cmd = args[0];
|
|
@@ -420,12 +529,42 @@ async function main() {
|
|
|
420
529
|
console.log(VERSION);
|
|
421
530
|
return;
|
|
422
531
|
}
|
|
532
|
+
if (cmd === "list" || cmd === "companies") {
|
|
533
|
+
const compact = args.includes("--compact");
|
|
534
|
+
printCompanyList(compact);
|
|
535
|
+
return;
|
|
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
|
+
}
|
|
423
561
|
const adapter = ADAPTERS[cmd];
|
|
424
562
|
if (adapter) {
|
|
425
563
|
await runCompany(adapter, cmd, args.slice(1));
|
|
426
564
|
return;
|
|
427
565
|
}
|
|
428
|
-
die(`unknown company: ${cmd}.
|
|
566
|
+
die(`unknown company: ${cmd}. Try \`job-pro list\` for the full list, ` +
|
|
567
|
+
`or \`job-pro help\` for usage.`);
|
|
429
568
|
}
|
|
430
569
|
main().catch((err) => {
|
|
431
570
|
console.error("Error:", err instanceof Error ? err.message : err);
|
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",
|