@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/trip.js ADDED
@@ -0,0 +1,396 @@
1
+ // Thin client for Trip.com / Ctrip (携程) public campus-recruiting API.
2
+ //
3
+ // Both portals are backed by the same API server:
4
+ // careers.ctrip.com — Chinese domestic portal (携程招聘)
5
+ // careers.trip.com — International portal (Trip.com Group Careers)
6
+ //
7
+ // This adapter targets careers.ctrip.com since it hosts the authoritative
8
+ // Chinese campus job feed. All JSON endpoints are unauthenticated; the server
9
+ // validates the presence of a mandatory `condition` wrapper in the POST body.
10
+ //
11
+ // ============================================================
12
+ // Endpoint inventory (probed 2026-05, JS bundle main.ad2ffe67.js):
13
+ //
14
+ // POST https://careers.ctrip.com/api/hrrecruit/getJobAd
15
+ // Payload (all fields inside a "condition" key):
16
+ // { condition: {
17
+ // pageIndex: <int>, // 1-based
18
+ // pageSize: <int>, // max tested: 100
19
+ // category: "2", // "2"=校招/campus, "1"=社招/social hire
20
+ // searchText: <string>, // keyword filter (free-text)
21
+ // city: <string>, // e.g. "CO0009" = Shanghai
22
+ // jobFamilyGroupCode: n/a // rejected with 202 — do not send
23
+ // } }
24
+ // Response: { retCode:"201", retMessage:"调用成功",
25
+ // retValue:{ total:<int>, recruitJobAdList:[...] } }
26
+ // retCode "201" = success (not HTTP 201).
27
+ // retCode "501" = validation error (missing `condition`).
28
+ // retCode "202" = data-validation error (bad field value).
29
+ //
30
+ // POST https://careers.ctrip.com/api/hrrecruit/getJobCount
31
+ // Payload: { source:"ctrip" }
32
+ // Response: retValue: [{categoryCode:"Categroy_1",total:44}, ...]
33
+ // Used for statistics only; not required for job search.
34
+ //
35
+ // IMPORTANT QUIRKS:
36
+ // 1. The `keyword` field (inside condition) crashes the server with a
37
+ // NullPointerException when combined with pagination. Use `searchText`
38
+ // instead — it is the working search field.
39
+ // 2. Combining `searchText` with `category` is accepted by the server but
40
+ // the server ignores searchText (returns all campus results). Keyword
41
+ // filtering therefore works only without the category filter.
42
+ // Practical consequence: when campus=true, keyword is applied client-side
43
+ // on the title after fetching the full campus set.
44
+ // 3. `category:"2"` (校招/fresh graduates) gives ~112 positions;
45
+ // no intern-only category exists (intern jobs appear mixed inside category 1
46
+ // or surface via keyword "实习" across all listings).
47
+ //
48
+ // ============================================================
49
+ // Field mapping (API response → PositionSummary)
50
+ // post_id ← item.id (numeric string, e.g. "27655163")
51
+ // title ← item.jobTitle (may include code suffix "(MJ034955)")
52
+ // project ← item.jobFamilyGroupName (e.g. "Software development")
53
+ // recruit_label ← item.kindName (e.g. "Fresh Graduates")
54
+ // bgs ← item.buName (BU = Business Unit, e.g. "International business")
55
+ // work_cities ← item.cityName
56
+ // apply_url ← https://careers.ctrip.com/campus/job-detail/<jobId>
57
+ // (uses UUID `jobId`, not numeric `id`)
58
+ //
59
+ // ============================================================
60
+ // Category/filter values probed 2026-05:
61
+ // category "1" = 社招 (social/experienced hire) ~657 positions
62
+ // category "2" = 校招 (campus / fresh graduates) ~112 positions
63
+ // No category (omit field) = all listings ~769 positions
64
+ //
65
+ // City codes (from item.city in responses):
66
+ // CO0009 = Shanghai CO0001 = Beijing CO0013 = Xiamen
67
+ // CO0004 = Shenzhen CO0006 = Chengdu (+ many others not enumerated)
68
+ //
69
+ // jobFamilyGroupName values seen in responses:
70
+ // "Software development", "Admin", "Business development",
71
+ // "Marketing & PR", "Finance", "Data & Analytics", "Product management"
72
+ //
73
+ // ============================================================
74
+ // Workday dead-end investigation:
75
+ // trip.wd1.myworkdayjobs.com — resolves and is behind Cloudflare but
76
+ // all POST attempts to /wday/cxs/trip/<slug>/jobs return HTTP 422 (no slug
77
+ // identifiable without an active UI page). The Workday tenant appears to be
78
+ // a legacy artifact from Trip.com's international hiring pre-2024.
79
+ // Not used in this adapter.
80
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
81
+ export { checkResume };
82
+ const API_ROOT = "https://careers.ctrip.com/api/hrrecruit";
83
+ const CAMPUS_PAGE = "https://careers.ctrip.com/campus";
84
+ const DETAIL_PAGE = (jobId) => `https://careers.ctrip.com/campus/job-detail/${encodeURIComponent(jobId)}`;
85
+ const DEFAULT_HEADERS = {
86
+ "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",
87
+ Accept: "application/json, text/plain, */*",
88
+ "Content-Type": "application/json",
89
+ Origin: "https://careers.ctrip.com",
90
+ Referer: CAMPUS_PAGE,
91
+ };
92
+ async function call(path, body) {
93
+ const url = `${API_ROOT}${path}`;
94
+ let response;
95
+ try {
96
+ response = await fetch(url, {
97
+ method: "POST",
98
+ headers: DEFAULT_HEADERS,
99
+ body: JSON.stringify(body),
100
+ });
101
+ }
102
+ catch (err) {
103
+ return {
104
+ ok: false,
105
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
106
+ };
107
+ }
108
+ if (!response.ok) {
109
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
110
+ }
111
+ let payload;
112
+ try {
113
+ payload = (await response.json());
114
+ }
115
+ catch (err) {
116
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
117
+ }
118
+ // retCode "201" = success; any other value is an error.
119
+ const ok = payload.retCode === "201";
120
+ return {
121
+ ok,
122
+ data: ok ? payload.retValue : undefined,
123
+ message: payload.retMessage || (ok ? "ok" : `upstream error (code ${payload.retCode})`),
124
+ };
125
+ }
126
+ function summarizePosition(item) {
127
+ const id = String(item.id ?? "");
128
+ const jobId = item.jobId ?? "";
129
+ return {
130
+ post_id: id,
131
+ title: item.jobTitle ?? "",
132
+ project: item.jobFamilyGroupName ?? "",
133
+ recruit_label: item.kindName ?? "",
134
+ bgs: (item.buName ?? "").trim(),
135
+ work_cities: item.cityName ?? "",
136
+ apply_url: jobId ? DETAIL_PAGE(jobId) : CAMPUS_PAGE,
137
+ };
138
+ }
139
+ // ---------- searchPositions ----------
140
+ export async function searchPositions(opts = {}) {
141
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
142
+ const page = Math.max(1, opts.page ?? 1);
143
+ const keyword = (opts.keyword ?? "").trim().slice(0, 60);
144
+ const campusOnly = opts.campusOnly !== false; // default true
145
+ // Build the condition object.
146
+ // NOTE: `keyword` crashes the server with a NullPointerException when combined
147
+ // with pagination; use `searchText` for safe text search. However, when
148
+ // `category` is also set, the server silently ignores `searchText`, so keyword
149
+ // filtering is applied client-side after the response is received.
150
+ const condition = {
151
+ pageIndex: page,
152
+ pageSize,
153
+ };
154
+ if (campusOnly) {
155
+ condition.category = "2";
156
+ // searchText is ignored by server when category is set; skip it to avoid confusion
157
+ }
158
+ else {
159
+ // Without category filter, searchText works correctly
160
+ if (keyword)
161
+ condition.searchText = keyword;
162
+ }
163
+ if (opts.cityCode?.trim()) {
164
+ condition.city = opts.cityCode.trim();
165
+ }
166
+ const response = await call("/getJobAd", { condition });
167
+ if (!response.ok || !response.data) {
168
+ return {
169
+ ok: false,
170
+ message: response.message,
171
+ source: "careers.ctrip.com",
172
+ query: condition,
173
+ positions: [],
174
+ };
175
+ }
176
+ let rows = response.data.recruitJobAdList ?? [];
177
+ // Client-side keyword filter when campusOnly is active (server ignores searchText in that mode)
178
+ if (campusOnly && keyword) {
179
+ const lk = keyword.toLowerCase();
180
+ rows = rows.filter((r) => (r.jobTitle ?? "").toLowerCase().includes(lk));
181
+ }
182
+ return {
183
+ ok: true,
184
+ source: "careers.ctrip.com",
185
+ query: condition,
186
+ page,
187
+ page_size: pageSize,
188
+ total: campusOnly && keyword ? rows.length : (response.data.total ?? rows.length),
189
+ positions: rows.map(summarizePosition),
190
+ };
191
+ }
192
+ // ---------- fetchAllPositions ----------
193
+ export async function fetchAllPositions(opts = {}) {
194
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
195
+ const maxPages = Math.max(1, opts.maxPages ?? 5);
196
+ const bucket = [];
197
+ let total;
198
+ for (let page = 1; page <= maxPages; page++) {
199
+ const result = await searchPositions({ ...opts, page, pageSize });
200
+ if (!result.ok) {
201
+ return {
202
+ ok: false,
203
+ message: result.message,
204
+ source: "careers.ctrip.com",
205
+ fetched: bucket.length,
206
+ positions: bucket,
207
+ };
208
+ }
209
+ if (total === undefined)
210
+ total = result.total;
211
+ if (!result.positions.length)
212
+ break;
213
+ bucket.push(...result.positions);
214
+ if (total !== undefined && bucket.length >= total)
215
+ break;
216
+ }
217
+ return {
218
+ ok: true,
219
+ source: "careers.ctrip.com",
220
+ total: total ?? bucket.length,
221
+ fetched: bucket.length,
222
+ positions: bucket,
223
+ };
224
+ }
225
+ // ---------- fetchPositionDetail ----------
226
+ // The API exposes the full `requirements` HTML in the search response itself,
227
+ // so detail is derived from the search list without a separate round-trip.
228
+ // We page through the campus listing to find the matching id.
229
+ export async function fetchPositionDetail(postId) {
230
+ const id = (postId ?? "").trim();
231
+ if (!id)
232
+ return { ok: false, source: "careers.ctrip.com", message: "post_id is required" };
233
+ const pageSize = 100;
234
+ const maxPages = 5;
235
+ for (let page = 1; page <= maxPages; page++) {
236
+ const condition = { pageIndex: page, pageSize, category: "2" };
237
+ const resp = await call("/getJobAd", { condition });
238
+ if (!resp.ok || !resp.data)
239
+ break;
240
+ const items = resp.data.recruitJobAdList ?? [];
241
+ const found = items.find((p) => String(p.id) === id);
242
+ if (found) {
243
+ const summary = summarizePosition(found);
244
+ return {
245
+ ok: true,
246
+ source: "careers.ctrip.com",
247
+ post_id: id,
248
+ job_id: found.jobId ?? "",
249
+ title: found.jobTitle ?? "",
250
+ requirements_html: found.requirements ?? "",
251
+ recruit_label: found.kindName ?? "",
252
+ job_family: found.jobFamilyGroupName ?? "",
253
+ bu: found.buName ?? "",
254
+ city: found.cityName ?? "",
255
+ publish_date: found.publishDate ?? "",
256
+ apply_url: summary.apply_url,
257
+ };
258
+ }
259
+ if (items.length < pageSize)
260
+ break;
261
+ }
262
+ return {
263
+ ok: false,
264
+ source: "careers.ctrip.com",
265
+ post_id: id,
266
+ message: `post ${id} not found in campus search results (searched up to ${maxPages * pageSize} posts)`,
267
+ };
268
+ }
269
+ // ---------- fetchDictionaries ----------
270
+ export async function fetchDictionaries() {
271
+ // getJobCount returns a breakdown by internal category code; not a full
272
+ // taxonomy, but useful for getting totals.
273
+ const response = await call("/getJobCount", { source: "ctrip" });
274
+ const knownCategories = [
275
+ { category: "2", label: "校招 / Campus (Fresh Graduates)", note: "~112 positions as of 2026-05" },
276
+ { category: "1", label: "社招 / Social (Experienced Hire)", note: "~657 positions" },
277
+ ];
278
+ return {
279
+ ok: response.ok,
280
+ source: "careers.ctrip.com",
281
+ campus_page: CAMPUS_PAGE,
282
+ categories: knownCategories,
283
+ job_count_by_family: response.ok ? (response.data ?? []) : [],
284
+ message: response.ok ? "ok" : response.message,
285
+ note: "Filter taxonomy: use category='2' for campus jobs in searchPositions(). " +
286
+ "City codes are in item.city of API responses (e.g. CO0009=Shanghai, CO0001=Beijing).",
287
+ };
288
+ }
289
+ // ---------- notices (no public endpoint) ----------
290
+ const STUB_NOTICE = {
291
+ ok: false,
292
+ source: "careers.ctrip.com",
293
+ message: "Trip.com / Ctrip: no public notices/announcements endpoint",
294
+ };
295
+ export async function listNotices() {
296
+ return STUB_NOTICE;
297
+ }
298
+ export async function getNotice(_id) {
299
+ return {
300
+ ok: false,
301
+ source: "careers.ctrip.com",
302
+ message: "Trip.com / Ctrip: no public notices endpoint",
303
+ };
304
+ }
305
+ export async function findNoticesByQuestion(_question, _opts = {}) {
306
+ return {
307
+ ok: false,
308
+ source: "careers.ctrip.com",
309
+ message: "Trip.com / Ctrip: no public notices endpoint",
310
+ };
311
+ }
312
+ // ---------- matchResume ----------
313
+ export async function matchResume(text, opts = {}) {
314
+ const topN = Math.max(1, opts.topN ?? 5);
315
+ const candidates = Math.max(topN, opts.candidates ?? 50);
316
+ const { terms, cities } = extractResumeSignals(text ?? "");
317
+ if (!terms.length) {
318
+ return {
319
+ ok: false,
320
+ source: "careers.ctrip.com",
321
+ message: "could not extract any technical signals from the text",
322
+ preview: (text ?? "").slice(0, 120),
323
+ };
324
+ }
325
+ // Fetch campus listings. Keyword is applied client-side when campusOnly=true.
326
+ const keyword = terms.slice(0, 3).join(" ");
327
+ const list = await fetchAllPositions({ campusOnly: true, pageSize: 100, maxPages: 2 });
328
+ if (!list.ok) {
329
+ return { ok: false, source: "careers.ctrip.com", message: list.message, positions: [] };
330
+ }
331
+ const scored = [];
332
+ for (const p of list.positions) {
333
+ const blob = [p.title, p.project, p.recruit_label, p.bgs, p.work_cities].join(" ");
334
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
335
+ if (score > 0)
336
+ scored.push({ score, position: p, reasons });
337
+ }
338
+ scored.sort((a, b) => b.score - a.score);
339
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
340
+ if (!shortlist.length) {
341
+ shortlist = list.positions.slice(0, candidates).map((position) => ({
342
+ score: 0,
343
+ position,
344
+ reasons: [],
345
+ }));
346
+ }
347
+ const matches = shortlist.slice(0, topN).map((s) => {
348
+ const mr = s.reasons.length > 0
349
+ ? s.reasons.slice(0, 5)
350
+ : ["no specific keyword overlap — surfaced from campus listing"];
351
+ return { ...s.position, match_reasons: mr };
352
+ });
353
+ return {
354
+ ok: true,
355
+ source: "careers.ctrip.com",
356
+ extracted_terms: terms,
357
+ city_preferences: cities,
358
+ keyword_used: keyword,
359
+ matches,
360
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
361
+ "The only authority on selection is HR.",
362
+ };
363
+ }
364
+ // Export helpers so other modules can import them from trip.js
365
+ export { extractResumeSignals, scoreOverlap };
366
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_trip } from "./apply.js";
367
+ export async function fetchApplicationSchema(postId) {
368
+ const id = (postId ?? "").trim();
369
+ if (!id)
370
+ return { ok: false, source: "careers.ctrip.com", message: "post_id is required" };
371
+ let title = "";
372
+ let applyUrl = "https://careers.ctrip.com";
373
+ try {
374
+ const detail = (await fetchPositionDetail(id));
375
+ if (detail?.ok === false) {
376
+ return { ok: false, source: "careers.ctrip.com", message: detail.message ?? "post not found" };
377
+ }
378
+ title = detail?.title ?? "";
379
+ if (detail?.apply_url)
380
+ applyUrl = detail.apply_url;
381
+ }
382
+ catch { }
383
+ return {
384
+ ok: true,
385
+ schema: _buildBespokeApplySchema_trip({
386
+ source: "careers.ctrip.com",
387
+ postId: id,
388
+ jobTitle: title,
389
+ applyUrl,
390
+ submitEndpoint: "https://careers.ctrip.com/api/hrrecruit/applyJob",
391
+ submitKind: "multipart-session",
392
+ endpointVerified: true,
393
+ submitNotes: "Trip.com — POST /api/hrrecruit/applyJob with session cookie. Endpoint extracted from the careers SPA main.ad2ffe67.js bundle (sibling routes /api/hrrecruit/getJobAd, /getLoginInfo, /getNewsDetail, etc). Anon-probed → HTTP 200 + Ctrip ResponseStatus envelope {Ack:\"Success\", retCode:\"402\", retMessage:\"没有当前用户\"} — real API, auth-gated. Body shape still needs validation.",
394
+ }),
395
+ };
396
+ }