job-pro 1.0.27 → 1.0.29
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 +25 -2
- package/dist/index.js +109 -2
- package/package.json +1 -1
package/dist/apply.js
CHANGED
|
@@ -715,10 +715,13 @@ async function fetchWithRetry(url, init, label, log) {
|
|
|
715
715
|
}
|
|
716
716
|
return { ok: false, message: lastErr };
|
|
717
717
|
}
|
|
718
|
-
// 4xx → user error, don't retry.
|
|
718
|
+
// 4xx → user error, don't retry. Enrich with a hint pointing at the most
|
|
719
|
+
// likely cause — bare "HTTP 401: " gives the user nothing to act on.
|
|
719
720
|
if (response.status >= 400 && response.status < 500) {
|
|
721
|
+
const hint = hintForStatus(response.status);
|
|
722
|
+
const message = `HTTP ${response.status}: ${response.statusText}${hint ? ` — ${hint}` : ""}`;
|
|
720
723
|
log?.push({ attempt: attempt + 1, ok: false, status: response.status, message: `${label}: HTTP ${response.status} (no retry — 4xx)` });
|
|
721
|
-
return { ok: false, status: response.status, message
|
|
724
|
+
return { ok: false, status: response.status, message };
|
|
722
725
|
}
|
|
723
726
|
// 5xx → server error, retry.
|
|
724
727
|
if (response.status >= 500 && attempt < maxRetries) {
|
|
@@ -732,6 +735,26 @@ async function fetchWithRetry(url, init, label, log) {
|
|
|
732
735
|
}
|
|
733
736
|
return { ok: false, message: lastErr || "exhausted retries" };
|
|
734
737
|
}
|
|
738
|
+
function hintForStatus(status) {
|
|
739
|
+
// Stale-session hints are by far the most common cause of 401/403 here —
|
|
740
|
+
// the session.json cookies have expired since capture. The
|
|
741
|
+
// really-submit-blocked / session-age gate catches >30d staleness, but
|
|
742
|
+
// sessions sometimes expire earlier (logout from another tab, password
|
|
743
|
+
// change, server-side revoke).
|
|
744
|
+
if (status === 401 || status === 403) {
|
|
745
|
+
return "session likely stale — recapture via `job-pro extension`, log into the careers site, click Export";
|
|
746
|
+
}
|
|
747
|
+
if (status === 404) {
|
|
748
|
+
return "endpoint not found — submit_endpoint may have drifted upstream; verify via `apply --schema` + `--debug-submit-to`";
|
|
749
|
+
}
|
|
750
|
+
if (status === 422 || status === 400) {
|
|
751
|
+
return "request rejected — likely a missing/malformed answer; rerun `apply --interactive` to refill required fields";
|
|
752
|
+
}
|
|
753
|
+
if (status === 429) {
|
|
754
|
+
return "rate limited — retry after a few minutes";
|
|
755
|
+
}
|
|
756
|
+
return "";
|
|
757
|
+
}
|
|
735
758
|
function retryDelayMs(attempt) {
|
|
736
759
|
// Exponential backoff with jitter: 250ms / 500ms / 1s / 2s / 4s, ±25%.
|
|
737
760
|
const base = 250 * Math.pow(2, attempt);
|
package/dist/index.js
CHANGED
|
@@ -53,10 +53,10 @@ import * as cambricon from "./cambricon.js";
|
|
|
53
53
|
import { loadProfile, loadProfileRaw, loadSession, profileTemplate, saveProfile, stageApplication, submitApplication, executeFeishu3Step, executeMokaApply, executeBeisenWecruit, executeBeisenITalent, executeCdpRealBrowser, buildFormTemplate, applyFormFile, promptUnansweredFields, formatStaged, } from "./apply.js";
|
|
54
54
|
import { createInterface } from "node:readline";
|
|
55
55
|
import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
|
|
56
|
-
import { writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
56
|
+
import { writeFileSync, mkdirSync, existsSync, readdirSync, statSync, mkdtempSync, rmSync } from "node:fs";
|
|
57
57
|
import { dirname, join } from "node:path";
|
|
58
58
|
import { fileURLToPath } from "node:url";
|
|
59
|
-
import { homedir } from "node:os";
|
|
59
|
+
import { homedir, tmpdir } from "node:os";
|
|
60
60
|
import { createRequire as require_createRequire } from "node:module";
|
|
61
61
|
function require_module() {
|
|
62
62
|
return { createRequire: require_createRequire };
|
|
@@ -142,6 +142,7 @@ USAGE
|
|
|
142
142
|
job-pro <company> <verb> [options]
|
|
143
143
|
job-pro list [--compact] list all 50 companies + source family
|
|
144
144
|
job-pro status [--compact] survey profile / sessions / memory / chrome
|
|
145
|
+
job-pro selftest [--compact] end-to-end check: search → schema → echo-submit
|
|
145
146
|
job-pro profile init [--interactive] [--force]
|
|
146
147
|
write ~/.jobpro/profile.json
|
|
147
148
|
--interactive fills it via prompts.
|
|
@@ -1170,6 +1171,112 @@ async function main() {
|
|
|
1170
1171
|
printStatus(compact);
|
|
1171
1172
|
return;
|
|
1172
1173
|
}
|
|
1174
|
+
if (cmd === "selftest") {
|
|
1175
|
+
// Three end-to-end checks against the easiest adapter (xpeng, anon-submit):
|
|
1176
|
+
// 1. searchPositions returns >0 hits
|
|
1177
|
+
// 2. fetchApplicationSchema for the first hit returns ok:true with questions
|
|
1178
|
+
// 3. submitApplication(staged, {kind:"debug", url:httpbin}) returns 200
|
|
1179
|
+
// Total ~3-5s. No profile / no session needed. Useful right after install
|
|
1180
|
+
// to confirm the CLI can actually round-trip end-to-end.
|
|
1181
|
+
const compact = args.includes("--compact");
|
|
1182
|
+
const xpengAdapter = ADAPTERS.xpeng;
|
|
1183
|
+
const checks = [];
|
|
1184
|
+
async function run(name, fn) {
|
|
1185
|
+
const t0 = Date.now();
|
|
1186
|
+
try {
|
|
1187
|
+
const r = await fn();
|
|
1188
|
+
checks.push({ name, ok: true, detail: "", ms: Date.now() - t0 });
|
|
1189
|
+
return r;
|
|
1190
|
+
}
|
|
1191
|
+
catch (err) {
|
|
1192
|
+
checks.push({ name, ok: false, detail: err instanceof Error ? err.message : String(err), ms: Date.now() - t0 });
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
// Step 1
|
|
1197
|
+
const list = await run("search xpeng", async () => {
|
|
1198
|
+
const r = (await xpengAdapter.searchPositions({ pageSize: 1 }));
|
|
1199
|
+
if (!r.ok || !r.positions?.[0]?.post_id)
|
|
1200
|
+
throw new Error("no positions returned");
|
|
1201
|
+
return r;
|
|
1202
|
+
});
|
|
1203
|
+
let postId = null;
|
|
1204
|
+
let title = "";
|
|
1205
|
+
if (list && list.positions?.[0]) {
|
|
1206
|
+
postId = String(list.positions[0].post_id ?? "");
|
|
1207
|
+
title = String(list.positions[0].title ?? "").trim();
|
|
1208
|
+
}
|
|
1209
|
+
// Step 2
|
|
1210
|
+
let schema = null;
|
|
1211
|
+
if (postId && typeof xpengAdapter.fetchApplicationSchema === "function") {
|
|
1212
|
+
schema = await run("fetch schema", async () => {
|
|
1213
|
+
const r = (await xpengAdapter.fetchApplicationSchema(postId));
|
|
1214
|
+
if (!r.ok || !r.schema)
|
|
1215
|
+
throw new Error(r.message ?? "schema fetch failed");
|
|
1216
|
+
return r.schema;
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
else if (!postId) {
|
|
1220
|
+
checks.push({ name: "fetch schema", ok: false, detail: "skipped — no post_id from search", ms: 0 });
|
|
1221
|
+
}
|
|
1222
|
+
// Step 3
|
|
1223
|
+
if (schema) {
|
|
1224
|
+
const tmp = mkdtempSync(join(tmpdir(), "jobpro-selftest-"));
|
|
1225
|
+
const resumePath = join(tmp, "resume.pdf");
|
|
1226
|
+
writeFileSync(resumePath, "%PDF\n");
|
|
1227
|
+
const profile = {
|
|
1228
|
+
first_name: "Self", last_name: "Test", email: "selftest@example.com",
|
|
1229
|
+
phone: "+86 13800138000", resume_path: resumePath, cover_letter_text: "",
|
|
1230
|
+
custom: {},
|
|
1231
|
+
};
|
|
1232
|
+
// Auto-fill required: first allowed value for selects, "N/A" for text.
|
|
1233
|
+
for (const q of schema.questions) {
|
|
1234
|
+
if (!q.required)
|
|
1235
|
+
continue;
|
|
1236
|
+
const f = q.fields[0];
|
|
1237
|
+
if (!f)
|
|
1238
|
+
continue;
|
|
1239
|
+
if (["input_text", "textarea"].includes(f.type))
|
|
1240
|
+
profile.custom[f.name] = "N/A (selftest)";
|
|
1241
|
+
else if (f.type.includes("select")) {
|
|
1242
|
+
const first = f.values?.[0];
|
|
1243
|
+
if (first && typeof first.value !== "undefined")
|
|
1244
|
+
profile.custom[f.name] = String(first.value);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const staged = stageApplication(schema, profile);
|
|
1248
|
+
if (!staged.ready) {
|
|
1249
|
+
checks.push({ name: "debug-submit echo", ok: false, detail: `staged not ready: ${staged.unanswered_required.slice(0, 3).join(", ")}`, ms: 0 });
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
await run("debug-submit echo", async () => {
|
|
1253
|
+
const r = (await submitApplication(staged, { kind: "debug", url: "https://httpbin.org/post" }));
|
|
1254
|
+
if (r.ok !== true || r.status !== 200)
|
|
1255
|
+
throw new Error(`echo failed: ok=${r.ok} status=${r.status} msg=${r.message}`);
|
|
1256
|
+
return r;
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
1260
|
+
}
|
|
1261
|
+
const fails = checks.filter((c) => !c.ok).length;
|
|
1262
|
+
if (compact) {
|
|
1263
|
+
console.log(JSON.stringify({ ok: fails === 0, checks }));
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
console.log(`\njob-pro selftest — using xpeng (anon Greenhouse board)\n`);
|
|
1267
|
+
for (const c of checks) {
|
|
1268
|
+
const icon = c.ok ? "✓" : "✗";
|
|
1269
|
+
const detail = c.detail ? ` ${c.detail}` : "";
|
|
1270
|
+
console.log(` ${icon} ${c.name.padEnd(20)} ${c.ms}ms${detail}`);
|
|
1271
|
+
}
|
|
1272
|
+
console.log(`\n ${checks.length - fails} pass / ${fails} fail / ${checks.length} total${title ? ` — sampled "${title}"` : ""}`);
|
|
1273
|
+
if (fails === 0)
|
|
1274
|
+
console.log(`\n Setup looks good. Run \`job-pro find "<keyword>"\` to scan all 50 companies.`);
|
|
1275
|
+
}
|
|
1276
|
+
if (fails > 0)
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1173
1280
|
if (cmd === "extension") {
|
|
1174
1281
|
// Locate the extension/ directory. The package ships it as a sibling of
|
|
1175
1282
|
// dist/, so __dirname is cli/dist and the extension lives at ../extension.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
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",
|