@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,455 @@
1
+ // Thin client for Bilibili's campus-recruiting API at jobs.bilibili.com.
2
+ //
3
+ // AUTH MODEL — Two-step stateless handshake (no login required):
4
+ // 1. GET /api/auth/v1/csrf/token
5
+ // Headers: X-AppKey: ops.ehr-api.auth, X-UserType: 2
6
+ // Response: { code:0, data:"<uuid>" }
7
+ // Side-effect: sets cookie X-CSRF=<uuid> on domain bilibili.co
8
+ // (note: curl won't auto-save it due to domain mismatch with jobs.bilibili.com)
9
+ // 2. POST /api/campus/position/positionList
10
+ // Pass the token both as:
11
+ // header X-CSRF: <token>
12
+ // cookie X-CSRF=<token>
13
+ // Without both, the server returns code:-3 ("csrf不能为空").
14
+ //
15
+ // The /api/campus/* endpoints require NO Bilibili account session (ajSessionId).
16
+ // A fresh CSRF token from step 1 is sufficient for public position browsing.
17
+ //
18
+ // ============================================================
19
+ // Endpoint inventory (probed 2026-05, JS bundle app.3a48ef6c.js + position.846fe539.js):
20
+ //
21
+ // GET https://jobs.bilibili.com/api/auth/v1/csrf/token
22
+ // Headers: X-AppKey, X-UserType:2
23
+ // Response: { code:0, data:"<csrf-uuid>" }
24
+ //
25
+ // POST https://jobs.bilibili.com/api/campus/position/positionList
26
+ // Headers: X-AppKey, X-UserType:2, X-CSRF:<token>, Cookie: X-CSRF=<token>
27
+ // Payload: { pageNum, pageSize, positionName?, workLocationList?, positionTypeList?,
28
+ // deptCodeList?, workTypeList?, practiceTypes?, onlyHotRecruit?, recruitType? }
29
+ // Response: { code:0, data:{ list:[...], pages:<int>, size:<int>, total:<int> } }
30
+ //
31
+ // GET https://jobs.bilibili.com/api/campus/dict/post
32
+ // Headers: X-AppKey, X-UserType:2, X-CSRF:<token>, Cookie: X-CSRF=<token>
33
+ // Response: code:0, data:[{ parentRankCode, rankCode, rankName, sonRankBasics:[...] }]
34
+ // This is the public job-category taxonomy — no account needed.
35
+ //
36
+ // ============================================================
37
+ // Filter taxonomy (probed 2026-05, total ~356 positions):
38
+ //
39
+ // DIMENSION 1 — positionTypeList (职位类型)
40
+ // "实习" — intern positions (~335 of 356 visible)
41
+ // "全职" — full-time campus hire (~21 of 356 visible)
42
+ // (default: both, pass [] or omit)
43
+ //
44
+ // DIMENSION 2 — workLocationList (工作地点, free-text city names from workLocation field)
45
+ // Common values seen: "上海", "北京", "上海/北京", "深圳", "杭州", "成都"
46
+ // The API matches substring, so "北京" will match "上海/北京".
47
+ // Pass [] or omit to query all cities.
48
+ //
49
+ // DIMENSION 3 — positionName (搜索关键词)
50
+ // Free-text search matched against position title. Pass "" or omit for all.
51
+ //
52
+ // DIMENSION 4 — practiceTypes (校招项目 project IDs)
53
+ // 53 — 实习生校招项目 (campus intern program, recruitType=1)
54
+ // 52 — 全职校招项目 (campus full-time program, recruitType=1)
55
+ // 0 — 普通实习 (regular intern, recruitType=0)
56
+ // Pass [] or omit to return all projects.
57
+ // Note: passing [52] or [53] alone does NOT reliably filter by type in this API —
58
+ // see the workTypeList + positionTypeList combination instead.
59
+ //
60
+ // DIMENSION 5 — recruitType (招聘类型)
61
+ // 1 — 校招 (campus program recruit)
62
+ // 0 — 普通实习 (ad-hoc intern)
63
+ // (default: both; pass undefined to include all)
64
+ //
65
+ // DIMENSION 6 — job category taxonomy from GET /api/campus/dict/post (positionType)
66
+ // Parent "01" 技术类
67
+ // "010" 开发序列, "011" 运维序列, "012" 测试序列, "013" 算法序列
68
+ // "014" 安全序列, "015" 信息管理序列, "016" 多媒体序列
69
+ // Parent "02" 大职能类
70
+ // "020" 财务, "021" 法务, "022" 投资, "023" 行政, "024" 采购
71
+ // "025" 综合业务, "026" 公共关系, "027" 信息管理, "028" 人力资源, "029" 战略
72
+ // Parent "03" 产品运营类
73
+ // "030" 产品, "031" 产品运营, "032" 用户运营, "033" 电商运营
74
+ // "034" 展会活动运营, "035" 数据分析, "036" 数据科学
75
+ // Parent "04" 设计类
76
+ // "040" UED, "041" 美术创意, "042" 平面设计
77
+ // Parent "05" 内容类
78
+ // "050" 内容运营, "051" 版权管理, "052" 内容合作
79
+ // Parent "06" 文创类
80
+ // "061" 制作, "062" 出品
81
+ // Parent "07" 市场营销类
82
+ // "070" 品牌市场, "071" 公关, "072" 商务BD, "073" 销售支持
83
+ // "074" 销售, "075" 广告运营
84
+ // Parent "08" 运营保障类
85
+ // "080" 审核, "081" 客服, "082" 审核管理, "083" 审核运营
86
+ // "084" 审核执行, "085" 客服执行, "086" 客服运营, "087" 客服管理
87
+ // Parent "09" 综合管理类 / "10" 项目管理类 / "11" 游戏类 / "12" 外包类 / "99" 其他
88
+ //
89
+ // ============================================================
90
+ // PositionSummary field mapping (Bilibili → canonical):
91
+ // post_id ← String(item.id)
92
+ // title ← item.positionName
93
+ // project ← item.postCodeName (e.g. "技术类" / "大职能类")
94
+ // recruit_label ← item.positionTypeName (e.g. "实习" / "全职")
95
+ // bgs ← "" (Bilibili does not expose BG/事业群 in public search)
96
+ // work_cities ← item.workLocation (e.g. "上海" / "上海/北京")
97
+ // apply_url ← https://jobs.bilibili.com/campus/positions/${id}
98
+ //
99
+ // ============================================================
100
+ // Endpoints that return 403 without a real account session (ajSessionId):
101
+ // GET/POST /api/campus/dict/dictMsg
102
+ // GET/POST /api/campus/position/cityList
103
+ // GET/POST /api/campus/position/postCodeList
104
+ // GET/POST /api/campus/position/detail/<id>
105
+ // GET/POST /api/srs/* (social recruit system — requires full login)
106
+ // GET/POST /api/rts/* (internal system — 403 or 500)
107
+ //
108
+ // The CSRF token is fresh per request; cache it for the process lifetime to
109
+ // avoid double-fetching on repeated searchPositions calls.
110
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
111
+ export { checkResume };
112
+ const API_ROOT = "https://jobs.bilibili.com";
113
+ const CAMPUS_PAGE = "https://jobs.bilibili.com/campus/positions";
114
+ const DETAIL_PAGE = (id) => `https://jobs.bilibili.com/campus/positions/${encodeURIComponent(id)}`;
115
+ const DEFAULT_HEADERS = {
116
+ "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",
117
+ Accept: "application/json, text/plain, */*",
118
+ "Content-Type": "application/json",
119
+ "X-AppKey": "ops.ehr-api.auth",
120
+ "X-UserType": "2",
121
+ Referer: "https://jobs.bilibili.com/",
122
+ };
123
+ // ---------- CSRF token cache ----------
124
+ // Fresh UUID from GET /api/auth/v1/csrf/token — valid for the process lifetime.
125
+ let _csrfCache = null;
126
+ async function fetchCsrfToken() {
127
+ if (_csrfCache)
128
+ return { ok: true, token: _csrfCache };
129
+ let response;
130
+ try {
131
+ response = await fetch(`${API_ROOT}/api/auth/v1/csrf/token`, {
132
+ headers: DEFAULT_HEADERS,
133
+ });
134
+ }
135
+ catch (err) {
136
+ return {
137
+ ok: false,
138
+ message: `network error fetching CSRF: ${err instanceof Error ? err.message : String(err)}`,
139
+ };
140
+ }
141
+ if (!response.ok) {
142
+ return { ok: false, message: `CSRF HTTP ${response.status}` };
143
+ }
144
+ let payload;
145
+ try {
146
+ payload = await response.json();
147
+ }
148
+ catch {
149
+ return { ok: false, message: "bad JSON in CSRF response" };
150
+ }
151
+ if (payload.code !== 0 || !payload.data) {
152
+ return {
153
+ ok: false,
154
+ message: payload.message ?? "CSRF endpoint returned error",
155
+ };
156
+ }
157
+ _csrfCache = payload.data;
158
+ return { ok: true, token: payload.data };
159
+ }
160
+ async function call(body) {
161
+ const csrfResult = await fetchCsrfToken();
162
+ if (!csrfResult.ok) {
163
+ return { ok: false, message: csrfResult.message };
164
+ }
165
+ const token = csrfResult.token;
166
+ const url = `${API_ROOT}/api/campus/position/positionList`;
167
+ let response;
168
+ try {
169
+ response = await fetch(url, {
170
+ method: "POST",
171
+ headers: {
172
+ ...DEFAULT_HEADERS,
173
+ "X-CSRF": token,
174
+ // The backend requires the CSRF token as both a request header AND a cookie.
175
+ // The Set-Cookie header from /api/auth/v1/csrf/token sets it on domain bilibili.co
176
+ // (not jobs.bilibili.com), so browsers do send it automatically but Node's fetch
177
+ // does not forward cross-domain cookies — we inject it manually here.
178
+ Cookie: `X-CSRF=${token}`,
179
+ },
180
+ body: JSON.stringify(body),
181
+ });
182
+ }
183
+ catch (err) {
184
+ return {
185
+ ok: false,
186
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
187
+ };
188
+ }
189
+ if (!response.ok) {
190
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
191
+ }
192
+ let payload;
193
+ try {
194
+ payload = (await response.json());
195
+ }
196
+ catch (err) {
197
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
198
+ }
199
+ return {
200
+ ok: payload.code === 0,
201
+ data: payload.data,
202
+ message: payload.message ?? (payload.code === 0 ? "ok" : `code ${payload.code}`),
203
+ };
204
+ }
205
+ function summarizePosition(item) {
206
+ const id = String(item.id ?? "");
207
+ return {
208
+ post_id: id,
209
+ title: item.positionName ?? "",
210
+ project: item.postCodeName ?? "",
211
+ recruit_label: item.positionTypeName ?? "",
212
+ bgs: "",
213
+ work_cities: item.workLocation ?? "",
214
+ apply_url: id ? DETAIL_PAGE(id) : CAMPUS_PAGE,
215
+ };
216
+ }
217
+ // ---------- searchPositions ----------
218
+ export async function searchPositions(opts = {}) {
219
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
220
+ const page = Math.max(1, opts.page ?? 1);
221
+ const keyword = (opts.keyword ?? "").trim().slice(0, 60);
222
+ const payload = {
223
+ pageNum: page,
224
+ pageSize,
225
+ positionName: keyword,
226
+ workTypeList: [],
227
+ deptCodeList: [],
228
+ };
229
+ if (opts.positionTypes?.length) {
230
+ payload.positionTypeList = opts.positionTypes;
231
+ }
232
+ else {
233
+ payload.positionTypeList = [];
234
+ }
235
+ if (opts.workLocations?.length) {
236
+ payload.workLocationList = opts.workLocations;
237
+ }
238
+ else {
239
+ payload.workLocationList = [];
240
+ }
241
+ if (opts.recruitType !== undefined) {
242
+ payload.recruitType = opts.recruitType;
243
+ }
244
+ const response = await call(payload);
245
+ if (!response.ok || !response.data) {
246
+ return {
247
+ ok: false,
248
+ message: response.message,
249
+ source: "jobs.bilibili.com",
250
+ query: payload,
251
+ positions: [],
252
+ };
253
+ }
254
+ const rows = response.data.list ?? [];
255
+ return {
256
+ ok: true,
257
+ source: "jobs.bilibili.com",
258
+ query: payload,
259
+ page,
260
+ page_size: pageSize,
261
+ total: response.data.total ?? rows.length,
262
+ positions: rows.map(summarizePosition),
263
+ };
264
+ }
265
+ // ---------- fetchAllPositions ----------
266
+ export async function fetchAllPositions(opts = {}) {
267
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
268
+ const maxPages = Math.max(1, opts.maxPages ?? 4); // ~400 positions max
269
+ const bucket = [];
270
+ let total;
271
+ for (let page = 1; page <= maxPages; page++) {
272
+ const result = await searchPositions({ ...opts, page, pageSize });
273
+ if (!result.ok) {
274
+ return {
275
+ ok: false,
276
+ message: result.message,
277
+ source: "jobs.bilibili.com",
278
+ fetched: bucket.length,
279
+ positions: bucket,
280
+ };
281
+ }
282
+ if (total === undefined)
283
+ total = result.total;
284
+ if (!result.positions.length)
285
+ break;
286
+ bucket.push(...result.positions);
287
+ if (total !== undefined && bucket.length >= total)
288
+ break;
289
+ }
290
+ return {
291
+ ok: true,
292
+ source: "jobs.bilibili.com",
293
+ total: total ?? bucket.length,
294
+ fetched: bucket.length,
295
+ positions: bucket,
296
+ };
297
+ }
298
+ // ---------- fetchPositionDetail ----------
299
+ // Bilibili's public search response carries description + requirement inline,
300
+ // so "detail" is a paginated scan-and-filter (same pattern as feishu.ts).
301
+ export async function fetchPositionDetail(postId) {
302
+ const id = (postId ?? "").trim();
303
+ if (!id)
304
+ return { ok: false, source: "jobs.bilibili.com", message: "post_id is required" };
305
+ const pageSize = 100;
306
+ const maxPages = 4;
307
+ for (let page = 1; page <= maxPages; page++) {
308
+ const result = await searchPositions({ page, pageSize });
309
+ if (!result.ok) {
310
+ return {
311
+ ok: false,
312
+ source: "jobs.bilibili.com",
313
+ post_id: id,
314
+ message: result.message,
315
+ };
316
+ }
317
+ const found = result.positions.find((p) => p.post_id === id);
318
+ if (found) {
319
+ return {
320
+ ok: true,
321
+ source: "jobs.bilibili.com",
322
+ post_id: id,
323
+ title: found.title,
324
+ project: found.project,
325
+ recruit_label: found.recruit_label,
326
+ bgs: found.bgs,
327
+ work_cities: found.work_cities,
328
+ apply_url: found.apply_url,
329
+ };
330
+ }
331
+ if (result.positions.length < pageSize)
332
+ break;
333
+ }
334
+ return {
335
+ ok: false,
336
+ source: "jobs.bilibili.com",
337
+ post_id: id,
338
+ message: `post ${id} not found in public search results (scanned up to ${maxPages * pageSize} posts)`,
339
+ };
340
+ }
341
+ // ---------- matchResume ----------
342
+ export async function matchResume(text, opts = {}) {
343
+ const topN = Math.max(1, opts.topN ?? 5);
344
+ const candidates = Math.max(topN, opts.candidates ?? 20);
345
+ const { terms, cities } = extractResumeSignals(text ?? "");
346
+ if (!terms.length) {
347
+ return {
348
+ ok: false,
349
+ source: "jobs.bilibili.com",
350
+ message: "could not extract any technical signals from the text",
351
+ preview: (text ?? "").slice(0, 120),
352
+ };
353
+ }
354
+ const keyword = terms.slice(0, 3).join(" ");
355
+ const list = await searchPositions({ keyword, page: 1, pageSize: 100 });
356
+ if (!list.ok) {
357
+ return { ok: false, source: "jobs.bilibili.com", message: list.message, positions: [] };
358
+ }
359
+ const scored = [];
360
+ for (const p of list.positions) {
361
+ const blob = [p.title, p.project, p.recruit_label, p.work_cities].join(" ");
362
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
363
+ if (score > 0) {
364
+ scored.push({ score, position: p, reasons });
365
+ }
366
+ }
367
+ scored.sort((a, b) => b.score - a.score);
368
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
369
+ if (!shortlist.length) {
370
+ shortlist = list.positions.slice(0, candidates).map((position) => ({
371
+ score: 0,
372
+ position,
373
+ reasons: [],
374
+ }));
375
+ }
376
+ const matches = shortlist.slice(0, topN).map((s) => {
377
+ const mr = s.reasons.length > 0
378
+ ? s.reasons.slice(0, 5)
379
+ : ["no specific keyword overlap — surfaced from initial keyword search"];
380
+ return { ...s.position, match_reasons: mr };
381
+ });
382
+ return {
383
+ ok: true,
384
+ source: "jobs.bilibili.com",
385
+ extracted_terms: terms,
386
+ city_preferences: cities,
387
+ matches,
388
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
389
+ "The only authority on selection is HR.",
390
+ };
391
+ }
392
+ // ---------- stub notices + dicts ----------
393
+ // Bilibili's campus site has no public notices/announcements endpoint
394
+ // and the dict/post endpoint requires a real Bilibili session (403 anon),
395
+ // so fetchDictionaries also stubs with an honest message.
396
+ const STUB_NOTICES = {
397
+ ok: false,
398
+ source: "jobs.bilibili.com",
399
+ message: "Bilibili: no public notices endpoint",
400
+ };
401
+ export async function fetchDictionaries() {
402
+ return {
403
+ ok: false,
404
+ source: "jobs.bilibili.com",
405
+ message: "Bilibili: dict endpoints (dict/post, cityList, etc.) require a real user session (ajSessionId); filter taxonomy is derivable from positionList responses instead.",
406
+ };
407
+ }
408
+ export async function listNotices() {
409
+ return STUB_NOTICES;
410
+ }
411
+ export async function getNotice(_id) {
412
+ return {
413
+ ok: false,
414
+ source: "jobs.bilibili.com",
415
+ message: "Bilibili: no public notices endpoint",
416
+ };
417
+ }
418
+ export async function findNoticesByQuestion(_question, _opts = {}) {
419
+ return {
420
+ ok: false,
421
+ source: "jobs.bilibili.com",
422
+ message: "Bilibili: no public notices endpoint",
423
+ };
424
+ }
425
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_bilibili } from "./apply.js";
426
+ export async function fetchApplicationSchema(postId) {
427
+ const id = (postId ?? "").trim();
428
+ if (!id)
429
+ return { ok: false, source: "jobs.bilibili.com", message: "post_id is required" };
430
+ let title = "";
431
+ let applyUrl = "https://jobs.bilibili.com";
432
+ try {
433
+ const detail = (await fetchPositionDetail(id));
434
+ if (detail?.ok === false) {
435
+ return { ok: false, source: "jobs.bilibili.com", message: detail.message ?? "post not found" };
436
+ }
437
+ title = detail?.title ?? "";
438
+ if (detail?.apply_url)
439
+ applyUrl = detail.apply_url;
440
+ }
441
+ catch { }
442
+ return {
443
+ ok: true,
444
+ schema: _buildBespokeApplySchema_bilibili({
445
+ source: "jobs.bilibili.com",
446
+ postId: id,
447
+ jobTitle: title,
448
+ applyUrl,
449
+ submitEndpoint: "https://jobs.bilibili.com/api/portal/post/apply",
450
+ submitKind: "multipart-session",
451
+ endpointVerified: true,
452
+ submitNotes: "Bilibili — POST /api/portal/post/apply with ajSessionId cookie. Endpoint anon-probed → HTTP 200 + {code:-101, msg:\"ajSessionId不能为空\"} (real apply route; original /api/post/apply returns structured 404, but the /api/portal/* sub-tree is the real customer-facing one). Body shape still needs validation.",
453
+ }),
454
+ };
455
+ }