@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
@@ -0,0 +1,380 @@
1
+ // 科大讯飞 (iFlytek) careers adapter for `job-pro`.
2
+ //
3
+ // ============================================================
4
+ // API DISCOVERY (probed 2026-05-16)
5
+ //
6
+ // campus.iflytek.com / career.iflytek.com / hr.iflytek.com all 301-chain into
7
+ // Beisen iTalent's candidate-portal sign-in form (favicon /italent.ico is the
8
+ // dead giveaway for Beisen / 北森). That portal is candidate-session-only.
9
+ //
10
+ // The *public* careers site is a sibling Beisen tenant hosted at
11
+ // https://iflytek.zhiye.com/ — the same SaaS stack we already use for vivo
12
+ // (see cli/src/vivo.ts). The paginated list endpoint is anonymous: no
13
+ // session cookie, no signed header, no CSRF token. Same response envelope
14
+ // as vivo and other zhiye.com tenants:
15
+ //
16
+ // POST /api/Jobad/GetJobAdPageList
17
+ // payload: { PageIndex (0-based), PageSize, KeyWords, SpecialType,
18
+ // PortalId: "", DisplayFields: [...], Category?: [...] }
19
+ // headers: standard browser UA + Content-Type=application/json +
20
+ // Referer=https://iflytek.zhiye.com/jobs +
21
+ // x-requested-with=xmlhttprequest + langtype=zh_CN
22
+ // envelope: { Code:200, Data:[RawJobAd[]], Count:<int>, Total:<int> }
23
+ //
24
+ // Probed 2026-05-16: 744 positions across campus / social / intern channels.
25
+ // Category labels seen: "校园招聘", "员工社招", "员工校招", "实习生".
26
+ //
27
+ // Endpoint inventory (all anon, all on iflytek.zhiye.com):
28
+ // POST /api/Jobad/GetJobAdPageList → paginated job list
29
+ // POST /api/Jobad/GetJobAdSearchConditions → filter taxonomy
30
+ // GET /api/Jobad/GetSpecialJobAdList → hot/special jobs
31
+ // GET /api/Jobad/SearchAreasTreeConditions → city tree
32
+ // GET /api/Common/GetPortalAIRobot → portal config
33
+ // ============================================================
34
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
35
+ export { checkResume };
36
+ const SOURCE = "iflytek.zhiye.com";
37
+ const API_ROOT = "https://iflytek.zhiye.com";
38
+ const SITE_ROOT = "https://iflytek.zhiye.com/jobs";
39
+ const DETAIL_PAGE = (id) => `https://iflytek.zhiye.com/jobs?jobAdId=${encodeURIComponent(id)}`;
40
+ const DEFAULT_HEADERS = {
41
+ "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",
42
+ Accept: "application/json",
43
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
44
+ Referer: SITE_ROOT,
45
+ Origin: API_ROOT,
46
+ "x-requested-with": "xmlhttprequest",
47
+ langtype: "zh_CN",
48
+ };
49
+ async function post(path, body) {
50
+ let response;
51
+ try {
52
+ response = await fetch(`${API_ROOT}${path}`, {
53
+ method: "POST",
54
+ headers: { ...DEFAULT_HEADERS, "Content-Type": "application/json" },
55
+ body: JSON.stringify(body),
56
+ });
57
+ }
58
+ catch (err) {
59
+ return {
60
+ ok: false,
61
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
62
+ };
63
+ }
64
+ if (!response.ok) {
65
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
66
+ }
67
+ let payload;
68
+ try {
69
+ payload = (await response.json());
70
+ }
71
+ catch (err) {
72
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
73
+ }
74
+ return {
75
+ ok: payload.Code === 200,
76
+ data: payload.Data,
77
+ count: payload.Count ?? payload.Total,
78
+ message: payload.Message || (payload.Code === 200 ? "ok" : "upstream error"),
79
+ };
80
+ }
81
+ async function get(path) {
82
+ let response;
83
+ try {
84
+ response = await fetch(`${API_ROOT}${path}`, { method: "GET", headers: DEFAULT_HEADERS });
85
+ }
86
+ catch (err) {
87
+ return {
88
+ ok: false,
89
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
90
+ };
91
+ }
92
+ if (!response.ok) {
93
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
94
+ }
95
+ let payload;
96
+ try {
97
+ payload = (await response.json());
98
+ }
99
+ catch (err) {
100
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
101
+ }
102
+ return {
103
+ ok: payload.Code === 200,
104
+ data: payload.Data,
105
+ message: payload.Message || (payload.Code === 200 ? "ok" : "upstream error"),
106
+ };
107
+ }
108
+ function summarize(item) {
109
+ const id = String(item.JobAdId ?? item.Id ?? "");
110
+ const cities = Array.isArray(item.LocNames) ? item.LocNames.join(", ") : "";
111
+ return {
112
+ post_id: id,
113
+ title: (item.JobAdName ?? "").trim(),
114
+ project: (item.Org ?? "").trim(),
115
+ recruit_label: (item.Category ?? "").trim(),
116
+ bgs: "",
117
+ work_cities: cities,
118
+ apply_url: id ? DETAIL_PAGE(id) : SITE_ROOT,
119
+ };
120
+ }
121
+ // Beisen tenants encode recruit type via numeric Category IDs that vary by
122
+ // tenant. We don't know iFlytek's exact mapping without probing the
123
+ // taxonomy endpoint, so we leave it open and let CLI users filter by the
124
+ // returned `recruit_label` string client-side. When the mapping is known,
125
+ // add the numeric codes here (vivo uses "3"=intern, "4"=social, "5"=campus).
126
+ function categoryFromRecruitType(_t) {
127
+ return undefined;
128
+ }
129
+ // ---------- searchPositions ----------
130
+ export async function searchPositions(opts = {}) {
131
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 20));
132
+ const page = Math.max(1, opts.page ?? 1);
133
+ // Beisen pageIndex is zero-based.
134
+ const body = {
135
+ PageIndex: page - 1,
136
+ PageSize: pageSize,
137
+ KeyWords: (opts.keyword ?? "").trim().slice(0, 60),
138
+ SpecialType: 0,
139
+ PortalId: "",
140
+ DisplayFields: ["Category", "Kind", "LocId", "Org", "HeadCount", "PostDate", "Salary"],
141
+ };
142
+ const category = categoryFromRecruitType(opts.recruitType);
143
+ if (category)
144
+ body.Category = category;
145
+ const r = await post("/api/Jobad/GetJobAdPageList", body);
146
+ if (!r.ok) {
147
+ return {
148
+ ok: false,
149
+ source: SOURCE,
150
+ message: r.message,
151
+ query: body,
152
+ positions: [],
153
+ };
154
+ }
155
+ const rows = r.data ?? [];
156
+ return {
157
+ ok: true,
158
+ source: SOURCE,
159
+ query: body,
160
+ page,
161
+ page_size: pageSize,
162
+ total: r.count ?? rows.length,
163
+ positions: rows.map(summarize),
164
+ };
165
+ }
166
+ // ---------- fetchAllPositions ----------
167
+ export async function fetchAllPositions(opts = {}) {
168
+ const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 30));
169
+ const maxPages = Math.max(1, opts.maxPages ?? 30);
170
+ const bucket = [];
171
+ let total;
172
+ for (let page = 1; page <= maxPages; page++) {
173
+ const r = await searchPositions({
174
+ keyword: opts.keyword,
175
+ page,
176
+ pageSize,
177
+ recruitType: opts.recruitType,
178
+ });
179
+ if (!r.ok) {
180
+ return { ok: false, source: SOURCE, message: r.message, total: 0, fetched: bucket.length, positions: bucket };
181
+ }
182
+ if (total === undefined)
183
+ total = r.total;
184
+ if (!r.positions.length)
185
+ break;
186
+ bucket.push(...r.positions);
187
+ if (total !== undefined && bucket.length >= total)
188
+ break;
189
+ }
190
+ return {
191
+ ok: true,
192
+ source: SOURCE,
193
+ total: total ?? bucket.length,
194
+ fetched: bucket.length,
195
+ positions: bucket,
196
+ };
197
+ }
198
+ // ---------- fetchPositionDetail ----------
199
+ // Beisen serves the detail page from the same paginated list; there is no
200
+ // per-id REST endpoint that returns plain JSON. We page through and filter.
201
+ export async function fetchPositionDetail(postId) {
202
+ const id = (postId ?? "").trim();
203
+ if (!id)
204
+ return { ok: false, source: SOURCE, message: "post_id is required" };
205
+ const pageSize = 50;
206
+ const maxPages = 20;
207
+ for (let page = 1; page <= maxPages; page++) {
208
+ const body = {
209
+ PageIndex: page - 1,
210
+ PageSize: pageSize,
211
+ KeyWords: "",
212
+ SpecialType: 0,
213
+ PortalId: "",
214
+ DisplayFields: ["Category", "Org", "LocId", "Kind", "Duty", "Require"],
215
+ };
216
+ const r = await post("/api/Jobad/GetJobAdPageList", body);
217
+ if (!r.ok) {
218
+ return { ok: false, source: SOURCE, post_id: id, message: r.message };
219
+ }
220
+ const posts = r.data ?? [];
221
+ const found = posts.find((p) => String(p.JobAdId ?? p.Id) === id);
222
+ if (found) {
223
+ const summary = summarize(found);
224
+ return {
225
+ ok: true,
226
+ source: SOURCE,
227
+ post_id: id,
228
+ title: found.JobAdName ?? "",
229
+ project: summary.project,
230
+ recruit_label: summary.recruit_label,
231
+ description: found.Duty ?? "",
232
+ requirements: found.Require ?? "",
233
+ head_count: found.HeadCount ?? 0,
234
+ post_date: found.PostDate ?? "",
235
+ work_cities: found.LocNames ?? [],
236
+ apply_url: summary.apply_url,
237
+ };
238
+ }
239
+ if (posts.length < pageSize)
240
+ break;
241
+ }
242
+ return {
243
+ ok: false,
244
+ source: SOURCE,
245
+ post_id: id,
246
+ message: `post ${id} not found in public search results (scanned up to ${maxPages * pageSize} posts)`,
247
+ };
248
+ }
249
+ let _filterCache = null;
250
+ export async function fetchDictionaries() {
251
+ if (_filterCache !== null)
252
+ return _filterCache;
253
+ const r = await post("/api/Jobad/GetJobAdSearchConditions", { PortalId: "", SpecialType: 0 });
254
+ if (!r.ok || !r.data) {
255
+ const result = { ok: false, source: SOURCE, message: r.message };
256
+ _filterCache = result;
257
+ return result;
258
+ }
259
+ const conditions = r.data.map((c) => ({
260
+ field: c.Field ?? "",
261
+ name: c.Name ?? "",
262
+ options: (c.Options ?? []).map((o) => ({
263
+ id: o.Id ?? o.Code ?? "",
264
+ name: o.Name ?? "",
265
+ })),
266
+ }));
267
+ const result = { ok: true, source: SOURCE, conditions };
268
+ _filterCache = result;
269
+ return result;
270
+ }
271
+ // ---------- notices (stub — Beisen tenants have no public notices feed) ----------
272
+ const NOTICES_STUB = {
273
+ ok: false,
274
+ source: SOURCE,
275
+ message: "iFlytek: no public notices endpoint on Beisen tenant",
276
+ };
277
+ export async function listNotices() {
278
+ return { ...NOTICES_STUB, notices: [] };
279
+ }
280
+ export async function getNotice(noticeId) {
281
+ return { ...NOTICES_STUB, notice_id: noticeId };
282
+ }
283
+ export async function findNoticesByQuestion(question, _opts = {}) {
284
+ return { ...NOTICES_STUB, question, matches: [] };
285
+ }
286
+ // ---------- matchResume ----------
287
+ export async function matchResume(text, opts = {}) {
288
+ const topN = Math.max(1, opts.topN ?? 5);
289
+ const candidates = Math.max(topN, opts.candidates ?? 20);
290
+ const { terms, cities } = extractResumeSignals(text ?? "");
291
+ if (!terms.length) {
292
+ return {
293
+ ok: false,
294
+ source: SOURCE,
295
+ message: "could not extract any technical signals from the text",
296
+ preview: (text ?? "").slice(0, 120),
297
+ };
298
+ }
299
+ const keyword = terms.slice(0, 3).join(" ");
300
+ const list = await searchPositions({ keyword, page: 1, pageSize: 50 });
301
+ if (!list.ok) {
302
+ return { ok: false, source: SOURCE, message: list.message, positions: [] };
303
+ }
304
+ const scored = [];
305
+ for (const p of list.positions) {
306
+ const blob = [p.title, p.project, p.recruit_label, p.work_cities].join(" ");
307
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
308
+ if (score > 0)
309
+ scored.push({ score, position: p, reasons });
310
+ }
311
+ scored.sort((a, b) => b.score - a.score);
312
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
313
+ if (!shortlist.length) {
314
+ shortlist = list.positions.slice(0, candidates).map((position) => ({
315
+ score: 0,
316
+ position,
317
+ reasons: [],
318
+ }));
319
+ }
320
+ const matches = shortlist.slice(0, topN).map((s) => {
321
+ const mr = s.reasons.length > 0
322
+ ? s.reasons.slice(0, 5)
323
+ : ["no specific keyword overlap — surfaced from initial keyword search"];
324
+ return { ...s.position, match_reasons: mr };
325
+ });
326
+ return {
327
+ ok: true,
328
+ source: SOURCE,
329
+ extracted_terms: terms,
330
+ city_preferences: cities,
331
+ matches,
332
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
333
+ "The only authority on selection is HR.",
334
+ };
335
+ }
336
+ export { extractResumeSignals, scoreOverlap };
337
+ // Silence unused warning for the GET helper — kept for future taxonomy/city
338
+ // endpoints that return BeisenEnvelope JSON via GET.
339
+ void get;
340
+ export async function fetchApplicationSchema(postId) {
341
+ const id = (postId ?? '').trim();
342
+ if (!id)
343
+ return { ok: false, source: SOURCE, message: 'post_id is required' };
344
+ let title = '';
345
+ try {
346
+ const detail = await fetchPositionDetail(id);
347
+ if (detail?.ok === false) {
348
+ return { ok: false, source: SOURCE, message: detail.message ?? 'post not found' };
349
+ }
350
+ title = detail?.title ?? '';
351
+ }
352
+ catch { }
353
+ const questions = [
354
+ { label: 'Name', required: true, fields: [{ name: 'name', type: 'input_text' }] },
355
+ { label: 'Email', required: true, fields: [{ name: 'email', type: 'input_text' }] },
356
+ { label: 'Phone', required: true, fields: [{ name: 'phone', type: 'input_text' }] },
357
+ { label: 'Resume', required: true, fields: [{ name: 'resume', type: 'input_file' }] },
358
+ ];
359
+ return {
360
+ ok: true,
361
+ schema: {
362
+ source: SOURCE,
363
+ post_id: id,
364
+ job_title: title,
365
+ apply_url: 'https://iflytek.zhiye.com/jobs',
366
+ submit_endpoint: 'https://iflytek.zhiye.com/api/Apply/SubmitResume',
367
+ submit_method: 'POST',
368
+ submit_kind: 'beisen-italent',
369
+ endpoint_verified: true,
370
+ submit_notes: 'Beisen iTalent apply: POST /api/Resume/UploadResume (multipart) + ' +
371
+ 'POST /api/Apply/SubmitResume with { JobAdId, ResumeId, … }. ' +
372
+ 'Endpoint anon-probed → HTTP 500 IIS Server Error template ' +
373
+ '(route exists, handler threw on missing required headers/body — ' +
374
+ 'not 404 fallthrough). Requires candidate session — Beisen iTalent ' +
375
+ 'uses email+phone+OTP login at /login.html. Capture via extension/, ' +
376
+ 'drop session.json under ~/.jobpro/.',
377
+ questions,
378
+ },
379
+ };
380
+ }