@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.
Files changed (68) hide show
  1. package/dist/adapter.js +17 -0
  2. package/dist/agibot.js +399 -0
  3. package/dist/alibaba.js +509 -0
  4. package/dist/antgroup.js +397 -0
  5. package/dist/apply.js +1373 -0
  6. package/dist/baichuan.js +49 -0
  7. package/dist/baidu.js +452 -0
  8. package/dist/bilibili.js +455 -0
  9. package/dist/byd.js +412 -0
  10. package/dist/bytedance.js +619 -0
  11. package/dist/cainiao.js +56 -0
  12. package/dist/cambricon.js +33 -0
  13. package/dist/cdp.js +237 -0
  14. package/dist/cicc.js +56 -0
  15. package/dist/coverage.js +60 -0
  16. package/dist/deepseek.js +25 -0
  17. package/dist/didi.js +381 -0
  18. package/dist/feishu.js +577 -0
  19. package/dist/galaxyuniversal.js +24 -0
  20. package/dist/geely.js +35 -0
  21. package/dist/greenhouse.js +432 -0
  22. package/dist/hikvision.js +58 -0
  23. package/dist/horizonrobotics.js +46 -0
  24. package/dist/hoyoverse.js +26 -0
  25. package/dist/huawei.js +537 -0
  26. package/dist/iflytek.js +380 -0
  27. package/dist/index.js +1828 -0
  28. package/dist/iqiyi.js +494 -0
  29. package/dist/jd.js +559 -0
  30. package/dist/kuaishou.js +496 -0
  31. package/dist/lever.js +455 -0
  32. package/dist/liauto.js +393 -0
  33. package/dist/liepin.js +357 -0
  34. package/dist/lilith.js +300 -0
  35. package/dist/megvii.js +27 -0
  36. package/dist/meituan.js +633 -0
  37. package/dist/memory.js +76 -0
  38. package/dist/mihoyo.js +308 -0
  39. package/dist/minimax.js +32 -0
  40. package/dist/moka.js +473 -0
  41. package/dist/moonshot.js +24 -0
  42. package/dist/netease.js +424 -0
  43. package/dist/nio.js +24 -0
  44. package/dist/oppo.js +285 -0
  45. package/dist/pdd.js +614 -0
  46. package/dist/pingan.js +493 -0
  47. package/dist/sensetime.js +51 -0
  48. package/dist/sf.js +310 -0
  49. package/dist/stepfun.js +24 -0
  50. package/dist/tencent.js +770 -0
  51. package/dist/trip.js +396 -0
  52. package/dist/unitree.js +418 -0
  53. package/dist/vivo.js +361 -0
  54. package/dist/webank.js +55 -0
  55. package/dist/wecruit.js +438 -0
  56. package/dist/weibo.js +337 -0
  57. package/dist/weride.js +29 -0
  58. package/dist/xiaohongshu.js +480 -0
  59. package/dist/xiaomi.js +529 -0
  60. package/dist/xpeng.js +34 -0
  61. package/dist/zerooneai.js +42 -0
  62. package/dist/zhipu.js +478 -0
  63. package/extension/README.md +79 -0
  64. package/extension/background.js +177 -0
  65. package/extension/manifest.json +55 -0
  66. package/extension/popup.html +37 -0
  67. package/extension/popup.js +54 -0
  68. package/package.json +61 -0
package/dist/vivo.js ADDED
@@ -0,0 +1,361 @@
1
+ // vivo careers adapter for `job-pro`.
2
+ //
3
+ // ============================================================
4
+ // API DISCOVERY (probed 2026-05-15)
5
+ //
6
+ // hr.vivo.com is vivo's *internal* BPM portal SPA and serves an all-paths-match
7
+ // catchall that returns its own HTML for every URL. The actual public careers
8
+ // site is a Beisen (北森) recruitment-portal tenant hosted at
9
+ // https://vivo.zhiye.com/ (tenant id 612022).
10
+ //
11
+ // The Beisen 2022 portal exposes a single paginated POST endpoint that backs
12
+ // every job-listing widget (社招 / 校招 / 全部职位 / 实习生):
13
+ //
14
+ // POST /api/Jobad/GetJobAdPageList
15
+ //
16
+ // The endpoint is anonymous; the only required headers are a real browser
17
+ // User-Agent, Content-Type=application/json, and a vivo.zhiye.com Referer.
18
+ // `Category` filters by recruit type:
19
+ //
20
+ // "1" 全部 / unspecified (default in widget config)
21
+ // "4" 员工社招 (social hire)
22
+ // "5" 员工校招 (campus hire)
23
+ // "2" 校园招聘 (templated campus)
24
+ // "3" 实习生
25
+ //
26
+ // Each position record exposes `Category` (Chinese label) and `CategoryId`.
27
+ //
28
+ // Endpoint inventory (anonymous POST, content-type application/json):
29
+ // POST /api/Jobad/GetJobAdPageList → paginated job list
30
+ // POST /api/Jobad/GetJobAdSearchConditions → filter taxonomy
31
+ // GET /api/Jobad/GetSpecialJobAdList → hot/special jobs
32
+ // GET /api/Jobad/SearchAreasTreeConditions → city tree
33
+ // GET /api/Common/GetPortalAIRobot → portal config
34
+ //
35
+ // ============================================================
36
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
37
+ export { checkResume };
38
+ const SOURCE = "vivo.zhiye.com";
39
+ const API_ROOT = "https://vivo.zhiye.com";
40
+ const SITE_ROOT = "https://vivo.zhiye.com/jobs";
41
+ const DETAIL_PAGE = (id) => `https://vivo.zhiye.com/jobs?jobAdId=${encodeURIComponent(id)}`;
42
+ const DEFAULT_HEADERS = {
43
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
44
+ Accept: "application/json",
45
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
46
+ Referer: SITE_ROOT,
47
+ Origin: API_ROOT,
48
+ "x-requested-with": "xmlhttprequest",
49
+ langtype: "zh_CN",
50
+ };
51
+ async function post(path, body) {
52
+ let response;
53
+ try {
54
+ response = await fetch(`${API_ROOT}${path}`, {
55
+ method: "POST",
56
+ headers: { ...DEFAULT_HEADERS, "Content-Type": "application/json" },
57
+ body: JSON.stringify(body),
58
+ });
59
+ }
60
+ catch (err) {
61
+ return {
62
+ ok: false,
63
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
64
+ };
65
+ }
66
+ if (!response.ok) {
67
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
68
+ }
69
+ let payload;
70
+ try {
71
+ payload = (await response.json());
72
+ }
73
+ catch (err) {
74
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
75
+ }
76
+ return {
77
+ ok: payload.Code === 200,
78
+ data: payload.Data,
79
+ count: payload.Count ?? payload.Total,
80
+ message: payload.Message || (payload.Code === 200 ? "ok" : "upstream error"),
81
+ };
82
+ }
83
+ async function get(path) {
84
+ let response;
85
+ try {
86
+ response = await fetch(`${API_ROOT}${path}`, { method: "GET", headers: DEFAULT_HEADERS });
87
+ }
88
+ catch (err) {
89
+ return {
90
+ ok: false,
91
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
92
+ };
93
+ }
94
+ if (!response.ok) {
95
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
96
+ }
97
+ let payload;
98
+ try {
99
+ payload = (await response.json());
100
+ }
101
+ catch (err) {
102
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
103
+ }
104
+ return {
105
+ ok: payload.Code === 200,
106
+ data: payload.Data,
107
+ message: payload.Message || (payload.Code === 200 ? "ok" : "upstream error"),
108
+ };
109
+ }
110
+ function summarize(item) {
111
+ const id = String(item.JobAdId ?? item.Id ?? "");
112
+ const cities = Array.isArray(item.LocNames) ? item.LocNames.join(", ") : "";
113
+ return {
114
+ post_id: id,
115
+ title: (item.JobAdName ?? "").trim(),
116
+ project: (item.Org ?? "").trim(),
117
+ recruit_label: (item.Category ?? "").trim(),
118
+ bgs: "",
119
+ work_cities: cities,
120
+ apply_url: id ? DETAIL_PAGE(id) : SITE_ROOT,
121
+ };
122
+ }
123
+ function categoryFromRecruitType(t) {
124
+ switch (t) {
125
+ case "social":
126
+ return ["4"];
127
+ case "campus":
128
+ return ["5"];
129
+ case "intern":
130
+ return ["3"];
131
+ case "all":
132
+ default:
133
+ return undefined;
134
+ }
135
+ }
136
+ // ---------- searchPositions ----------
137
+ export async function searchPositions(opts = {}) {
138
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 20));
139
+ const page = Math.max(1, opts.page ?? 1);
140
+ // Beisen pageIndex is zero-based.
141
+ const body = {
142
+ PageIndex: page - 1,
143
+ PageSize: pageSize,
144
+ KeyWords: (opts.keyword ?? "").trim().slice(0, 60),
145
+ SpecialType: 0,
146
+ PortalId: "",
147
+ DisplayFields: ["Category", "Kind", "LocId", "Org", "HeadCount", "PostDate", "Salary"],
148
+ };
149
+ const category = categoryFromRecruitType(opts.recruitType);
150
+ if (category)
151
+ body.Category = category;
152
+ const r = await post("/api/Jobad/GetJobAdPageList", body);
153
+ if (!r.ok) {
154
+ return {
155
+ ok: false,
156
+ source: SOURCE,
157
+ message: r.message,
158
+ query: body,
159
+ positions: [],
160
+ };
161
+ }
162
+ const rows = r.data ?? [];
163
+ return {
164
+ ok: true,
165
+ source: SOURCE,
166
+ query: body,
167
+ page,
168
+ page_size: pageSize,
169
+ total: r.count ?? rows.length,
170
+ positions: rows.map(summarize),
171
+ };
172
+ }
173
+ // ---------- fetchAllPositions ----------
174
+ export async function fetchAllPositions(opts = {}) {
175
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 30));
176
+ const maxPages = Math.max(1, opts.maxPages ?? 20);
177
+ const bucket = [];
178
+ let total;
179
+ for (let page = 1; page <= maxPages; page++) {
180
+ const r = await searchPositions({
181
+ keyword: opts.keyword,
182
+ page,
183
+ pageSize,
184
+ recruitType: opts.recruitType,
185
+ });
186
+ if (!r.ok) {
187
+ return {
188
+ ok: false,
189
+ source: SOURCE,
190
+ message: r.message,
191
+ total: 0,
192
+ fetched: bucket.length,
193
+ positions: bucket,
194
+ };
195
+ }
196
+ if (total === undefined)
197
+ total = r.total;
198
+ if (!r.positions.length)
199
+ break;
200
+ bucket.push(...r.positions);
201
+ if (total !== undefined && bucket.length >= total)
202
+ break;
203
+ }
204
+ return {
205
+ ok: true,
206
+ source: SOURCE,
207
+ total: total ?? bucket.length,
208
+ fetched: bucket.length,
209
+ positions: bucket,
210
+ };
211
+ }
212
+ // ---------- fetchPositionDetail ----------
213
+ //
214
+ // Beisen returns the full duty/require text directly on the list endpoint
215
+ // when DisplayFields is omitted or includes those keys. We therefore call
216
+ // GetJobAdPageList with the exact JobAdId to recover a single record.
217
+ export async function fetchPositionDetail(postId) {
218
+ const id = (postId ?? "").trim();
219
+ if (!id)
220
+ return { ok: false, source: SOURCE, message: "post_id is required", post_id: id };
221
+ const r = await post("/api/Jobad/GetJobAdPageList", {
222
+ PageIndex: 0,
223
+ PageSize: 1,
224
+ KeyWords: "",
225
+ SpecialType: 0,
226
+ PortalId: "",
227
+ JobAdIds: [Number(id) || id],
228
+ DisplayFields: [
229
+ "Category",
230
+ "Kind",
231
+ "LocId",
232
+ "Org",
233
+ "HeadCount",
234
+ "PostDate",
235
+ "Salary",
236
+ "DetailAddress",
237
+ ],
238
+ });
239
+ if (!r.ok || !r.data || !r.data.length) {
240
+ return { ok: false, source: SOURCE, message: r.message || "no detail returned", post_id: id };
241
+ }
242
+ const raw = r.data[0];
243
+ return {
244
+ ok: true,
245
+ source: SOURCE,
246
+ post_id: String(raw.JobAdId ?? id),
247
+ title: raw.JobAdName ?? "",
248
+ project: raw.Org ?? "",
249
+ recruit_label: raw.Category ?? "",
250
+ description: (raw.Duty ?? "").trim(),
251
+ requirements: (raw.Require ?? "").trim(),
252
+ work_cities: Array.isArray(raw.LocNames) ? raw.LocNames.join(", ") : "",
253
+ salary: raw.Salary ?? "",
254
+ kind: raw.Kind ?? "",
255
+ head_count: raw.HeadCount,
256
+ post_date: raw.PostDate ?? "",
257
+ apply_url: DETAIL_PAGE(String(raw.JobAdId ?? id)),
258
+ };
259
+ }
260
+ // ---------- fetchDictionaries ----------
261
+ export async function fetchDictionaries() {
262
+ const [conditions, areas] = await Promise.all([
263
+ post("/api/Jobad/GetJobAdSearchConditions", { Category: [] }),
264
+ get("/api/Jobad/SearchAreasTreeConditions"),
265
+ ]);
266
+ return {
267
+ ok: conditions.ok || areas.ok,
268
+ source: SOURCE,
269
+ api_host: API_ROOT,
270
+ verified_at: new Date().toISOString(),
271
+ search_conditions: conditions.data ?? null,
272
+ areas_tree: areas.data ?? null,
273
+ category_map: { "4": "员工社招", "5": "员工校招", "2": "校园招聘", "3": "实习生" },
274
+ };
275
+ }
276
+ // ---------- notices ----------
277
+ const NO_NOTICES = "vivo careers (Beisen tenant 612022) does not expose a public notices endpoint.";
278
+ export async function listNotices() {
279
+ return { ok: false, source: SOURCE, message: NO_NOTICES, notices: [] };
280
+ }
281
+ export async function getNotice(noticeId) {
282
+ return { ok: false, source: SOURCE, message: NO_NOTICES, notice_id: noticeId };
283
+ }
284
+ export async function findNoticesByQuestion(question, _opts = {}) {
285
+ return { ok: false, source: SOURCE, question, message: NO_NOTICES, matches: [] };
286
+ }
287
+ // ---------- matchResume ----------
288
+ export async function matchResume(text, opts = {}) {
289
+ const { terms, cities } = extractResumeSignals(text ?? "");
290
+ const topN = Math.max(1, opts.topN ?? 5);
291
+ const candidates = Math.max(topN, opts.candidates ?? 200);
292
+ const all = await fetchAllPositions({ pageSize: 30, maxPages: Math.ceil(candidates / 30) });
293
+ if (!all.ok) {
294
+ return {
295
+ ok: false,
296
+ source: SOURCE,
297
+ message: all.message,
298
+ extracted_terms: terms,
299
+ city_preferences: cities,
300
+ matches: [],
301
+ };
302
+ }
303
+ const scored = [];
304
+ for (const p of all.positions) {
305
+ const haystack = `${p.title} ${p.project} ${p.bgs} ${p.work_cities}`;
306
+ const score = scoreOverlap(haystack, terms, cities).score;
307
+ if (score > 0)
308
+ scored.push({ score, position: p });
309
+ }
310
+ scored.sort((a, b) => b.score - a.score);
311
+ return {
312
+ ok: true,
313
+ source: SOURCE,
314
+ extracted_terms: terms,
315
+ city_preferences: cities,
316
+ candidate_pool: all.positions.length,
317
+ matches: scored.slice(0, topN).map((s) => s.position),
318
+ };
319
+ }
320
+ export { extractResumeSignals, scoreOverlap };
321
+ export async function fetchApplicationSchema(postId) {
322
+ const id = (postId ?? '').trim();
323
+ if (!id)
324
+ return { ok: false, source: SOURCE, message: 'post_id is required' };
325
+ let title = '';
326
+ try {
327
+ const detail = await fetchPositionDetail(id);
328
+ if (detail?.ok === false) {
329
+ return { ok: false, source: SOURCE, message: detail.message ?? 'post not found' };
330
+ }
331
+ title = detail?.title ?? '';
332
+ }
333
+ catch { }
334
+ const questions = [
335
+ { label: 'Name', required: true, fields: [{ name: 'name', type: 'input_text' }] },
336
+ { label: 'Email', required: true, fields: [{ name: 'email', type: 'input_text' }] },
337
+ { label: 'Phone', required: true, fields: [{ name: 'phone', type: 'input_text' }] },
338
+ { label: 'Resume', required: true, fields: [{ name: 'resume', type: 'input_file' }] },
339
+ ];
340
+ return {
341
+ ok: true,
342
+ schema: {
343
+ source: SOURCE,
344
+ post_id: id,
345
+ job_title: title,
346
+ apply_url: 'https://vivo.zhiye.com/jobs',
347
+ submit_endpoint: 'https://vivo.zhiye.com/api/Apply/SubmitResume',
348
+ submit_method: 'POST',
349
+ submit_kind: 'beisen-italent',
350
+ endpoint_verified: true,
351
+ submit_notes: 'Beisen iTalent apply: POST /api/Resume/UploadResume (multipart) + ' +
352
+ 'POST /api/Apply/SubmitResume with { JobAdId, ResumeId, … }. ' +
353
+ 'Endpoint anon-probed → HTTP 500 IIS Server Error template ' +
354
+ '(route exists, handler threw on missing required headers/body — ' +
355
+ 'not 404 fallthrough). Requires candidate session via Beisen iTalent ' +
356
+ 'login at /login.html. Capture via extension/, drop session.json ' +
357
+ 'under ~/.jobpro/.',
358
+ questions,
359
+ },
360
+ };
361
+ }
package/dist/webank.js ADDED
@@ -0,0 +1,55 @@
1
+ // 微众银行 (WeBank) careers adapter — Liepin aggregator fallback.
2
+ //
3
+ // WeBank's career page at www.webank.com/career/ is a 15KB static Vue
4
+ // brochure with no embedded job feed; recruitment runs through the
5
+ // 微众银行招聘 WeChat 公众号 → 微信小程序 chain. We surface real
6
+ // currently-open WeBank positions by querying Liepin
7
+ // (api-c.liepin.com) filtered by compName="微众银行". See
8
+ // `cli/src/liepin.ts` for the shared factory.
9
+ //
10
+ // Source: api-c.liepin.com (`source` field on responses) — clearly NOT
11
+ // the same as WeBank's WeChat mini-program funnel.
12
+ import { createAdapter } from "./liepin.js";
13
+ const adapter = createAdapter({
14
+ companyName: "微众银行",
15
+ label: "WeBank / 微众银行",
16
+ });
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
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_webank } from "./apply.js";
27
+ export async function fetchApplicationSchema(postId) {
28
+ const id = (postId ?? "").trim();
29
+ if (!id)
30
+ return { ok: false, source: "webank.com (via api-c.liepin.com)", message: "post_id is required" };
31
+ let title = "";
32
+ let applyUrl = "https://webank.com";
33
+ try {
34
+ const detail = (await fetchPositionDetail(id));
35
+ if (detail?.ok === false) {
36
+ return { ok: false, source: "webank.com (via api-c.liepin.com)", message: detail.message ?? "post not found" };
37
+ }
38
+ title = detail?.title ?? "";
39
+ if (detail?.apply_url)
40
+ applyUrl = detail.apply_url;
41
+ }
42
+ catch { }
43
+ return {
44
+ ok: true,
45
+ schema: _buildBespokeApplySchema_webank({
46
+ source: "webank.com (via api-c.liepin.com)",
47
+ postId: id,
48
+ jobTitle: title,
49
+ applyUrl,
50
+ submitEndpoint: undefined,
51
+ submitKind: "external",
52
+ submitNotes: "WeBank (Liepin-backed) — submission is recruiter-IM-mediated through Liepin. Open the apply_url to start the chat.",
53
+ }),
54
+ };
55
+ }