@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/oppo.js ADDED
@@ -0,0 +1,285 @@
1
+ // OPPO careers adapter for `job-pro`.
2
+ //
3
+ // ============================================================
4
+ // API DISCOVERY (probed 2026-05-15)
5
+ //
6
+ // careers.oppo.com is a Vite SPA whose campus job listing is rendered by the
7
+ // dynamically-loaded chunk /assets/js/job-edfe7d6e.js. The chunk exposes two
8
+ // candidate routes:
9
+ //
10
+ // POST /ats-candidate-api/open-api/position/queryPositionList → HTTP 404
11
+ // POST /openapi/position/pageNew → HTTP 200 ✓
12
+ //
13
+ // The working route is `/openapi/position/pageNew`. It returns a paginated
14
+ // list of all currently-open positions across the OPPO recruiting site without
15
+ // any token or signed header — only standard browser headers are required.
16
+ // Both campus (校招/应届生) and intern (实习生) postings live on this endpoint;
17
+ // the `recruitmentType` field on each record distinguishes them.
18
+ //
19
+ // Endpoint inventory (all anon, all on careers.oppo.com):
20
+ // POST /openapi/position/pageNew → paginated job list
21
+ // GET /openapi/position/detail?idRecruitPosition=<id> → single posting
22
+ // GET /openapi/position/project/list → recruitment projects
23
+ // GET /openapi/position/relatedPosition?... → related jobs
24
+ // GET /openapi/sec/getRiskReport → WAF risk probe
25
+ // GET /openapi/system/dictionary/queryList → filter taxonomy
26
+ // ============================================================
27
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
28
+ export { checkResume };
29
+ const SOURCE = "careers.oppo.com";
30
+ const API_ROOT = "https://careers.oppo.com";
31
+ const SITE_ROOT = "https://careers.oppo.com/";
32
+ const DETAIL_PAGE = (id) => `https://careers.oppo.com/#/campus/talent/positionDetail/${encodeURIComponent(id)}`;
33
+ const DEFAULT_HEADERS = {
34
+ "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",
35
+ Accept: "application/json, text/plain, */*",
36
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
37
+ Referer: SITE_ROOT,
38
+ Origin: "https://careers.oppo.com",
39
+ };
40
+ async function call(method, path, opts = {}) {
41
+ let url = `${API_ROOT}${path}`;
42
+ if (opts.query) {
43
+ const params = new URLSearchParams();
44
+ for (const [k, v] of Object.entries(opts.query)) {
45
+ if (v !== undefined)
46
+ params.set(k, String(v));
47
+ }
48
+ const qs = params.toString();
49
+ if (qs)
50
+ url += (path.includes("?") ? "&" : "?") + qs;
51
+ }
52
+ const headers = { ...DEFAULT_HEADERS };
53
+ let body;
54
+ if (opts.body !== undefined) {
55
+ body = JSON.stringify(opts.body);
56
+ headers["Content-Type"] = "application/json;charset=UTF-8";
57
+ }
58
+ let response;
59
+ try {
60
+ response = await fetch(url, { method, headers, body });
61
+ }
62
+ catch (err) {
63
+ return {
64
+ ok: false,
65
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
66
+ };
67
+ }
68
+ if (!response.ok) {
69
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
70
+ }
71
+ let payload;
72
+ try {
73
+ payload = (await response.json());
74
+ }
75
+ catch (err) {
76
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
77
+ }
78
+ return {
79
+ ok: payload.code === 0,
80
+ data: payload.data,
81
+ message: payload.msg || (payload.code === 0 ? "ok" : "upstream error"),
82
+ };
83
+ }
84
+ function summarize(item) {
85
+ const id = String(item.idRecruitPosition ?? item.idProjPosition ?? item.projectPositionId ?? "");
86
+ return {
87
+ post_id: id,
88
+ title: (item.positionName ?? item.projectPositionName ?? "").trim(),
89
+ project: (item.projectName ?? "").trim(),
90
+ recruit_label: (item.recruitmentTypeName ?? item.recruitmentType ?? "").trim(),
91
+ bgs: (item.positionTypeName ?? "").trim(),
92
+ work_cities: (item.workCityName ?? "").trim(),
93
+ apply_url: id ? DETAIL_PAGE(id) : SITE_ROOT,
94
+ };
95
+ }
96
+ // ---------- searchPositions ----------
97
+ export async function searchPositions(opts = {}) {
98
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
99
+ const page = Math.max(1, opts.page ?? 1);
100
+ const body = {
101
+ pageNum: page,
102
+ pageSize,
103
+ };
104
+ if (opts.keyword)
105
+ body.keyword = opts.keyword.trim().slice(0, 60);
106
+ if (opts.recruitType === "campus")
107
+ body.recruitmentType = "Campus";
108
+ else if (opts.recruitType === "intern")
109
+ body.recruitmentType = "Intern";
110
+ if (opts.cityCode)
111
+ body.workCityCode = opts.cityCode;
112
+ const r = await call("POST", "/openapi/position/pageNew", { body });
113
+ if (!r.ok || !r.data) {
114
+ return {
115
+ ok: false,
116
+ source: SOURCE,
117
+ message: r.message,
118
+ query: body,
119
+ positions: [],
120
+ };
121
+ }
122
+ const rows = r.data.records ?? [];
123
+ return {
124
+ ok: true,
125
+ source: SOURCE,
126
+ query: body,
127
+ page,
128
+ page_size: pageSize,
129
+ total: r.data.total ?? rows.length,
130
+ positions: rows.map(summarize),
131
+ };
132
+ }
133
+ // ---------- fetchAllPositions ----------
134
+ export async function fetchAllPositions(opts = {}) {
135
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 50));
136
+ const maxPages = Math.max(1, opts.maxPages ?? 40);
137
+ const bucket = [];
138
+ let total;
139
+ for (let page = 1; page <= maxPages; page++) {
140
+ const r = await searchPositions({
141
+ keyword: opts.keyword,
142
+ page,
143
+ pageSize,
144
+ recruitType: opts.recruitType,
145
+ });
146
+ if (!r.ok) {
147
+ return { ok: false, source: SOURCE, message: r.message, total: 0, fetched: bucket.length, positions: bucket };
148
+ }
149
+ if (total === undefined)
150
+ total = r.total;
151
+ if (!r.positions.length)
152
+ break;
153
+ bucket.push(...r.positions);
154
+ if (total !== undefined && bucket.length >= total)
155
+ break;
156
+ }
157
+ return {
158
+ ok: true,
159
+ source: SOURCE,
160
+ total: total ?? bucket.length,
161
+ fetched: bucket.length,
162
+ positions: bucket,
163
+ };
164
+ }
165
+ export async function fetchPositionDetail(postId) {
166
+ const id = (postId ?? "").trim();
167
+ if (!id)
168
+ return { ok: false, source: SOURCE, message: "post_id is required", post_id: id };
169
+ // The endpoint expects `id`, not `idRecruitPosition` — passing
170
+ // `idRecruitPosition` returns the puzzling "id不能为空" error even when the
171
+ // value is present. The response body still keys the id back as
172
+ // `idRecruitPosition`, which is what tripped this in the first place.
173
+ const r = await call("GET", "/openapi/position/detail", {
174
+ query: { id },
175
+ });
176
+ if (!r.ok || !r.data) {
177
+ return { ok: false, source: SOURCE, message: r.message || "no detail returned", post_id: id };
178
+ }
179
+ const raw = r.data;
180
+ return {
181
+ ok: true,
182
+ source: SOURCE,
183
+ post_id: String(raw.idRecruitPosition ?? id),
184
+ title: raw.positionName ?? raw.projectPositionName ?? "",
185
+ project: raw.projectName ?? "",
186
+ recruit_label: raw.recruitmentTypeName ?? raw.recruitmentType ?? "",
187
+ position_type: raw.positionTypeName ?? "",
188
+ description: (raw.positionDesc ?? raw.projectPositionDesc ?? "").trim(),
189
+ requirements: (raw.positionRequire ?? raw.projectPositionRequire ?? "").trim(),
190
+ work_city: raw.workCityName ?? "",
191
+ work_city_code: raw.workCityCode ?? "",
192
+ head_count: raw.positionNum,
193
+ release_time: raw.releaseTime ?? "",
194
+ apply_url: DETAIL_PAGE(id),
195
+ };
196
+ }
197
+ // ---------- fetchDictionaries ----------
198
+ export async function fetchDictionaries() {
199
+ const r = await call("GET", "/openapi/system/dictionary/queryList");
200
+ if (!r.ok)
201
+ return { ok: false, source: SOURCE, message: r.message };
202
+ return {
203
+ ok: true,
204
+ source: SOURCE,
205
+ api_host: API_ROOT,
206
+ verified_at: new Date().toISOString(),
207
+ dictionaries: r.data,
208
+ };
209
+ }
210
+ // ---------- notices (not exposed publicly) ----------
211
+ const NO_NOTICES = "OPPO careers does not expose a public notices/announcements endpoint.";
212
+ export async function listNotices() {
213
+ return { ok: false, source: SOURCE, message: NO_NOTICES, notices: [] };
214
+ }
215
+ export async function getNotice(noticeId) {
216
+ return { ok: false, source: SOURCE, message: NO_NOTICES, notice_id: noticeId };
217
+ }
218
+ export async function findNoticesByQuestion(question, _opts = {}) {
219
+ return { ok: false, source: SOURCE, question, message: NO_NOTICES, matches: [] };
220
+ }
221
+ // ---------- matchResume ----------
222
+ export async function matchResume(text, opts = {}) {
223
+ const { terms, cities } = extractResumeSignals(text ?? "");
224
+ const topN = Math.max(1, opts.topN ?? 5);
225
+ const candidates = Math.max(topN, opts.candidates ?? 200);
226
+ const all = await fetchAllPositions({ pageSize: 50, maxPages: Math.ceil(candidates / 50) });
227
+ if (!all.ok) {
228
+ return {
229
+ ok: false,
230
+ source: SOURCE,
231
+ message: all.message,
232
+ extracted_terms: terms,
233
+ city_preferences: cities,
234
+ matches: [],
235
+ };
236
+ }
237
+ const scored = [];
238
+ for (const p of all.positions) {
239
+ const haystack = `${p.title} ${p.project} ${p.bgs} ${p.work_cities}`;
240
+ const score = scoreOverlap(haystack, terms, cities).score;
241
+ if (score > 0)
242
+ scored.push({ score, position: p });
243
+ }
244
+ scored.sort((a, b) => b.score - a.score);
245
+ return {
246
+ ok: true,
247
+ source: SOURCE,
248
+ extracted_terms: terms,
249
+ city_preferences: cities,
250
+ candidate_pool: all.positions.length,
251
+ matches: scored.slice(0, topN).map((s) => s.position),
252
+ };
253
+ }
254
+ export { extractResumeSignals, scoreOverlap };
255
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_oppo } from "./apply.js";
256
+ export async function fetchApplicationSchema(postId) {
257
+ const id = (postId ?? "").trim();
258
+ if (!id)
259
+ return { ok: false, source: "careers.oppo.com", message: "post_id is required" };
260
+ let title = "";
261
+ let applyUrl = "https://careers.oppo.com";
262
+ try {
263
+ const detail = (await fetchPositionDetail(id));
264
+ if (detail?.ok === false) {
265
+ return { ok: false, source: "careers.oppo.com", message: detail.message ?? "post not found" };
266
+ }
267
+ title = detail?.title ?? "";
268
+ if (detail?.apply_url)
269
+ applyUrl = detail.apply_url;
270
+ }
271
+ catch { }
272
+ return {
273
+ ok: true,
274
+ schema: _buildBespokeApplySchema_oppo({
275
+ source: "careers.oppo.com",
276
+ postId: id,
277
+ jobTitle: title,
278
+ applyUrl,
279
+ submitEndpoint: "https://careers.oppo.com/api/delivery/saveDelivery",
280
+ submitKind: "multipart-session",
281
+ endpointVerified: true,
282
+ submitNotes: "OPPO — POST /api/delivery/saveDelivery with session cookie. Endpoint anon-probed → HTTP 500 + Spring \"Internal Server Error\" (real Spring controller; the /api/delivery/* sub-tree was discovered by reading the SPA's resume-787081aa.js chunk which references /api/delivery/getDeliveryInfo etc, then probing siblings — all 7 candidates returned 500 = real routes). The original /openapi/position/apply returned structured 404 from a different Spring service. Body shape still needs validation.",
283
+ }),
284
+ };
285
+ }