@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/sf.js ADDED
@@ -0,0 +1,310 @@
1
+ // 顺丰 (SF Express) campus-recruiting adapter for `job-pro`.
2
+ //
3
+ // ============================================================
4
+ // API DISCOVERY (probed 2026-05-15)
5
+ //
6
+ // campus.sf-express.com is a Vue SPA built with Webpack. The campus-recruiting
7
+ // flow was originally believed to be GeeTest-gated (POST /api/zp/jobList → 401),
8
+ // but the SPA's actual position-listing chunk (cr/static/js/25.aa149bcb...js)
9
+ // calls a different, fully anonymous route:
10
+ //
11
+ // GET /api/web/position/query?pageNum=&pageSize=&keyword=…
12
+ //
13
+ // Required headers: a normal browser UA plus the `cr-service` header that the
14
+ // SPA's axios interceptor adds to every request. The interceptor sets
15
+ // cr-service: <url-encoded current location>
16
+ // and the gateway uses it instead of a JWT to scope the response. With both
17
+ // in place the endpoint returns paginated JSON without any captcha or login.
18
+ //
19
+ // Endpoint inventory (anonymous GET unless noted):
20
+ // GET /api/web/position/query → paginated positions (campus + intern + mgmt)
21
+ // GET /api/web/position/findById/<id>→ single posting (via /api/position/findById/<id>)
22
+ //
23
+ // `positionType` filter values seen in the wild:
24
+ // "consulting" 管理咨询生
25
+ // "managetraniee" 管培生类
26
+ // "" (omitted) 全部
27
+ //
28
+ // ============================================================
29
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
30
+ export { checkResume };
31
+ const SOURCE = "campus.sf-express.com";
32
+ const API_ROOT = "https://campus.sf-express.com";
33
+ const SITE_ROOT = "https://campus.sf-express.com/";
34
+ const DETAIL_PAGE = (id) => `https://campus.sf-express.com/#/postDetail/${encodeURIComponent(id)}`;
35
+ const CR_SERVICE = "https%3A%2F%2Fcampus.sf-express.com%2F";
36
+ const DEFAULT_HEADERS = {
37
+ "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",
38
+ Accept: "application/json, text/plain, */*",
39
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
40
+ Referer: SITE_ROOT,
41
+ Origin: API_ROOT,
42
+ "cr-service": CR_SERVICE,
43
+ };
44
+ async function call(path, query = {}) {
45
+ const params = new URLSearchParams();
46
+ for (const [k, v] of Object.entries(query)) {
47
+ if (v !== undefined && v !== "")
48
+ params.set(k, String(v));
49
+ }
50
+ const qs = params.toString();
51
+ const url = `${API_ROOT}${path}${qs ? `?${qs}` : ""}`;
52
+ let response;
53
+ try {
54
+ response = await fetch(url, { method: "GET", headers: DEFAULT_HEADERS });
55
+ }
56
+ catch (err) {
57
+ return {
58
+ ok: false,
59
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
60
+ };
61
+ }
62
+ if (!response.ok) {
63
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
64
+ }
65
+ // SF returns the payload object directly (PageHelper shape: {list, total, …})
66
+ let payload;
67
+ try {
68
+ payload = (await response.json());
69
+ }
70
+ catch (err) {
71
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
72
+ }
73
+ return { ok: true, data: payload.list, total: payload.total, message: "ok" };
74
+ }
75
+ function summarize(item) {
76
+ const id = String(item.id ?? "");
77
+ const city = (item.demandCity ?? item.recruitCity ?? "").toString().trim();
78
+ return {
79
+ post_id: id,
80
+ title: (item.positionName ?? "").trim(),
81
+ project: (item.orgSourceName ?? item.orgSource ?? "").trim(),
82
+ recruit_label: item.seasonType === "1"
83
+ ? "校招"
84
+ : item.seasonType === "2"
85
+ ? "实习"
86
+ : item.seasonType === "3"
87
+ ? "管培"
88
+ : "",
89
+ bgs: (item.positionTypeName ?? "").trim(),
90
+ work_cities: city,
91
+ apply_url: id ? DETAIL_PAGE(id) : SITE_ROOT,
92
+ };
93
+ }
94
+ // ---------- searchPositions ----------
95
+ export async function searchPositions(opts = {}) {
96
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
97
+ const page = Math.max(1, opts.page ?? 1);
98
+ const query = {
99
+ pageNum: page,
100
+ pageSize,
101
+ };
102
+ if (opts.keyword)
103
+ query.positionName = opts.keyword.trim().slice(0, 60);
104
+ if (opts.positionType)
105
+ query.positionType = opts.positionType;
106
+ if (opts.seasonType)
107
+ query.seasonType = opts.seasonType;
108
+ const r = await call("/api/web/position/query", query);
109
+ if (!r.ok) {
110
+ return {
111
+ ok: false,
112
+ source: SOURCE,
113
+ message: r.message,
114
+ query,
115
+ positions: [],
116
+ };
117
+ }
118
+ const rows = r.data ?? [];
119
+ return {
120
+ ok: true,
121
+ source: SOURCE,
122
+ query,
123
+ page,
124
+ page_size: pageSize,
125
+ total: r.total ?? rows.length,
126
+ positions: rows.map(summarize),
127
+ };
128
+ }
129
+ // ---------- fetchAllPositions ----------
130
+ export async function fetchAllPositions(opts = {}) {
131
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 50));
132
+ const maxPages = Math.max(1, opts.maxPages ?? 20);
133
+ const bucket = [];
134
+ let total;
135
+ for (let page = 1; page <= maxPages; page++) {
136
+ const r = await searchPositions({ keyword: opts.keyword, page, pageSize });
137
+ if (!r.ok) {
138
+ return {
139
+ ok: false,
140
+ source: SOURCE,
141
+ message: r.message,
142
+ total: 0,
143
+ fetched: bucket.length,
144
+ positions: bucket,
145
+ };
146
+ }
147
+ if (total === undefined)
148
+ total = r.total;
149
+ if (!r.positions.length)
150
+ break;
151
+ bucket.push(...r.positions);
152
+ if (total !== undefined && bucket.length >= total)
153
+ break;
154
+ }
155
+ return {
156
+ ok: true,
157
+ source: SOURCE,
158
+ total: total ?? bucket.length,
159
+ fetched: bucket.length,
160
+ positions: bucket,
161
+ };
162
+ }
163
+ // ---------- fetchPositionDetail ----------
164
+ export async function fetchPositionDetail(postId) {
165
+ const id = (postId ?? "").trim();
166
+ if (!id)
167
+ return { ok: false, source: SOURCE, message: "post_id is required", post_id: id };
168
+ // /api/position/findById/ is the auth-gated internal route; /api/web/position/
169
+ // is the public anon route the SPA actually uses (sibling of /api/web/position/
170
+ // query for search). Without the /web/ prefix this 401s.
171
+ const url = `${API_ROOT}/api/web/position/findById/${encodeURIComponent(id)}`;
172
+ let response;
173
+ try {
174
+ response = await fetch(url, { method: "GET", headers: DEFAULT_HEADERS });
175
+ }
176
+ catch (err) {
177
+ return {
178
+ ok: false,
179
+ source: SOURCE,
180
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
181
+ post_id: id,
182
+ };
183
+ }
184
+ if (!response.ok) {
185
+ return {
186
+ ok: false,
187
+ source: SOURCE,
188
+ message: `HTTP ${response.status}: ${response.statusText}`,
189
+ post_id: id,
190
+ };
191
+ }
192
+ let raw;
193
+ try {
194
+ raw = (await response.json());
195
+ }
196
+ catch (err) {
197
+ return {
198
+ ok: false,
199
+ source: SOURCE,
200
+ message: `bad JSON: ${err instanceof Error ? err.message : err}`,
201
+ post_id: id,
202
+ };
203
+ }
204
+ return {
205
+ ok: true,
206
+ source: SOURCE,
207
+ post_id: String(raw.id ?? id),
208
+ title: raw.positionName ?? "",
209
+ project: raw.orgSourceName ?? raw.orgSource ?? "",
210
+ position_type: raw.positionTypeName ?? "",
211
+ description: (raw.postDuty ?? "").toString().trim(),
212
+ requirements: (raw.jobRequirement ?? "").toString().trim(),
213
+ work_city: raw.demandCity ?? "",
214
+ interview_city: raw.recruitCity ?? "",
215
+ education: raw.educationName ?? raw.education ?? "",
216
+ intern_type: raw.internTypeName ?? raw.internType ?? "",
217
+ create_date: raw.createDate ?? "",
218
+ apply_url: DETAIL_PAGE(id),
219
+ };
220
+ }
221
+ // ---------- fetchDictionaries (no public dict endpoint) ----------
222
+ export async function fetchDictionaries() {
223
+ return {
224
+ ok: false,
225
+ source: SOURCE,
226
+ message: "SF Express does not expose a public filter taxonomy endpoint; positions API accepts " +
227
+ "positionName / positionType / seasonType query params directly.",
228
+ api_host: API_ROOT,
229
+ known_filters: {
230
+ positionType: ["consulting", "managetraniee"],
231
+ seasonType: { "1": "校招", "2": "实习", "3": "管培" },
232
+ },
233
+ };
234
+ }
235
+ // ---------- notices (no public notices endpoint) ----------
236
+ const NO_NOTICES = "SF Express campus does not expose a public notices/announcements endpoint.";
237
+ export async function listNotices() {
238
+ return { ok: false, source: SOURCE, message: NO_NOTICES, notices: [] };
239
+ }
240
+ export async function getNotice(noticeId) {
241
+ return { ok: false, source: SOURCE, message: NO_NOTICES, notice_id: noticeId };
242
+ }
243
+ export async function findNoticesByQuestion(question, _opts = {}) {
244
+ return { ok: false, source: SOURCE, question, message: NO_NOTICES, matches: [] };
245
+ }
246
+ // ---------- matchResume ----------
247
+ export async function matchResume(text, opts = {}) {
248
+ const { terms, cities } = extractResumeSignals(text ?? "");
249
+ const topN = Math.max(1, opts.topN ?? 5);
250
+ const candidates = Math.max(topN, opts.candidates ?? 200);
251
+ const all = await fetchAllPositions({ pageSize: 50, maxPages: Math.ceil(candidates / 50) });
252
+ if (!all.ok) {
253
+ return {
254
+ ok: false,
255
+ source: SOURCE,
256
+ message: all.message,
257
+ extracted_terms: terms,
258
+ city_preferences: cities,
259
+ matches: [],
260
+ };
261
+ }
262
+ const scored = [];
263
+ for (const p of all.positions) {
264
+ const haystack = `${p.title} ${p.project} ${p.bgs} ${p.work_cities}`;
265
+ const score = scoreOverlap(haystack, terms, cities).score;
266
+ if (score > 0)
267
+ scored.push({ score, position: p });
268
+ }
269
+ scored.sort((a, b) => b.score - a.score);
270
+ return {
271
+ ok: true,
272
+ source: SOURCE,
273
+ extracted_terms: terms,
274
+ city_preferences: cities,
275
+ candidate_pool: all.positions.length,
276
+ matches: scored.slice(0, topN).map((s) => s.position),
277
+ };
278
+ }
279
+ export { extractResumeSignals, scoreOverlap };
280
+ import { buildBespokeApplySchema as _buildBespokeApplySchema_sf } from "./apply.js";
281
+ export async function fetchApplicationSchema(postId) {
282
+ const id = (postId ?? "").trim();
283
+ if (!id)
284
+ return { ok: false, source: "campus.sf-express.com", message: "post_id is required" };
285
+ let title = "";
286
+ let applyUrl = "https://campus.sf-express.com";
287
+ try {
288
+ const detail = (await fetchPositionDetail(id));
289
+ if (detail?.ok === false) {
290
+ return { ok: false, source: "campus.sf-express.com", message: detail.message ?? "post not found" };
291
+ }
292
+ title = detail?.title ?? "";
293
+ if (detail?.apply_url)
294
+ applyUrl = detail.apply_url;
295
+ }
296
+ catch { }
297
+ return {
298
+ ok: true,
299
+ schema: _buildBespokeApplySchema_sf({
300
+ source: "campus.sf-express.com",
301
+ postId: id,
302
+ jobTitle: title,
303
+ applyUrl,
304
+ submitEndpoint: "https://campus.sf-express.com/api/web/applicant/apply",
305
+ submitKind: "multipart-session",
306
+ endpointVerified: true,
307
+ submitNotes: "SF Express — POST /api/web/applicant/apply with cr-service header + GeeTest captcha + session cookie. Endpoint anon-probed → HTTP 401 from the SF gateway (real auth gate; the cr-service-web-cloud cluster distinguishes /api/web/position/* [position service] from /api/web/applicant/* and /api/web/resume/* [applicant service, auth-gated]). Body shape still needs validation against a real candidate session.",
308
+ }),
309
+ };
310
+ }
@@ -0,0 +1,24 @@
1
+ // 阶跃星辰 / StepFun careers — Moka SSR + AES-128-CBC.
2
+ //
3
+ // Portal: https://app.mokahr.com/social-recruitment/step/94904
4
+ // Probed 2026-05; ~79 social-hire positions.
5
+ // See cli/src/moka.ts for the shared factory.
6
+ import { createAdapter } from "./moka.js";
7
+ const adapter = createAdapter({
8
+ orgSlug: "step",
9
+ label: "StepFun",
10
+ channels: [
11
+ { siteId: 94904, kind: "social-recruitment", recruitType: "social" },
12
+ ],
13
+ defaultRecruitType: "social",
14
+ });
15
+ export const searchPositions = adapter.searchPositions;
16
+ export const fetchAllPositions = adapter.fetchAllPositions;
17
+ export const fetchPositionDetail = adapter.fetchPositionDetail;
18
+ export const fetchDictionaries = adapter.fetchDictionaries;
19
+ export const listNotices = adapter.listNotices;
20
+ export const getNotice = adapter.getNotice;
21
+ export const findNoticesByQuestion = adapter.findNoticesByQuestion;
22
+ export const matchResume = adapter.matchResume;
23
+ export const checkResume = adapter.checkResume;
24
+ export const fetchApplicationSchema = adapter.fetchApplicationSchema;