job-pro 0.9.1 → 0.9.3
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 +73 -7
- package/dist/baichuan.js +1 -0
- package/dist/feishu.js +129 -0
- package/dist/greenhouse.js +3 -0
- package/dist/index.js +87 -18
- package/dist/iqiyi.js +9 -0
- package/dist/lever.js +4 -0
- package/dist/lilith.js +10 -0
- package/dist/minimax.js +1 -1
- package/dist/nio.js +1 -1
- 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
|
@@ -17,6 +17,33 @@ import { readFileSync, existsSync, statSync } from "node:fs";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { basename, join } from "node:path";
|
|
19
19
|
const PROFILE_PATH = process.env.JOB_PRO_PROFILE_PATH ?? join(homedir(), ".jobpro", "profile.json");
|
|
20
|
+
const SESSION_DIR = process.env.JOB_PRO_SESSION_DIR ?? join(homedir(), ".jobpro");
|
|
21
|
+
/** Read a captured session for an adapter, or null if none exists. */
|
|
22
|
+
export function loadSession(adapterKey) {
|
|
23
|
+
const path = join(SESSION_DIR, `${adapterKey}.session.json`);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(path, "utf8");
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Convert a CapturedSession into a single Cookie header string. */
|
|
35
|
+
export function serializeCookieHeader(session, targetHost) {
|
|
36
|
+
const cookies = session.cookies.filter((c) => {
|
|
37
|
+
if (!targetHost)
|
|
38
|
+
return true;
|
|
39
|
+
if (!c.domain)
|
|
40
|
+
return true;
|
|
41
|
+
// RFC-style domain match: ".example.com" matches any subdomain.
|
|
42
|
+
const dom = c.domain.startsWith(".") ? c.domain.slice(1) : c.domain;
|
|
43
|
+
return targetHost === dom || targetHost.endsWith("." + dom);
|
|
44
|
+
});
|
|
45
|
+
return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
46
|
+
}
|
|
20
47
|
const TEMPLATE = {
|
|
21
48
|
first_name: "",
|
|
22
49
|
last_name: "",
|
|
@@ -93,6 +120,8 @@ export function stageApplication(schema, profile) {
|
|
|
93
120
|
apply_url: schema.apply_url,
|
|
94
121
|
submit_endpoint: schema.submit_endpoint,
|
|
95
122
|
submit_method: schema.submit_method,
|
|
123
|
+
submit_kind: schema.submit_kind,
|
|
124
|
+
submit_notes: schema.submit_notes,
|
|
96
125
|
staged,
|
|
97
126
|
unanswered_required,
|
|
98
127
|
ready: unanswered_required.length === 0,
|
|
@@ -106,6 +135,14 @@ function resolveAnswer(field, profile) {
|
|
|
106
135
|
return { value: profile.first_name ?? "", reason: "profile.first_name missing" };
|
|
107
136
|
case "last_name":
|
|
108
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
|
+
};
|
|
109
146
|
case "email":
|
|
110
147
|
return { value: profile.email ?? "", reason: "profile.email missing" };
|
|
111
148
|
case "phone":
|
|
@@ -164,7 +201,7 @@ export function formatStaged(s) {
|
|
|
164
201
|
function truncate(s, n) {
|
|
165
202
|
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
166
203
|
}
|
|
167
|
-
export async function submitApplication(staged, target) {
|
|
204
|
+
export async function submitApplication(staged, target, options = {}) {
|
|
168
205
|
if (!staged.submit_endpoint) {
|
|
169
206
|
return {
|
|
170
207
|
ok: false,
|
|
@@ -188,16 +225,45 @@ export async function submitApplication(staged, target) {
|
|
|
188
225
|
}
|
|
189
226
|
const url = target.kind === "debug" ? target.url : staged.submit_endpoint;
|
|
190
227
|
const fd = await buildMultipartForm(staged);
|
|
228
|
+
const headers = {
|
|
229
|
+
// Don't set Content-Type — fetch/undici picks the correct
|
|
230
|
+
// multipart/form-data boundary for the FormData instance.
|
|
231
|
+
Accept: "application/json, text/plain, */*",
|
|
232
|
+
"User-Agent": "job-pro/0.9 (https://github.com/HA7CH/job-pro)",
|
|
233
|
+
};
|
|
234
|
+
// Layer in captured-session headers (Cookie, X-Xsrf-Token, etc.) only
|
|
235
|
+
// when we're actually hitting the upstream endpoint. Debug echo endpoints
|
|
236
|
+
// (httpbin) don't need them and might log them, so we strip there.
|
|
237
|
+
if (target.kind === "upstream" && options.session) {
|
|
238
|
+
const targetHost = (() => {
|
|
239
|
+
try {
|
|
240
|
+
return new URL(url).hostname;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
})();
|
|
246
|
+
const cookieHeader = serializeCookieHeader(options.session, targetHost);
|
|
247
|
+
if (cookieHeader)
|
|
248
|
+
headers.Cookie = cookieHeader;
|
|
249
|
+
for (const [k, v] of Object.entries(options.session.headers ?? {})) {
|
|
250
|
+
// Skip cookie — already handled. Skip content-type — let undici set
|
|
251
|
+
// the multipart boundary one. Skip authorization-bearer only if the
|
|
252
|
+
// upstream's auth model isn't cookie-based.
|
|
253
|
+
if (k === "cookie" || k === "content-type")
|
|
254
|
+
continue;
|
|
255
|
+
// Normalise to canonical casing — fetch's Headers preserves what we set.
|
|
256
|
+
headers[k] = v;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (options.extraHeaders) {
|
|
260
|
+
Object.assign(headers, options.extraHeaders);
|
|
261
|
+
}
|
|
191
262
|
let response;
|
|
192
263
|
try {
|
|
193
264
|
response = await fetch(url, {
|
|
194
265
|
method: staged.submit_method ?? "POST",
|
|
195
|
-
headers
|
|
196
|
-
// Don't set Content-Type — fetch/undici picks the correct
|
|
197
|
-
// multipart/form-data boundary for the FormData instance.
|
|
198
|
-
Accept: "application/json, text/plain, */*",
|
|
199
|
-
"User-Agent": "job-pro/0.9 (https://github.com/HA7CH/job-pro)",
|
|
200
|
-
},
|
|
266
|
+
headers,
|
|
201
267
|
body: fd,
|
|
202
268
|
});
|
|
203
269
|
}
|
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/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/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/index.js
CHANGED
|
@@ -50,11 +50,11 @@ import * as geely from "./geely.js";
|
|
|
50
50
|
import * as webank from "./webank.js";
|
|
51
51
|
import * as horizonrobotics from "./horizonrobotics.js";
|
|
52
52
|
import * as cambricon from "./cambricon.js";
|
|
53
|
-
import { loadProfile, profileTemplate, stageApplication, submitApplication, formatStaged, } from "./apply.js";
|
|
53
|
+
import { loadProfile, loadSession, profileTemplate, stageApplication, submitApplication, formatStaged, } from "./apply.js";
|
|
54
54
|
import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
|
|
55
55
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
56
56
|
import { dirname } from "node:path";
|
|
57
|
-
const VERSION = "0.9.
|
|
57
|
+
const VERSION = "0.9.3";
|
|
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 / 字节跳动" },
|
|
@@ -406,17 +406,10 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
406
406
|
`See docs/auto-apply.md for the rollout plan.`,
|
|
407
407
|
}, compact);
|
|
408
408
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
post_id: postId,
|
|
414
|
-
message: `--really-submit is intentionally disabled until per-ATS submission flows ` +
|
|
415
|
-
`are validated end-to-end against live boards. Use --debug-submit-to <url> ` +
|
|
416
|
-
`to verify the multipart wire format against your own echo server (e.g. ` +
|
|
417
|
-
`https://httpbin.org/post) without spamming the real ATS.`,
|
|
418
|
-
}, compact);
|
|
419
|
-
}
|
|
409
|
+
// Note: we DON'T early-return on reallySubmit here — we fall through
|
|
410
|
+
// to stage the application first, then re-gate before actually posting.
|
|
411
|
+
// This lets the user verify the staged payload one last time even
|
|
412
|
+
// when they pass --really-submit by accident.
|
|
420
413
|
const schemaResult = await fetchSchema.call(adapter, postId);
|
|
421
414
|
const sr = schemaResult;
|
|
422
415
|
if (!sr.ok || !sr.schema) {
|
|
@@ -434,24 +427,100 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
434
427
|
}, compact);
|
|
435
428
|
}
|
|
436
429
|
const staged = stageApplication(sr.schema, prof.profile);
|
|
437
|
-
|
|
430
|
+
const session = loadSession(company);
|
|
431
|
+
// Mode selection: --debug-submit-to <url> overrides everything.
|
|
438
432
|
if (debugUrl) {
|
|
439
433
|
const result = await submitApplication(staged, { kind: "debug", url: debugUrl });
|
|
440
434
|
return emit({ mode: "debug-submit", staged, result }, compact);
|
|
441
435
|
}
|
|
436
|
+
// --really-submit: actually hit the upstream endpoint. Guarded by both
|
|
437
|
+
// an env-var attestation and (for non-anon adapters) a session.json.
|
|
438
|
+
if (reallySubmit) {
|
|
439
|
+
const understood = process.env.JOB_PRO_I_UNDERSTAND_REAL_SUBMIT === "yes";
|
|
440
|
+
if (!understood) {
|
|
441
|
+
return emit({
|
|
442
|
+
ok: false,
|
|
443
|
+
source: company,
|
|
444
|
+
post_id: postId,
|
|
445
|
+
mode: "really-submit-blocked",
|
|
446
|
+
staged,
|
|
447
|
+
message: `--really-submit is gated by an env-var attestation. To unlock, set ` +
|
|
448
|
+
`JOB_PRO_I_UNDERSTAND_REAL_SUBMIT=yes in your shell. This submission will ` +
|
|
449
|
+
`POST a real application to ${staged.submit_endpoint}; doing so without a ` +
|
|
450
|
+
`valid resume / answers is spam against the company's recruiters.`,
|
|
451
|
+
}, compact);
|
|
452
|
+
}
|
|
453
|
+
if (!staged.ready) {
|
|
454
|
+
return emit({
|
|
455
|
+
ok: false,
|
|
456
|
+
source: company,
|
|
457
|
+
post_id: postId,
|
|
458
|
+
mode: "really-submit-blocked",
|
|
459
|
+
staged,
|
|
460
|
+
message: `${staged.unanswered_required.length} required field(s) still unanswered; refusing to submit incomplete application`,
|
|
461
|
+
}, compact);
|
|
462
|
+
}
|
|
463
|
+
// Submission flow selection by submit_kind. Only the generic
|
|
464
|
+
// multipart families are wired to actually fire today; everything
|
|
465
|
+
// else gets a useful refusal message.
|
|
466
|
+
const kind = (sr.schema.submit_kind ?? "multipart-anon");
|
|
467
|
+
const isAnonMultipart = kind === "multipart-anon";
|
|
468
|
+
const isSessionMultipart = kind === "multipart-session";
|
|
469
|
+
const isGenericMultipart = isAnonMultipart || isSessionMultipart;
|
|
470
|
+
if (!isGenericMultipart) {
|
|
471
|
+
return emit({
|
|
472
|
+
ok: false,
|
|
473
|
+
source: company,
|
|
474
|
+
post_id: postId,
|
|
475
|
+
mode: "really-submit-blocked",
|
|
476
|
+
staged,
|
|
477
|
+
submit_kind: kind,
|
|
478
|
+
submit_notes: sr.schema.submit_notes,
|
|
479
|
+
message: `submit_kind="${kind}" — this adapter family doesn't yet have an ` +
|
|
480
|
+
`executor wired. The application schema + submit endpoint are ` +
|
|
481
|
+
`documented (see submit_notes), but firing the submission needs a ` +
|
|
482
|
+
`family-specific multi-step flow (token exchange / AES envelope / ` +
|
|
483
|
+
`CDP / etc.). Landing per-family executors is the next iteration of ` +
|
|
484
|
+
`Phase 2. Use --debug-submit-to <url> to inspect what we have today.`,
|
|
485
|
+
}, compact);
|
|
486
|
+
}
|
|
487
|
+
// Non-anon multipart families need session.json.
|
|
488
|
+
if (!isAnonMultipart && !session) {
|
|
489
|
+
return emit({
|
|
490
|
+
ok: false,
|
|
491
|
+
source: company,
|
|
492
|
+
post_id: postId,
|
|
493
|
+
mode: "really-submit-blocked",
|
|
494
|
+
staged,
|
|
495
|
+
message: `no captured session at ~/.jobpro/${company}.session.json. Install the ` +
|
|
496
|
+
`extension/ directory in Chrome, log into the careers site, click Export, ` +
|
|
497
|
+
`then mv ~/Downloads/jobpro/${company}.session.json ~/.jobpro/`,
|
|
498
|
+
}, compact);
|
|
499
|
+
}
|
|
500
|
+
const result = await submitApplication(staged, { kind: "upstream" }, { session });
|
|
501
|
+
return emit({ mode: "really-submit", staged, submit_kind: kind, session_used: !!session, result }, compact);
|
|
502
|
+
}
|
|
442
503
|
// Default: dry-run print, no network.
|
|
443
504
|
if (compact) {
|
|
444
|
-
return emit({ mode: "dry-run", staged }, compact);
|
|
505
|
+
return emit({ mode: "dry-run", staged, has_session: !!session }, compact);
|
|
445
506
|
}
|
|
446
507
|
console.log(formatStaged(staged));
|
|
508
|
+
if (session) {
|
|
509
|
+
console.log(`\nSession captured (~/.jobpro/${company}.session.json): ${session.cookies.length} cookies + ${Object.keys(session.headers).length} auth headers.`);
|
|
510
|
+
}
|
|
447
511
|
if (!staged.ready) {
|
|
448
512
|
console.log(`\nFill the unanswered required fields in ${profileTemplate().path} ` +
|
|
449
513
|
`(profile.custom.<name> for unknown fields), then re-run.`);
|
|
450
514
|
}
|
|
451
515
|
else {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
516
|
+
const isAnon = staged.source.startsWith("boards-api.greenhouse.io/") ||
|
|
517
|
+
staged.source.startsWith("api.lever.co/");
|
|
518
|
+
console.log(`\nDry-run complete. To actually submit:\n` +
|
|
519
|
+
` • --debug-submit-to https://httpbin.org/post — verify wire format\n` +
|
|
520
|
+
` • JOB_PRO_I_UNDERSTAND_REAL_SUBMIT=yes job-pro ${company} apply ${postId} --really-submit\n` +
|
|
521
|
+
(isAnon
|
|
522
|
+
? ` ${company} is Greenhouse/Lever (anonymous submission, no session needed).\n`
|
|
523
|
+
: ` ${company} needs ~/.jobpro/${company}.session.json — capture via the browser extension.\n`));
|
|
455
524
|
}
|
|
456
525
|
void aDebug; // silence "unused" — `args` flow goes through popFlagValue
|
|
457
526
|
return;
|
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/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/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/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.3",
|
|
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",
|