job-pro 1.0.0 → 1.0.1
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 +73 -0
- package/dist/index.js +41 -8
- package/package.json +1 -1
package/dist/apply.js
CHANGED
|
@@ -202,6 +202,79 @@ export function formatStaged(s) {
|
|
|
202
202
|
function truncate(s, n) {
|
|
203
203
|
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
204
204
|
}
|
|
205
|
+
export function buildFormTemplate(schema, profile) {
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const q of schema.questions) {
|
|
208
|
+
for (const f of q.fields) {
|
|
209
|
+
const resolved = resolveAnswer(f, profile);
|
|
210
|
+
out.push({
|
|
211
|
+
name: f.name,
|
|
212
|
+
type: f.type,
|
|
213
|
+
required: q.required,
|
|
214
|
+
label: q.label,
|
|
215
|
+
description: q.description,
|
|
216
|
+
options: f.values && f.values.length > 0 ? f.values : undefined,
|
|
217
|
+
value: resolved.value,
|
|
218
|
+
unanswered_reason: resolved.value ? undefined : resolved.reason || undefined,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
source: schema.source,
|
|
224
|
+
post_id: schema.post_id,
|
|
225
|
+
job_title: schema.job_title,
|
|
226
|
+
apply_url: schema.apply_url,
|
|
227
|
+
submit_kind: schema.submit_kind,
|
|
228
|
+
fields: out,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Merge a `{ field_name: value }` map into the profile's custom overrides. */
|
|
232
|
+
export function applyFormFile(profile, formFilePath) {
|
|
233
|
+
if (!existsSync(formFilePath)) {
|
|
234
|
+
return { ok: false, message: `form file not found: ${formFilePath}` };
|
|
235
|
+
}
|
|
236
|
+
let raw;
|
|
237
|
+
try {
|
|
238
|
+
raw = readFileSync(formFilePath, "utf8");
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
return { ok: false, message: `read ${formFilePath} failed: ${err instanceof Error ? err.message : err}` };
|
|
242
|
+
}
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(raw);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
return { ok: false, message: `${formFilePath} is not valid JSON: ${err instanceof Error ? err.message : err}` };
|
|
249
|
+
}
|
|
250
|
+
// Accept either:
|
|
251
|
+
// (a) a flat { name: value } map, or
|
|
252
|
+
// (b) the full FormTemplate shape (fields:[{ name, value }, …])
|
|
253
|
+
const overrides = {};
|
|
254
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.fields)) {
|
|
255
|
+
for (const f of parsed.fields) {
|
|
256
|
+
if (typeof f.name === "string" && typeof f.value === "string" && f.value.length > 0) {
|
|
257
|
+
overrides[f.name] = f.value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (parsed && typeof parsed === "object") {
|
|
262
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
263
|
+
if (typeof v === "string" && v.length > 0)
|
|
264
|
+
overrides[k] = v;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
return { ok: false, message: "form file must be a JSON object or FormTemplate" };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
profile: {
|
|
273
|
+
...profile,
|
|
274
|
+
custom: { ...(profile.custom ?? {}), ...overrides },
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
205
278
|
export function buildBespokeApplySchema(cfg) {
|
|
206
279
|
const standard = [
|
|
207
280
|
{ label: "Name", required: true, fields: [{ name: "name", type: "input_text" }] },
|
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, loadSession, profileTemplate, stageApplication, submitApplication, executeFeishu3Step, executeMokaApply, executeBeisenWecruit, executeBeisenITalent, executeCdpRealBrowser, formatStaged, } from "./apply.js";
|
|
53
|
+
import { loadProfile, loadSession, profileTemplate, stageApplication, submitApplication, executeFeishu3Step, executeMokaApply, executeBeisenWecruit, executeBeisenITalent, executeCdpRealBrowser, buildFormTemplate, applyFormFile, 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 = "1.0.
|
|
57
|
+
const VERSION = "1.0.1";
|
|
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 / 字节跳动" },
|
|
@@ -152,8 +152,10 @@ VERBS (same surface for every company)
|
|
|
152
152
|
pass "-" to read resume from stdin
|
|
153
153
|
resume-check <resume-text-or--> structural sanity check on a resume
|
|
154
154
|
apply <post_id> stage an application (Phase 2 dry-run)
|
|
155
|
-
--
|
|
156
|
-
|
|
155
|
+
--print-form emit a fillable JSON template
|
|
156
|
+
--form-file <path> merge per-job answers
|
|
157
|
+
--debug-submit-to <url> verify wire format
|
|
158
|
+
--really-submit actually fire (env-gated)
|
|
157
159
|
memory list | get <k> | set k=v | event <kind> [payload] | clear
|
|
158
160
|
|
|
159
161
|
OUTPUT
|
|
@@ -403,9 +405,12 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
403
405
|
if (verb === "apply") {
|
|
404
406
|
const postId = args[0];
|
|
405
407
|
if (!postId)
|
|
406
|
-
die(`usage: job-pro ${company} apply <post_id> [--dry-run | --debug-submit-to <url> | --really-submit]`);
|
|
408
|
+
die(`usage: job-pro ${company} apply <post_id> [--print-form | --form-file <path>] [--dry-run | --debug-submit-to <url> | --really-submit]`);
|
|
407
409
|
const reallySubmit = args.includes("--really-submit");
|
|
410
|
+
const printForm = args.includes("--print-form");
|
|
408
411
|
const { args: aDebug, value: debugUrl } = popFlagValue(args, "--debug-submit-to");
|
|
412
|
+
const { args: _aForm, value: formFilePath } = popFlagValue(aDebug, "--form-file");
|
|
413
|
+
void _aForm;
|
|
409
414
|
const fetchSchema = adapter.fetchApplicationSchema;
|
|
410
415
|
if (typeof fetchSchema !== "function") {
|
|
411
416
|
return emit({
|
|
@@ -437,7 +442,27 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
437
442
|
hint: `run \`job-pro profile init\` to create a template.`,
|
|
438
443
|
}, compact);
|
|
439
444
|
}
|
|
440
|
-
|
|
445
|
+
// --print-form short-circuits everything else: emit a fillable
|
|
446
|
+
// template specific to this job's schema and exit.
|
|
447
|
+
if (printForm) {
|
|
448
|
+
const template = buildFormTemplate(sr.schema, prof.profile);
|
|
449
|
+
return emit(template, compact);
|
|
450
|
+
}
|
|
451
|
+
// --form-file merges per-job overrides into profile.custom.
|
|
452
|
+
let effectiveProfile = prof.profile;
|
|
453
|
+
if (formFilePath) {
|
|
454
|
+
const merged = applyFormFile(effectiveProfile, formFilePath);
|
|
455
|
+
if (!merged.ok) {
|
|
456
|
+
return emit({
|
|
457
|
+
ok: false,
|
|
458
|
+
source: company,
|
|
459
|
+
post_id: postId,
|
|
460
|
+
message: merged.message,
|
|
461
|
+
}, compact);
|
|
462
|
+
}
|
|
463
|
+
effectiveProfile = merged.profile;
|
|
464
|
+
}
|
|
465
|
+
const staged = stageApplication(sr.schema, effectiveProfile);
|
|
441
466
|
const session = loadSession(company);
|
|
442
467
|
// Mode selection: --debug-submit-to <url> overrides everything.
|
|
443
468
|
if (debugUrl) {
|
|
@@ -572,8 +597,16 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
572
597
|
console.log(`\nSession captured (~/.jobpro/${company}.session.json): ${session.cookies.length} cookies + ${Object.keys(session.headers).length} auth headers.`);
|
|
573
598
|
}
|
|
574
599
|
if (!staged.ready) {
|
|
575
|
-
console.log(`\nFill the unanswered required fields
|
|
576
|
-
`
|
|
600
|
+
console.log(`\nFill the unanswered required fields. Easiest path:\n` +
|
|
601
|
+
` 1. job-pro ${company} apply ${postId} --print-form > form.json\n` +
|
|
602
|
+
` 2. Edit form.json — set each \`value\` for required fields.\n` +
|
|
603
|
+
` 3. job-pro ${company} apply ${postId} --form-file form.json\n` +
|
|
604
|
+
`Or paste the following into ${profileTemplate().path} under \`custom\`:`);
|
|
605
|
+
// Emit a copy-pasteable JSON snippet listing each unanswered required.
|
|
606
|
+
const snippet = {};
|
|
607
|
+
for (const f of staged.unanswered_required)
|
|
608
|
+
snippet[f.name] = "";
|
|
609
|
+
console.log(JSON.stringify({ custom: snippet }, null, 2));
|
|
577
610
|
}
|
|
578
611
|
else {
|
|
579
612
|
const isAnon = staged.source.startsWith("boards-api.greenhouse.io/") ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|