@ha7ch/job-pro 1.0.93
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/adapter.js +17 -0
- package/dist/agibot.js +399 -0
- package/dist/alibaba.js +509 -0
- package/dist/antgroup.js +397 -0
- package/dist/apply.js +1373 -0
- package/dist/baichuan.js +49 -0
- package/dist/baidu.js +452 -0
- package/dist/bilibili.js +455 -0
- package/dist/byd.js +412 -0
- package/dist/bytedance.js +619 -0
- package/dist/cainiao.js +56 -0
- package/dist/cambricon.js +33 -0
- package/dist/cdp.js +237 -0
- package/dist/cicc.js +56 -0
- package/dist/coverage.js +60 -0
- package/dist/deepseek.js +25 -0
- package/dist/didi.js +381 -0
- package/dist/feishu.js +577 -0
- package/dist/galaxyuniversal.js +24 -0
- package/dist/geely.js +35 -0
- package/dist/greenhouse.js +432 -0
- package/dist/hikvision.js +58 -0
- package/dist/horizonrobotics.js +46 -0
- package/dist/hoyoverse.js +26 -0
- package/dist/huawei.js +537 -0
- package/dist/iflytek.js +380 -0
- package/dist/index.js +1828 -0
- package/dist/iqiyi.js +494 -0
- package/dist/jd.js +559 -0
- package/dist/kuaishou.js +496 -0
- package/dist/lever.js +455 -0
- package/dist/liauto.js +393 -0
- package/dist/liepin.js +357 -0
- package/dist/lilith.js +300 -0
- package/dist/megvii.js +27 -0
- package/dist/meituan.js +633 -0
- package/dist/memory.js +76 -0
- package/dist/mihoyo.js +308 -0
- package/dist/minimax.js +32 -0
- package/dist/moka.js +473 -0
- package/dist/moonshot.js +24 -0
- package/dist/netease.js +424 -0
- package/dist/nio.js +24 -0
- package/dist/oppo.js +285 -0
- package/dist/pdd.js +614 -0
- package/dist/pingan.js +493 -0
- package/dist/sensetime.js +51 -0
- package/dist/sf.js +310 -0
- package/dist/stepfun.js +24 -0
- package/dist/tencent.js +770 -0
- package/dist/trip.js +396 -0
- package/dist/unitree.js +418 -0
- package/dist/vivo.js +361 -0
- package/dist/webank.js +55 -0
- package/dist/wecruit.js +438 -0
- package/dist/weibo.js +337 -0
- package/dist/weride.js +29 -0
- package/dist/xiaohongshu.js +480 -0
- package/dist/xiaomi.js +529 -0
- package/dist/xpeng.js +34 -0
- package/dist/zerooneai.js +42 -0
- package/dist/zhipu.js +478 -0
- package/extension/README.md +79 -0
- package/extension/background.js +177 -0
- package/extension/manifest.json +55 -0
- package/extension/popup.html +37 -0
- package/extension/popup.js +54 -0
- package/package.json +61 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// Generic Greenhouse Boards adapter factory.
|
|
2
|
+
//
|
|
3
|
+
// Greenhouse (boards-api.greenhouse.io) is a widely-used SaaS ATS. Multiple
|
|
4
|
+
// Chinese companies (or their international arms) self-host their public job
|
|
5
|
+
// board on a `<slug>` namespace there. The unauthenticated REST surface is
|
|
6
|
+
// stable across tenants:
|
|
7
|
+
//
|
|
8
|
+
// GET https://boards-api.greenhouse.io/v1/boards/<slug>/jobs
|
|
9
|
+
// → { jobs: [...], meta: { total: <int> } }
|
|
10
|
+
//
|
|
11
|
+
// GET https://boards-api.greenhouse.io/v1/boards/<slug>/jobs/<id>?content=true
|
|
12
|
+
// → full job object including the rendered description HTML
|
|
13
|
+
//
|
|
14
|
+
// GET https://boards-api.greenhouse.io/v1/boards/<slug>/departments
|
|
15
|
+
// → { departments: [{ id, name, child_ids[], parent_id }] }
|
|
16
|
+
//
|
|
17
|
+
// GET https://boards-api.greenhouse.io/v1/boards/<slug>/offices
|
|
18
|
+
// → { offices: [{ id, name, location, child_ids[], parent_id }] }
|
|
19
|
+
//
|
|
20
|
+
// All endpoints are GET-only, return JSON, and require no auth headers.
|
|
21
|
+
//
|
|
22
|
+
// ---- PositionSummary field mapping (Greenhouse → canonical) ----
|
|
23
|
+
// post_id ← String(job.id)
|
|
24
|
+
// title ← job.title
|
|
25
|
+
// project ← job.departments[0]?.name (or "")
|
|
26
|
+
// recruit_label ← job.metadata where name matches "Employment Type" (else "")
|
|
27
|
+
// bgs ← "" (Greenhouse has no BG dimension)
|
|
28
|
+
// work_cities ← job.location.name
|
|
29
|
+
// apply_url ← job.absolute_url
|
|
30
|
+
//
|
|
31
|
+
// ---- Discovery notes ----
|
|
32
|
+
// * Greenhouse returns the full job list in a single call — no pagination is
|
|
33
|
+
// required for ATS sizes seen so far (<2000 jobs).
|
|
34
|
+
// * The `meta.total` field is always present.
|
|
35
|
+
// * `content=true` on the detail endpoint returns description as escaped HTML.
|
|
36
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
37
|
+
export { checkResume };
|
|
38
|
+
// ---------- createAdapter ----------
|
|
39
|
+
export function createAdapter(cfg) {
|
|
40
|
+
const API_ROOT = `https://boards-api.greenhouse.io/v1/boards/${encodeURIComponent(cfg.slug)}`;
|
|
41
|
+
const SOURCE = `boards-api.greenhouse.io/${cfg.slug}`;
|
|
42
|
+
const BOARD_URL = `https://job-boards.greenhouse.io/${encodeURIComponent(cfg.slug)}`;
|
|
43
|
+
const HEADERS = {
|
|
44
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
45
|
+
Accept: "application/json",
|
|
46
|
+
};
|
|
47
|
+
function summarize(job) {
|
|
48
|
+
const id = String(job.id ?? "");
|
|
49
|
+
const dept = job.departments?.[0]?.name ?? "";
|
|
50
|
+
const employmentType = (job.metadata ?? []).find((m) => (m.name ?? "").toLowerCase() === "employment type")?.value;
|
|
51
|
+
const recruit_label = typeof employmentType === "string" ? employmentType : "";
|
|
52
|
+
return {
|
|
53
|
+
post_id: id,
|
|
54
|
+
title: job.title ?? "",
|
|
55
|
+
project: dept,
|
|
56
|
+
recruit_label,
|
|
57
|
+
bgs: "",
|
|
58
|
+
work_cities: job.location?.name ?? "",
|
|
59
|
+
apply_url: job.absolute_url ?? `${BOARD_URL}/jobs/${id}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
let _allCache = null;
|
|
63
|
+
async function fetchAllRaw() {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (_allCache && now - _allCache.fetchedAt < 5 * 60 * 1000) {
|
|
66
|
+
return _allCache.ok ? { ok: true, jobs: _allCache.jobs } : { ok: false, message: _allCache.message };
|
|
67
|
+
}
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetch(`${API_ROOT}/jobs?content=false`, { headers: HEADERS });
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const msg = `network error: ${err instanceof Error ? err.message : String(err)}`;
|
|
74
|
+
_allCache = { ok: false, message: msg, fetchedAt: now };
|
|
75
|
+
return { ok: false, message: msg };
|
|
76
|
+
}
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const msg = `HTTP ${response.status}: ${response.statusText}`;
|
|
79
|
+
_allCache = { ok: false, message: msg, fetchedAt: now };
|
|
80
|
+
return { ok: false, message: msg };
|
|
81
|
+
}
|
|
82
|
+
let payload;
|
|
83
|
+
try {
|
|
84
|
+
payload = (await response.json());
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const msg = `bad JSON: ${err instanceof Error ? err.message : String(err)}`;
|
|
88
|
+
_allCache = { ok: false, message: msg, fetchedAt: now };
|
|
89
|
+
return { ok: false, message: msg };
|
|
90
|
+
}
|
|
91
|
+
const jobs = payload.jobs ?? [];
|
|
92
|
+
_allCache = { ok: true, jobs, fetchedAt: now };
|
|
93
|
+
return { ok: true, jobs };
|
|
94
|
+
}
|
|
95
|
+
function applyFilters(jobs, opts) {
|
|
96
|
+
const kw = (opts.keyword ?? "").trim().toLowerCase();
|
|
97
|
+
const deptFilters = (opts.departments ?? []).map((s) => String(s).toLowerCase());
|
|
98
|
+
const cityFilters = (opts.cities ?? []).map((s) => String(s).toLowerCase());
|
|
99
|
+
return jobs.filter((job) => {
|
|
100
|
+
if (kw) {
|
|
101
|
+
const blob = [
|
|
102
|
+
job.title ?? "",
|
|
103
|
+
job.location?.name ?? "",
|
|
104
|
+
(job.departments ?? []).map((d) => d.name).join(" "),
|
|
105
|
+
]
|
|
106
|
+
.join(" ")
|
|
107
|
+
.toLowerCase();
|
|
108
|
+
if (!blob.includes(kw))
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (deptFilters.length) {
|
|
112
|
+
const blob = (job.departments ?? [])
|
|
113
|
+
.map((d) => (d.name ?? "").toLowerCase())
|
|
114
|
+
.join(" ");
|
|
115
|
+
if (!deptFilters.some((d) => blob.includes(d)))
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (cityFilters.length) {
|
|
119
|
+
const blob = (job.location?.name ?? "").toLowerCase();
|
|
120
|
+
if (!cityFilters.some((c) => blob.includes(c)))
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function searchPositions(opts = {}) {
|
|
127
|
+
const pageSize = Math.max(1, Math.min(200, opts.pageSize ?? 20));
|
|
128
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
129
|
+
const pool = await fetchAllRaw();
|
|
130
|
+
if (!pool.ok) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
message: pool.message,
|
|
134
|
+
source: SOURCE,
|
|
135
|
+
apply_url: BOARD_URL,
|
|
136
|
+
positions: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const filtered = applyFilters(pool.jobs, opts);
|
|
140
|
+
const offset = (page - 1) * pageSize;
|
|
141
|
+
const paginated = filtered.slice(offset, offset + pageSize);
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
source: SOURCE,
|
|
145
|
+
query: opts,
|
|
146
|
+
page,
|
|
147
|
+
page_size: pageSize,
|
|
148
|
+
total: filtered.length,
|
|
149
|
+
positions: paginated.map(summarize),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function fetchAllPositions(opts = {}) {
|
|
153
|
+
const pool = await fetchAllRaw();
|
|
154
|
+
if (!pool.ok) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
message: pool.message,
|
|
158
|
+
source: SOURCE,
|
|
159
|
+
apply_url: BOARD_URL,
|
|
160
|
+
fetched: 0,
|
|
161
|
+
positions: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const filtered = applyFilters(pool.jobs, opts);
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
source: SOURCE,
|
|
168
|
+
total: filtered.length,
|
|
169
|
+
fetched: filtered.length,
|
|
170
|
+
positions: filtered.map(summarize),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async function fetchPositionDetail(postId) {
|
|
174
|
+
const id = (postId ?? "").trim();
|
|
175
|
+
if (!id) {
|
|
176
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
177
|
+
}
|
|
178
|
+
let response;
|
|
179
|
+
try {
|
|
180
|
+
response = await fetch(`${API_ROOT}/jobs/${encodeURIComponent(id)}?content=true`, { headers: HEADERS });
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
source: SOURCE,
|
|
186
|
+
post_id: id,
|
|
187
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
source: SOURCE,
|
|
194
|
+
post_id: id,
|
|
195
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
let job;
|
|
199
|
+
try {
|
|
200
|
+
job = (await response.json());
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
source: SOURCE,
|
|
206
|
+
post_id: id,
|
|
207
|
+
message: `bad JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const summary = summarize(job);
|
|
211
|
+
const html = job.content ?? "";
|
|
212
|
+
// Crude HTML-to-text: decode common entities, strip tags.
|
|
213
|
+
const description = html
|
|
214
|
+
.replace(/<[^>]+>/g, " ")
|
|
215
|
+
.replace(/ /g, " ")
|
|
216
|
+
.replace(/&/g, "&")
|
|
217
|
+
.replace(/</g, "<")
|
|
218
|
+
.replace(/>/g, ">")
|
|
219
|
+
.replace(/"/g, '"')
|
|
220
|
+
.replace(/'/g, "'")
|
|
221
|
+
.replace(/\s+/g, " ")
|
|
222
|
+
.trim();
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
source: SOURCE,
|
|
226
|
+
post_id: id,
|
|
227
|
+
title: job.title ?? "",
|
|
228
|
+
project: summary.project,
|
|
229
|
+
recruit_label: summary.recruit_label,
|
|
230
|
+
requisition_id: job.requisition_id ?? "",
|
|
231
|
+
first_published: job.first_published ?? "",
|
|
232
|
+
updated_at: job.updated_at ?? "",
|
|
233
|
+
description,
|
|
234
|
+
work_cities: job.location?.name ?? "",
|
|
235
|
+
apply_url: summary.apply_url,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// ---------- fetchDictionaries ----------
|
|
239
|
+
let _dictCache = null;
|
|
240
|
+
async function fetchDictionaries() {
|
|
241
|
+
if (_dictCache !== null)
|
|
242
|
+
return _dictCache;
|
|
243
|
+
try {
|
|
244
|
+
const [deptRes, offRes] = await Promise.all([
|
|
245
|
+
fetch(`${API_ROOT}/departments`, { headers: HEADERS }),
|
|
246
|
+
fetch(`${API_ROOT}/offices`, { headers: HEADERS }),
|
|
247
|
+
]);
|
|
248
|
+
if (!deptRes.ok && !offRes.ok) {
|
|
249
|
+
const r = {
|
|
250
|
+
ok: false,
|
|
251
|
+
source: SOURCE,
|
|
252
|
+
message: `HTTP ${deptRes.status}/${offRes.status}`,
|
|
253
|
+
};
|
|
254
|
+
_dictCache = r;
|
|
255
|
+
return r;
|
|
256
|
+
}
|
|
257
|
+
const deptJson = deptRes.ok
|
|
258
|
+
? (await deptRes.json())
|
|
259
|
+
: { departments: [] };
|
|
260
|
+
const offJson = offRes.ok
|
|
261
|
+
? (await offRes.json())
|
|
262
|
+
: { offices: [] };
|
|
263
|
+
const result = {
|
|
264
|
+
ok: true,
|
|
265
|
+
source: SOURCE,
|
|
266
|
+
departments: (deptJson.departments ?? []).map((d) => ({
|
|
267
|
+
id: d.id ?? 0,
|
|
268
|
+
name: d.name ?? "",
|
|
269
|
+
parent_id: d.parent_id ?? null,
|
|
270
|
+
})),
|
|
271
|
+
offices: (offJson.offices ?? []).map((o) => ({
|
|
272
|
+
id: o.id ?? 0,
|
|
273
|
+
name: o.name ?? "",
|
|
274
|
+
location: o.location ?? "",
|
|
275
|
+
parent_id: o.parent_id ?? null,
|
|
276
|
+
})),
|
|
277
|
+
};
|
|
278
|
+
_dictCache = result;
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
const r = {
|
|
283
|
+
ok: false,
|
|
284
|
+
source: SOURCE,
|
|
285
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
286
|
+
};
|
|
287
|
+
_dictCache = r;
|
|
288
|
+
return r;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ---------- notices (stub) ----------
|
|
292
|
+
const NOTICES_STUB = {
|
|
293
|
+
ok: false,
|
|
294
|
+
source: SOURCE,
|
|
295
|
+
message: `${cfg.label}: Greenhouse boards have no announcements endpoint`,
|
|
296
|
+
};
|
|
297
|
+
async function listNotices() {
|
|
298
|
+
return NOTICES_STUB;
|
|
299
|
+
}
|
|
300
|
+
async function getNotice(_id) {
|
|
301
|
+
return NOTICES_STUB;
|
|
302
|
+
}
|
|
303
|
+
async function findNoticesByQuestion(_question, _opts = {}) {
|
|
304
|
+
return NOTICES_STUB;
|
|
305
|
+
}
|
|
306
|
+
// ---------- matchResume ----------
|
|
307
|
+
async function matchResume(text, opts = {}) {
|
|
308
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
309
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
310
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
311
|
+
if (!terms.length) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
source: SOURCE,
|
|
315
|
+
message: "could not extract any technical signals from the text",
|
|
316
|
+
preview: (text ?? "").slice(0, 120),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const pool = await fetchAllRaw();
|
|
320
|
+
if (!pool.ok) {
|
|
321
|
+
return { ok: false, source: SOURCE, message: pool.message, positions: [] };
|
|
322
|
+
}
|
|
323
|
+
const scored = [];
|
|
324
|
+
for (const job of pool.jobs) {
|
|
325
|
+
const blob = [
|
|
326
|
+
job.title ?? "",
|
|
327
|
+
job.location?.name ?? "",
|
|
328
|
+
(job.departments ?? []).map((d) => d.name).join(" "),
|
|
329
|
+
].join(" ");
|
|
330
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
331
|
+
if (score > 0)
|
|
332
|
+
scored.push({ score, raw: job, reasons });
|
|
333
|
+
}
|
|
334
|
+
scored.sort((a, b) => b.score - a.score);
|
|
335
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
336
|
+
if (!shortlist.length) {
|
|
337
|
+
shortlist = pool.jobs.slice(0, candidates).map((raw) => ({
|
|
338
|
+
score: 0,
|
|
339
|
+
raw,
|
|
340
|
+
reasons: [],
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
344
|
+
const mr = s.reasons.length > 0
|
|
345
|
+
? s.reasons.slice(0, 5)
|
|
346
|
+
: ["no specific keyword overlap — surfaced from full board listing"];
|
|
347
|
+
return { ...summarize(s.raw), match_reasons: mr };
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
ok: true,
|
|
351
|
+
source: SOURCE,
|
|
352
|
+
extracted_terms: terms,
|
|
353
|
+
city_preferences: cities,
|
|
354
|
+
matches,
|
|
355
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
356
|
+
"The only authority on selection is HR.",
|
|
357
|
+
};
|
|
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
|
+
submit_kind: "multipart-anon",
|
|
413
|
+
endpoint_verified: true,
|
|
414
|
+
submit_notes: "Greenhouse Job Board API accepts anonymous multipart/form-data POSTs " +
|
|
415
|
+
"whose field names match the questions[].fields[].name returned here.",
|
|
416
|
+
questions,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
searchPositions,
|
|
422
|
+
fetchAllPositions,
|
|
423
|
+
fetchPositionDetail,
|
|
424
|
+
fetchDictionaries,
|
|
425
|
+
listNotices,
|
|
426
|
+
getNotice,
|
|
427
|
+
findNoticesByQuestion,
|
|
428
|
+
matchResume,
|
|
429
|
+
checkResume,
|
|
430
|
+
fetchApplicationSchema,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// 海康威视 / Hikvision careers adapter — Liepin aggregator fallback.
|
|
2
|
+
//
|
|
3
|
+
// hr.hikvision.com is gated by Tencent EdgeOne which 403s any non-CN IP
|
|
4
|
+
// regardless of cookies. www.hikvision.com.cn has no public DNS A record
|
|
5
|
+
// outside Mainland China. There is no third-party ATS tenant.
|
|
6
|
+
//
|
|
7
|
+
// Until a CN-egress proxy path lands (set `JOB_PRO_HTTPS_PROXY` and see
|
|
8
|
+
// the historical CDP-driven adapter at `git log cli/src/hikvision.ts`),
|
|
9
|
+
// we surface real currently-open Hikvision positions by querying Liepin
|
|
10
|
+
// (api-c.liepin.com) filtered by compName="海康威视". See
|
|
11
|
+
// `cli/src/liepin.ts` for the shared factory.
|
|
12
|
+
//
|
|
13
|
+
// Source: api-c.liepin.com (`source` field on responses) — clearly NOT
|
|
14
|
+
// the same as Hikvision's own portal.
|
|
15
|
+
import { createAdapter } from "./liepin.js";
|
|
16
|
+
const adapter = createAdapter({
|
|
17
|
+
companyName: "海康威视",
|
|
18
|
+
label: "Hikvision / 海康威视",
|
|
19
|
+
});
|
|
20
|
+
export const searchPositions = adapter.searchPositions;
|
|
21
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
22
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
23
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
24
|
+
export const listNotices = adapter.listNotices;
|
|
25
|
+
export const getNotice = adapter.getNotice;
|
|
26
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
27
|
+
export const matchResume = adapter.matchResume;
|
|
28
|
+
export const checkResume = adapter.checkResume;
|
|
29
|
+
import { buildBespokeApplySchema as _buildBespokeApplySchema_hikvision } from "./apply.js";
|
|
30
|
+
export async function fetchApplicationSchema(postId) {
|
|
31
|
+
const id = (postId ?? "").trim();
|
|
32
|
+
if (!id)
|
|
33
|
+
return { ok: false, source: "hikvision.com (via api-c.liepin.com)", message: "post_id is required" };
|
|
34
|
+
let title = "";
|
|
35
|
+
let applyUrl = "https://hikvision.com";
|
|
36
|
+
try {
|
|
37
|
+
const detail = (await fetchPositionDetail(id));
|
|
38
|
+
if (detail?.ok === false) {
|
|
39
|
+
return { ok: false, source: "hikvision.com (via api-c.liepin.com)", message: detail.message ?? "post not found" };
|
|
40
|
+
}
|
|
41
|
+
title = detail?.title ?? "";
|
|
42
|
+
if (detail?.apply_url)
|
|
43
|
+
applyUrl = detail.apply_url;
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
schema: _buildBespokeApplySchema_hikvision({
|
|
49
|
+
source: "hikvision.com (via api-c.liepin.com)",
|
|
50
|
+
postId: id,
|
|
51
|
+
jobTitle: title,
|
|
52
|
+
applyUrl,
|
|
53
|
+
submitEndpoint: undefined,
|
|
54
|
+
submitKind: "external",
|
|
55
|
+
submitNotes: "Hikvision (Liepin-backed) — submission is recruiter-IM-mediated through Liepin. Open the apply_url to start the chat.",
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// 地平线 (Horizon Robotics) careers adapter for `job-pro`.
|
|
2
|
+
//
|
|
3
|
+
// ============================================================
|
|
4
|
+
// API DISCOVERY (probed 2026-05-16 via puppeteer-core network capture)
|
|
5
|
+
//
|
|
6
|
+
// Horizon's careers run on `wecruit.hotjob.cn`, the same Beisen Wecruit
|
|
7
|
+
// stack as SenseTime (see cli/src/sensetime.ts). The `/{SU…}/pb/<channel>.html`
|
|
8
|
+
// SPA path returns nginx 405 on any anonymous POST. The real XHR is fired
|
|
9
|
+
// at the sibling `/wecruit/positionInfo/listPosition/{SU…}` route.
|
|
10
|
+
//
|
|
11
|
+
// Channels (probed 2026-05-16):
|
|
12
|
+
// * school — `SU6409ef49bef57c635fd390a6` (校园招聘 / 实习生) ~84 positions
|
|
13
|
+
// * social — `SU64819a4f2f9d2433ba8b043a` (社会招聘) ~216 positions
|
|
14
|
+
//
|
|
15
|
+
// Anonymous, no token, no cookie. See cli/src/wecruit.ts for the shared
|
|
16
|
+
// factory: POST to `/wecruit/positionInfo/listPosition/{channelId}` with
|
|
17
|
+
// `application/x-www-form-urlencoded` body containing
|
|
18
|
+
// `isFrompb=true&recruitType=<1|2>&pageSize=N¤tPage=N`. Response is
|
|
19
|
+
// `{ data:{ pageForm:{ totalPage, pageData[…] } }, state:"200" }`.
|
|
20
|
+
import { createAdapter } from "./wecruit.js";
|
|
21
|
+
const adapter = createAdapter({
|
|
22
|
+
host: "wecruit.hotjob.cn",
|
|
23
|
+
label: "Horizon Robotics",
|
|
24
|
+
channels: [
|
|
25
|
+
{
|
|
26
|
+
channelId: "SU6409ef49bef57c635fd390a6",
|
|
27
|
+
recruitType: "campus",
|
|
28
|
+
pagePath: "school",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
channelId: "SU64819a4f2f9d2433ba8b043a",
|
|
32
|
+
recruitType: "social",
|
|
33
|
+
pagePath: "social",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
export const searchPositions = adapter.searchPositions;
|
|
38
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
39
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
40
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
41
|
+
export const listNotices = adapter.listNotices;
|
|
42
|
+
export const getNotice = adapter.getNotice;
|
|
43
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
44
|
+
export const matchResume = adapter.matchResume;
|
|
45
|
+
export const checkResume = adapter.checkResume;
|
|
46
|
+
export const fetchApplicationSchema = adapter.fetchApplicationSchema;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Thin wrapper for HoYoverse careers (miHoYo's international brand), hosted on Greenhouse.
|
|
2
|
+
//
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Discovery notes (probed 2026-05):
|
|
5
|
+
//
|
|
6
|
+
// Live endpoint: https://boards-api.greenhouse.io/v1/boards/hoyoverse/jobs
|
|
7
|
+
// Greenhouse slug: hoyoverse
|
|
8
|
+
// Tenant: HoYoverse (international operations of 米哈游 / miHoYo)
|
|
9
|
+
// Total positions: ~28 (probed 2026-05) — Singapore / Montreal /
|
|
10
|
+
// Santa Monica game-dev, art, engineering, and ops.
|
|
11
|
+
//
|
|
12
|
+
// Note: this is the international Greenhouse board. The China-side campus
|
|
13
|
+
// board lives at https://campus.mihoyo.com (covered by the `mihoyo` adapter
|
|
14
|
+
// as a stub since that SPA has no public unauthenticated JSON endpoint).
|
|
15
|
+
import { createAdapter } from "./greenhouse.js";
|
|
16
|
+
const adapter = createAdapter({ slug: "hoyoverse", label: "HoYoverse" });
|
|
17
|
+
export const searchPositions = adapter.searchPositions;
|
|
18
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
19
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
20
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
21
|
+
export const listNotices = adapter.listNotices;
|
|
22
|
+
export const getNotice = adapter.getNotice;
|
|
23
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
24
|
+
export const matchResume = adapter.matchResume;
|
|
25
|
+
export const checkResume = adapter.checkResume;
|
|
26
|
+
export const fetchApplicationSchema = adapter.fetchApplicationSchema;
|