@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,424 @@
1
+ // Thin client for NetEase's (网易) public recruiting API at hr.163.com.
2
+ //
3
+ // Both campus-recruiting (校园/实习) and social-hire (社招) positions are served
4
+ // from the same host. This adapter targets campus + intern postings (workType "1").
5
+ //
6
+ // ============================================================
7
+ // Endpoint inventory (probed 2026-05, commons.288fd140.chunk.js):
8
+ //
9
+ // POST https://hr.163.com/api/hr163/position/queryPage
10
+ // Payload: { pageNum, pageSize, workType, keyword, ... }
11
+ // Response: { code:200, data:{ total, pages, list:[...], lastPage } }
12
+ // Verified fields in payload:
13
+ // workType "0"=社招 (social hire) "1"=校园/实习 (campus+intern)
14
+ // keyword free-text search — the only filter that actually narrows results
15
+ // pageNum accepted but IGNORED — server always returns same top N records
16
+ // pageSize works; max=200 (code 402 if exceeded)
17
+ // All other filter params (positionTypeCode, firstPostTypeCode,
18
+ // workPlaceId, workPlaceList, etc.) are accepted with 200 but have NO
19
+ // effect on the result set without an authenticated session cookie.
20
+ //
21
+ // GET https://hr.163.com/api/hr163/position/query?id=<id>
22
+ // Returns full JD fields for one position ID.
23
+ // No auth required; same shape as list items plus description/requirement.
24
+ //
25
+ // GET https://hr.163.com/api/hr163/options/positionType/queryItemList
26
+ // Returns the positionType dictionary (职位类别).
27
+ // id/name pairs — see DIMENSION 1 below.
28
+ //
29
+ // GET https://hr.163.com/api/hr163/position/queryPositionMetric
30
+ // Returns aggregate counts: positionCount, cityCount, firstDepartmentCount.
31
+ //
32
+ // GET https://campus.163.com/api/campuspc/position/getJobList [NOTE: auth-gated]
33
+ // The campus.163.com SPA (校园招聘) exposes a dedicated campus portal with
34
+ // BU/city/positionType filters — params: workPlaceId, positionType, firstBuId,
35
+ // keyword, pageNum, pageSize (GET with query params, axios passes as params).
36
+ // However the endpoint returns code:406 "当前用户未登录" for all filter dictionary
37
+ // endpoints, and getJobList returns total:0 for unauthenticated requests.
38
+ // ▶ Not usable without credentials; we fall back to hr.163.com.
39
+ //
40
+ // ============================================================
41
+ // Pagination caveat:
42
+ // pageNum is sent but IGNORED by the server without auth.
43
+ // The API returns the top N (up to pageSize ≤ 200) positions sorted by relevance.
44
+ // fetchAllPositions() transparently makes multiple keyword-scoped calls when
45
+ // the caller requests many pages, but because the underlying sort is fixed, pages
46
+ // beyond the first will repeat the same records. We document this honestly.
47
+ //
48
+ // ============================================================
49
+ // DIMENSION 1 — positionType codes (GET /options/positionType/queryItemList):
50
+ // 01=技术 02=游戏策划 03=游戏程序 04=游戏艺术 05=游戏测试
51
+ // 06=产品 07=人工智能 08=运营 11=用户体验及设计 12=项目管理
52
+ // 16=市场渠道 21=销售 26=内容 31=客服 41=电商 51=职能支持
53
+ // 56=高管 57=教育 58=企业服务 00,99=其他
54
+ //
55
+ // DIMENSION 2 — workType:
56
+ // "0" = 社招 (social/experienced hire) ~1952 positions
57
+ // "1" = 校园/实习 (campus new-grad + intern) ~417 positions
58
+ //
59
+ // DIMENSION 3 — workPlaceList city codes (observed in list responses):
60
+ // 1=北京 2=上海 138=广州 229=杭州
61
+ // (NOTE: server ignores this filter without auth — keyword is the only filter)
62
+ //
63
+ // DIMENSION 4 — product/firstDep groupings observed in campus data:
64
+ // P008=网易游戏(雷火) P041=网易游戏(互娱) P001=网易严选
65
+ // firstDepName examples: 雷火事业群 / 音乐事业部 / 有道事业群 / 伏羲机器人 / 网易伏羲 / 严选事业部
66
+ //
67
+ // ============================================================
68
+ // ---- PositionSummary field mapping (NetEase → canonical) ----
69
+ // post_id ← item.id (stringified)
70
+ // title ← item.name
71
+ // project ← item.firstPostTypeName (职位类别, e.g. "游戏程序" / "技术" / "人工智能")
72
+ // recruit_label ← item.workType === "1" ? "校园/实习" : "社招" (API has no sub-label)
73
+ // bgs ← item.firstDepName (一级部门/事业群, closest to BG)
74
+ // work_cities ← item.workPlaceNameList joined with " / "
75
+ // apply_url ← https://hr.163.com/job-detail?id=${id}
76
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
77
+ export { checkResume };
78
+ const API_ROOT = "https://hr.163.com/api/hr163";
79
+ const CAMPUS_PAGE = "https://hr.163.com/job-list?workType=1";
80
+ const DETAIL_PAGE = (id) => `https://hr.163.com/job-detail?id=${encodeURIComponent(id)}`;
81
+ const SOURCE = "hr.163.com";
82
+ const DEFAULT_HEADERS = {
83
+ "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",
84
+ Accept: "application/json, text/plain, */*",
85
+ Referer: "https://hr.163.com/",
86
+ };
87
+ // ---------- low-level helpers ----------
88
+ async function get(path) {
89
+ const url = `${API_ROOT}${path}`;
90
+ let response;
91
+ try {
92
+ response = await fetch(url, { headers: DEFAULT_HEADERS });
93
+ }
94
+ catch (err) {
95
+ return {
96
+ ok: false,
97
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
98
+ };
99
+ }
100
+ if (!response.ok) {
101
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
102
+ }
103
+ let payload;
104
+ try {
105
+ payload = (await response.json());
106
+ }
107
+ catch (err) {
108
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
109
+ }
110
+ return {
111
+ ok: payload.code === 200,
112
+ data: payload.data ?? undefined,
113
+ message: payload.msg ?? (payload.code === 200 ? "ok" : "upstream error"),
114
+ };
115
+ }
116
+ async function post(path, body) {
117
+ const url = `${API_ROOT}${path}`;
118
+ let response;
119
+ try {
120
+ response = await fetch(url, {
121
+ method: "POST",
122
+ headers: { ...DEFAULT_HEADERS, "Content-Type": "application/json" },
123
+ body: JSON.stringify(body),
124
+ });
125
+ }
126
+ catch (err) {
127
+ return {
128
+ ok: false,
129
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
130
+ };
131
+ }
132
+ if (!response.ok) {
133
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
134
+ }
135
+ let payload;
136
+ try {
137
+ payload = (await response.json());
138
+ }
139
+ catch (err) {
140
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
141
+ }
142
+ return {
143
+ ok: payload.code === 200,
144
+ data: payload.data ?? undefined,
145
+ message: payload.msg ?? (payload.code === 200 ? "ok" : "upstream error"),
146
+ };
147
+ }
148
+ function summarizePosition(item) {
149
+ const id = String(item.id ?? "");
150
+ const workCities = (item.workPlaceNameList ?? [])
151
+ .map((c) => c.trim())
152
+ .filter(Boolean)
153
+ .join(" / ");
154
+ return {
155
+ post_id: id,
156
+ title: item.name ?? "",
157
+ project: item.firstPostTypeName ?? "",
158
+ recruit_label: item.workType === "1" ? "校园/实习" : "社招",
159
+ bgs: item.firstDepName ?? "",
160
+ work_cities: workCities,
161
+ apply_url: id ? DETAIL_PAGE(id) : CAMPUS_PAGE,
162
+ };
163
+ }
164
+ // ---------- searchPositions ----------
165
+ export async function searchPositions(opts = {}) {
166
+ const pageSize = Math.max(1, Math.min(200, opts.pageSize ?? 20));
167
+ const page = Math.max(1, opts.page ?? 1);
168
+ const keyword = (opts.keyword ?? "").trim().slice(0, 80);
169
+ const workType = opts.workType ?? "1";
170
+ const payload = {
171
+ pageNum: page,
172
+ pageSize,
173
+ workType,
174
+ keyword,
175
+ };
176
+ const response = await post("/position/queryPage", payload);
177
+ if (!response.ok || !response.data) {
178
+ return {
179
+ ok: false,
180
+ source: SOURCE,
181
+ message: response.message,
182
+ query: payload,
183
+ positions: [],
184
+ };
185
+ }
186
+ const rows = response.data.list ?? [];
187
+ return {
188
+ ok: true,
189
+ source: SOURCE,
190
+ query: payload,
191
+ page,
192
+ page_size: pageSize,
193
+ total: response.data.total ?? rows.length,
194
+ positions: rows.map(summarizePosition),
195
+ note: "pageNum is ignored by the server without auth; results are always top-N by relevance. " +
196
+ "Use `keyword` to narrow the result set.",
197
+ };
198
+ }
199
+ // ---------- fetchAllPositions ----------
200
+ // Because pageNum is ignored, we cannot truly paginate.
201
+ // We fetch the maximum allowed pageSize=200 in one call and return it.
202
+ // When keyword is provided, narrow set may fit in one call.
203
+ export async function fetchAllPositions(opts = {}) {
204
+ const pageSize = 200; // server max
205
+ const workType = opts.workType ?? "1";
206
+ const keyword = (opts.keyword ?? "").trim();
207
+ const result = await searchPositions({ keyword, pageSize, workType, page: 1 });
208
+ if (!result.ok) {
209
+ return {
210
+ ok: false,
211
+ source: SOURCE,
212
+ message: result.message,
213
+ fetched: 0,
214
+ positions: [],
215
+ };
216
+ }
217
+ return {
218
+ ok: true,
219
+ source: SOURCE,
220
+ total: result.total,
221
+ fetched: result.positions.length,
222
+ positions: result.positions,
223
+ note: "fetchAllPositions returns up to 200 positions in a single call (server max). " +
224
+ "True multi-page iteration is not available without authentication. " +
225
+ `Reported total: ${result.total}`,
226
+ };
227
+ }
228
+ // ---------- fetchPositionDetail ----------
229
+ export async function fetchPositionDetail(postId) {
230
+ const id = (postId ?? "").trim();
231
+ if (!id) {
232
+ return { ok: false, source: SOURCE, message: "post_id is required" };
233
+ }
234
+ const response = await get(`/position/query?id=${encodeURIComponent(id)}`);
235
+ if (!response.ok || !response.data) {
236
+ return {
237
+ ok: false,
238
+ source: SOURCE,
239
+ post_id: id,
240
+ message: response.message || "no detail returned",
241
+ };
242
+ }
243
+ const raw = response.data;
244
+ const workCities = (raw.workPlaceNameList ?? []).map((c) => c.trim()).filter(Boolean);
245
+ return {
246
+ ok: true,
247
+ source: SOURCE,
248
+ post_id: String(raw.id ?? id),
249
+ title: raw.name ?? "",
250
+ project: raw.firstPostTypeName ?? "",
251
+ recruit_label: raw.workType === "1" ? "校园/实习" : "社招",
252
+ bgs: raw.firstDepName ?? "",
253
+ product: raw.productName ?? raw.product ?? "",
254
+ req_education: raw.reqEducationName ?? "",
255
+ req_work_years: raw.reqWorkYearsName ?? "",
256
+ description: raw.description ?? "",
257
+ requirements: raw.requirement ?? "",
258
+ work_cities: workCities,
259
+ recruit_cities: workCities, // API does not separate work city from interview city
260
+ apply_url: DETAIL_PAGE(String(raw.id ?? id)),
261
+ };
262
+ }
263
+ export async function fetchDictionaries() {
264
+ const response = await get("/options/positionType/queryItemList");
265
+ const positionTypes = response.ok
266
+ ? (response.data ?? []).map((item) => ({
267
+ id: item.id ?? "",
268
+ name: item.name ?? "",
269
+ }))
270
+ : [];
271
+ // Static known city codes (observed in campus responses 2026-05)
272
+ const cities = [
273
+ { code: 1, name: "北京市" },
274
+ { code: 2, name: "上海市" },
275
+ { code: 138, name: "广州市" },
276
+ { code: 229, name: "杭州市" },
277
+ ];
278
+ // Static workType values
279
+ const workTypes = [
280
+ { value: "1", label: "校园/实习", note: "campus new-grad + intern (~417 posts)" },
281
+ { value: "0", label: "社招", note: "social/experienced hire (~1952 posts)" },
282
+ ];
283
+ return {
284
+ ok: response.ok,
285
+ source: SOURCE,
286
+ verified_at: new Date().toISOString(),
287
+ campus_only: false,
288
+ note: "City and BU dictionaries are static (derived from observed data 2026-05). " +
289
+ "However, city/BU filters are NOT effective without authentication — " +
290
+ "only `keyword` actually narrows results in unauthenticated calls.",
291
+ positionTypes,
292
+ cities,
293
+ workTypes,
294
+ message: response.ok ? "ok" : response.message,
295
+ };
296
+ }
297
+ // ---------- notices (stub) ----------
298
+ // hr.163.com has no public announcement/notice endpoint.
299
+ const STUB_MSG = "NetEase: no public notices endpoint on hr.163.com";
300
+ export async function listNotices() {
301
+ return { ok: false, source: SOURCE, message: STUB_MSG };
302
+ }
303
+ export async function getNotice(_id) {
304
+ return { ok: false, source: SOURCE, message: STUB_MSG };
305
+ }
306
+ export async function findNoticesByQuestion(_question, _opts = {}) {
307
+ return { ok: false, source: SOURCE, message: STUB_MSG };
308
+ }
309
+ // ---------- matchResume ----------
310
+ // Mirror bytedance/tencent algorithm:
311
+ // 1. Extract signals from resume text.
312
+ // 2. Search with top-3 terms as keyword (the only working filter).
313
+ // 3. Score each post against title + project + bgs + work_cities + description + requirement.
314
+ // 4. Return top N matches with reasons.
315
+ export async function matchResume(text, opts = {}) {
316
+ const topN = Math.max(1, opts.topN ?? 5);
317
+ const candidates = Math.max(topN, opts.candidates ?? 20);
318
+ const { terms, cities } = extractResumeSignals(text ?? "");
319
+ if (!terms.length) {
320
+ return {
321
+ ok: false,
322
+ source: SOURCE,
323
+ message: "could not extract any technical signals from the text",
324
+ preview: (text ?? "").slice(0, 120),
325
+ };
326
+ }
327
+ const keyword = terms.slice(0, 3).join(" ");
328
+ const listResult = await searchPositions({ keyword, pageSize: 100, workType: "1" });
329
+ if (!listResult.ok) {
330
+ return {
331
+ ok: false,
332
+ source: SOURCE,
333
+ message: listResult.message,
334
+ positions: [],
335
+ };
336
+ }
337
+ const scored = [];
338
+ const shortlist = listResult.positions.slice(0, candidates);
339
+ for (const p of shortlist) {
340
+ // Quick score from summary fields first
341
+ const summaryBlob = [p.title, p.project, p.bgs, p.work_cities, p.recruit_label].join(" ");
342
+ const { score: quickScore, reasons: quickReasons } = scoreOverlap(summaryBlob, terms, cities);
343
+ // Fetch detail for JD text
344
+ const detail = await fetchPositionDetail(p.post_id);
345
+ let description;
346
+ let requirements;
347
+ let extraScore = 0;
348
+ let extraReasons = [];
349
+ if (detail.ok) {
350
+ description = detail.description;
351
+ requirements = detail.requirements;
352
+ const jdBlob = [detail.description, detail.requirements].join(" ");
353
+ const extra = scoreOverlap(jdBlob, terms, cities);
354
+ extraScore = extra.score;
355
+ extraReasons = extra.reasons;
356
+ }
357
+ const totalScore = quickScore + extraScore;
358
+ const allReasons = [...new Set([...quickReasons, ...extraReasons])].slice(0, 5);
359
+ if (totalScore > 0 || scored.length < topN) {
360
+ scored.push({ score: totalScore, position: p, reasons: allReasons, description, requirements });
361
+ }
362
+ }
363
+ scored.sort((a, b) => b.score - a.score);
364
+ let finalList = scored.slice(0, topN);
365
+ if (!finalList.length) {
366
+ // Fall back: return first topN from list without enrichment
367
+ finalList = listResult.positions.slice(0, topN).map((position) => ({
368
+ score: 0,
369
+ position,
370
+ reasons: [],
371
+ }));
372
+ }
373
+ const matches = finalList.map((s) => {
374
+ const mr = s.reasons.length > 0
375
+ ? s.reasons
376
+ : ["no specific keyword overlap — surfaced from initial keyword search"];
377
+ return {
378
+ ...s.position,
379
+ description: s.description,
380
+ requirements: s.requirements,
381
+ match_reasons: mr,
382
+ };
383
+ });
384
+ return {
385
+ ok: true,
386
+ source: SOURCE,
387
+ extracted_terms: terms,
388
+ city_preferences: cities,
389
+ matches,
390
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
391
+ "The only authority on selection is HR.",
392
+ };
393
+ }
394
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_netease } from "./apply.js";
395
+ export async function fetchApplicationSchema(postId) {
396
+ const id = (postId ?? "").trim();
397
+ if (!id)
398
+ return { ok: false, source: "hr.163.com", message: "post_id is required" };
399
+ let title = "";
400
+ let applyUrl = "https://hr.163.com";
401
+ try {
402
+ const detail = (await fetchPositionDetail(id));
403
+ if (detail?.ok === false) {
404
+ return { ok: false, source: "hr.163.com", message: detail.message ?? "post not found" };
405
+ }
406
+ title = detail?.title ?? "";
407
+ if (detail?.apply_url)
408
+ applyUrl = detail.apply_url;
409
+ }
410
+ catch { }
411
+ return {
412
+ ok: true,
413
+ schema: _buildBespokeApplySchema_netease({
414
+ source: "hr.163.com",
415
+ postId: id,
416
+ jobTitle: title,
417
+ applyUrl,
418
+ submitEndpoint: "https://hr.163.com/post-app/apply.do",
419
+ submitKind: "multipart-session",
420
+ endpointVerified: true,
421
+ submitNotes: "NetEase — POST /post-app/apply.do with session cookie. Endpoint anon-probed → HTTP 405 (Nginx routing table has this .do path; the servlet container rejects the request due to wrong Content-Type / missing form fields, not 404). Body shape still needs validation against a real candidate session.",
422
+ }),
423
+ };
424
+ }
package/dist/nio.js ADDED
@@ -0,0 +1,24 @@
1
+ // Thin adapter for NIO / 蔚来 campus recruiting via Feishu Recruiting (ATSX).
2
+ //
3
+ // NIO self-hosts the Feishu Recruiting platform at:
4
+ // https://nio.jobs.feishu.cn/
5
+ //
6
+ // API (probed 2026-05):
7
+ // POST https://nio.jobs.feishu.cn/api/v1/search/job/posts
8
+ // Headers: portal-channel: campus, portal-platform: pc, website-path: campus
9
+ // Total: ~771 posts (正式/new-grad only; internship channel returns "site not exist")
10
+ // GET https://nio.jobs.feishu.cn/api/v1/config/job/filters/campus
11
+ //
12
+ // Field notes:
13
+ // - job_category is null; project ← job_function.name
14
+ // - city_info is null; work_cities ← city_list
15
+ // - No internship channel (returns code -9000003 "site not exist")
16
+ //
17
+ // apply_url pattern: https://nio.jobs.feishu.cn/campus/position/<id>/detail
18
+ import { createAdapter } from "./feishu.js";
19
+ export const { searchPositions, fetchAllPositions, fetchPositionDetail, fetchDictionaries, listNotices, getNotice, findNoticesByQuestion, matchResume, checkResume, fetchApplicationSchema, } = createAdapter({
20
+ host: "nio.jobs.feishu.cn",
21
+ channel: "campus",
22
+ label: "NIO / 蔚来",
23
+ applyUrlPrefix: "https://nio.jobs.feishu.cn/campus/position",
24
+ });