@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.
- package/dist/adapter.js +17 -0
- package/dist/agibot.js +399 -0
- package/dist/alibaba.js +509 -0
- package/dist/antgroup.js +397 -0
- package/dist/apply.js +1373 -0
- package/dist/baichuan.js +49 -0
- package/dist/baidu.js +452 -0
- package/dist/bilibili.js +455 -0
- package/dist/byd.js +412 -0
- package/dist/bytedance.js +619 -0
- package/dist/cainiao.js +56 -0
- package/dist/cambricon.js +33 -0
- package/dist/cdp.js +237 -0
- package/dist/cicc.js +56 -0
- package/dist/coverage.js +60 -0
- package/dist/deepseek.js +25 -0
- package/dist/didi.js +381 -0
- package/dist/feishu.js +577 -0
- package/dist/galaxyuniversal.js +24 -0
- package/dist/geely.js +35 -0
- package/dist/greenhouse.js +432 -0
- package/dist/hikvision.js +58 -0
- package/dist/horizonrobotics.js +46 -0
- package/dist/hoyoverse.js +26 -0
- package/dist/huawei.js +537 -0
- package/dist/iflytek.js +380 -0
- package/dist/index.js +1828 -0
- package/dist/iqiyi.js +494 -0
- package/dist/jd.js +559 -0
- package/dist/kuaishou.js +496 -0
- package/dist/lever.js +455 -0
- package/dist/liauto.js +393 -0
- package/dist/liepin.js +357 -0
- package/dist/lilith.js +300 -0
- package/dist/megvii.js +27 -0
- package/dist/meituan.js +633 -0
- package/dist/memory.js +76 -0
- package/dist/mihoyo.js +308 -0
- package/dist/minimax.js +32 -0
- package/dist/moka.js +473 -0
- package/dist/moonshot.js +24 -0
- package/dist/netease.js +424 -0
- package/dist/nio.js +24 -0
- package/dist/oppo.js +285 -0
- package/dist/pdd.js +614 -0
- package/dist/pingan.js +493 -0
- package/dist/sensetime.js +51 -0
- package/dist/sf.js +310 -0
- package/dist/stepfun.js +24 -0
- package/dist/tencent.js +770 -0
- package/dist/trip.js +396 -0
- package/dist/unitree.js +418 -0
- package/dist/vivo.js +361 -0
- package/dist/webank.js +55 -0
- package/dist/wecruit.js +438 -0
- package/dist/weibo.js +337 -0
- package/dist/weride.js +29 -0
- package/dist/xiaohongshu.js +480 -0
- package/dist/xiaomi.js +529 -0
- package/dist/xpeng.js +34 -0
- package/dist/zerooneai.js +42 -0
- package/dist/zhipu.js +478 -0
- package/extension/README.md +79 -0
- package/extension/background.js +177 -0
- package/extension/manifest.json +55 -0
- package/extension/popup.html +37 -0
- package/extension/popup.js +54 -0
- package/package.json +61 -0
package/dist/pingan.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
// Thin client for Ping An's (中国平安) public campus-recruiting API at campus.pingan.com.
|
|
2
|
+
//
|
|
3
|
+
// campus.pingan.com is a self-hosted Vue 2 SPA (webpack). All API calls
|
|
4
|
+
// go through a single backend at:
|
|
5
|
+
//
|
|
6
|
+
// https://campus.pingan.com/zztj-recruit-talent-webserver/rctt
|
|
7
|
+
//
|
|
8
|
+
// Endpoints are unauthenticated for read-only operations; the server returns
|
|
9
|
+
// JSON with an envelope { responseCode, responseMsg, data }.
|
|
10
|
+
// Success code is "10001"; "20006" = missing required parameter.
|
|
11
|
+
//
|
|
12
|
+
// ============================================================
|
|
13
|
+
// Endpoint inventory (probed 2026-05, JS bundle app.0687451e.js +
|
|
14
|
+
// chunk_freshStudent~chunk_internStudent~chunk_position.aba9b06f.js):
|
|
15
|
+
//
|
|
16
|
+
// POST /candidate/officialWebsite/selectGroupOfficial
|
|
17
|
+
// Gets the wecruitId (session-like token) for a given campus site.
|
|
18
|
+
// Required payload: { websiteType: "3", officialUrl: "campus.pingan.com",
|
|
19
|
+
// recruitType: "3" }
|
|
20
|
+
// Response: { responseCode:"10001", data:"<32-char-hex-wecruitId>" }
|
|
21
|
+
// websiteType values: 3 = 集团官网/Group campus site (confirmed).
|
|
22
|
+
// The wecruitId for the production Group campus site is stable across
|
|
23
|
+
// requests (probed 2026-05: "6c1db1bba8c33deab19a733ec785711a").
|
|
24
|
+
// We re-fetch it live on each cold start and cache in-process.
|
|
25
|
+
//
|
|
26
|
+
// POST /candidate/position/campus/positionSearch/queryPositionPage
|
|
27
|
+
// Search / list positions.
|
|
28
|
+
// Required: { wecruitId, pageNo, pageSize }
|
|
29
|
+
// Optional filters: { keyWord, workCity, interviewCity, businessUnitId,
|
|
30
|
+
// positionCategoryId, positionType }
|
|
31
|
+
// Response: { responseCode:"10001", data:{ list:[...], pageNo, pageSize,
|
|
32
|
+
// totalCount, totalPage } }
|
|
33
|
+
// Total positions (no filter, 2026-05): ~849 across all subsidiaries.
|
|
34
|
+
//
|
|
35
|
+
// POST /candidate/position/campus/positionSearch/queryPositionDetail
|
|
36
|
+
// Fetch a single position's full detail.
|
|
37
|
+
// Required: { positionId: "<idPosition>", wecruitId }
|
|
38
|
+
// Response: { data: { position:{...}, description, checkResumeRepeat } }
|
|
39
|
+
//
|
|
40
|
+
// POST /candidate/position/campus/positionSearch/queryCityCompanyCategory
|
|
41
|
+
// Returns filter taxonomy (cities, subsidiaries, positionCategoryMap).
|
|
42
|
+
// Required: { wecruitId }
|
|
43
|
+
// Returns: { data: { domesticCity, overseasCity, interviewCity,
|
|
44
|
+
// campusCompanyMap, positionCategoryMap,
|
|
45
|
+
// newPositionCategory, specialCompany } }
|
|
46
|
+
//
|
|
47
|
+
// ============================================================
|
|
48
|
+
// Filter semantics (from JS bundle analysis + probing, 2026-05):
|
|
49
|
+
//
|
|
50
|
+
// positionType (招聘性质)
|
|
51
|
+
// "全职" = 应届生 full-time positions ~787 posts
|
|
52
|
+
// "实习" = intern positions ~62 posts
|
|
53
|
+
// No filter = all ~849 posts
|
|
54
|
+
//
|
|
55
|
+
// positionCategoryId (职位类别, short codes used in real data):
|
|
56
|
+
// C001 技术类 C003 产品类 C004 设计类
|
|
57
|
+
// C005 市场类 C006 职能类 C009 业务类
|
|
58
|
+
// C015 共同资源类 C016 管培生
|
|
59
|
+
// (These come from position.positionCategoryId in list responses, not from
|
|
60
|
+
// the positionCategoryMap which uses UUID keys — the UUID keys do NOT match.)
|
|
61
|
+
//
|
|
62
|
+
// businessUnitId (成员公司/subsidiary):
|
|
63
|
+
// PA001 平安集团 PA002 平安寿险 PA004 平安产险
|
|
64
|
+
// PA006 平安银行 PA010 平安健康险 PA011 平安证券
|
|
65
|
+
// PA014 平安资管 PA017 陆控 PA021 平安科技
|
|
66
|
+
// PA023 平安医疗健康 PA026 平安租赁 PA043 金融壹帐通
|
|
67
|
+
// (From real position data — not exhaustive; more exist)
|
|
68
|
+
//
|
|
69
|
+
// workCity / interviewCity: Chinese city name string, e.g. "上海市", "北京市"
|
|
70
|
+
//
|
|
71
|
+
// ============================================================
|
|
72
|
+
// Position detail URL (from chunk_positionDetail.24051db4.js analysis):
|
|
73
|
+
// https://campus.pingan.com/positionDetail?positionId=<idPosition>
|
|
74
|
+
//
|
|
75
|
+
// ============================================================
|
|
76
|
+
// Subsidiaries sharing the API: ALL Ping An group entities (平安集团, 平安银行,
|
|
77
|
+
// 平安寿险, 平安科技, etc.) share a single campus.pingan.com portal and the
|
|
78
|
+
// same API backend. There is no separate endpoint per subsidiary.
|
|
79
|
+
// The businessUnitId field in responses identifies the specific entity.
|
|
80
|
+
//
|
|
81
|
+
// ============================================================
|
|
82
|
+
// Endpoints that exist but require login (10005 response):
|
|
83
|
+
// POST /candidate/campus/deliveryRecord/getAll (application history)
|
|
84
|
+
// POST /candidate/campus/deliveryRecord/insertJobIntension (apply)
|
|
85
|
+
//
|
|
86
|
+
// No public /notices or /announcements equivalent found.
|
|
87
|
+
//
|
|
88
|
+
// ============================================================
|
|
89
|
+
// ---- PositionSummary field mapping (PingAn → canonical) ----
|
|
90
|
+
// post_id ← item.idPosition (32-char hex UUID)
|
|
91
|
+
// title ← item.positionName
|
|
92
|
+
// project ← item.positionCategoryName (职位类别)
|
|
93
|
+
// recruit_label ← item.positionType (全职 / 实习)
|
|
94
|
+
// bgs ← item.businessUnitName + " / " + (item.deptShowName || item.deptName)
|
|
95
|
+
// work_cities ← item.workCity
|
|
96
|
+
// apply_url ← https://campus.pingan.com/positionDetail?positionId=<id>
|
|
97
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
98
|
+
export { extractResumeSignals, scoreOverlap, checkResume };
|
|
99
|
+
const API_ROOT = "https://campus.pingan.com/zztj-recruit-talent-webserver/rctt";
|
|
100
|
+
const CAMPUS_PAGE = "https://campus.pingan.com";
|
|
101
|
+
const DETAIL_PAGE = (id) => `${CAMPUS_PAGE}/positionDetail?positionId=${encodeURIComponent(id)}`;
|
|
102
|
+
const DEFAULT_HEADERS = {
|
|
103
|
+
"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",
|
|
104
|
+
Accept: "application/json;charset=utf-8",
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
Origin: CAMPUS_PAGE,
|
|
107
|
+
Referer: `${CAMPUS_PAGE}/`,
|
|
108
|
+
};
|
|
109
|
+
async function call(path, body) {
|
|
110
|
+
const url = `${API_ROOT}${path}`;
|
|
111
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await fetch(url, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: DEFAULT_HEADERS,
|
|
116
|
+
body: JSON.stringify(body),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
127
|
+
}
|
|
128
|
+
let payload;
|
|
129
|
+
try {
|
|
130
|
+
payload = (await response.json());
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
134
|
+
}
|
|
135
|
+
const ok = payload.responseCode === "10001";
|
|
136
|
+
return {
|
|
137
|
+
ok,
|
|
138
|
+
data: payload.data,
|
|
139
|
+
message: payload.responseMsg || (ok ? "ok" : "upstream error"),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ---------- wecruitId cache ----------
|
|
143
|
+
// wecruitId is a session-like token that the Group campus site issues.
|
|
144
|
+
// It is stable across requests (same value for campus.pingan.com in all probes).
|
|
145
|
+
// We fetch it once per process and cache it.
|
|
146
|
+
let _wecruitIdCache = null;
|
|
147
|
+
async function getWecruitId() {
|
|
148
|
+
if (_wecruitIdCache !== null)
|
|
149
|
+
return _wecruitIdCache;
|
|
150
|
+
const result = await call("/candidate/officialWebsite/selectGroupOfficial", {
|
|
151
|
+
websiteType: "3", // 集团官网/Group campus
|
|
152
|
+
officialUrl: "campus.pingan.com",
|
|
153
|
+
recruitType: "3", // campus / 校园招聘
|
|
154
|
+
});
|
|
155
|
+
if (result.ok && typeof result.data === "string" && result.data.length > 0) {
|
|
156
|
+
_wecruitIdCache = result.data;
|
|
157
|
+
return _wecruitIdCache;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function summarizePosition(item) {
|
|
162
|
+
const id = item.idPosition ?? "";
|
|
163
|
+
const dept = (item.deptShowName ?? item.deptName ?? "").trim();
|
|
164
|
+
const company = (item.businessUnitName ?? "").trim();
|
|
165
|
+
const bgs = dept ? `${company} / ${dept}` : company;
|
|
166
|
+
return {
|
|
167
|
+
post_id: id,
|
|
168
|
+
title: item.positionName ?? "",
|
|
169
|
+
project: item.positionCategoryName ?? "",
|
|
170
|
+
recruit_label: item.positionType ?? "",
|
|
171
|
+
bgs,
|
|
172
|
+
work_cities: (item.workCity ?? "").trim(),
|
|
173
|
+
apply_url: id ? DETAIL_PAGE(id) : CAMPUS_PAGE,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// ---------- searchPositions ----------
|
|
177
|
+
export async function searchPositions(opts = {}) {
|
|
178
|
+
const wecruitId = await getWecruitId();
|
|
179
|
+
if (!wecruitId) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
source: "campus.pingan.com",
|
|
183
|
+
message: "could not obtain wecruitId from selectGroupOfficial",
|
|
184
|
+
positions: [],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
188
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
189
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 20);
|
|
190
|
+
const payload = {
|
|
191
|
+
wecruitId,
|
|
192
|
+
pageNo: page,
|
|
193
|
+
pageSize,
|
|
194
|
+
};
|
|
195
|
+
if (keyword)
|
|
196
|
+
payload["keyWord"] = keyword;
|
|
197
|
+
if (opts.workCity)
|
|
198
|
+
payload["workCity"] = opts.workCity.trim();
|
|
199
|
+
if (opts.interviewCity)
|
|
200
|
+
payload["interviewCity"] = opts.interviewCity.trim();
|
|
201
|
+
if (opts.businessUnitId)
|
|
202
|
+
payload["businessUnitId"] = opts.businessUnitId.trim();
|
|
203
|
+
if (opts.positionCategoryId)
|
|
204
|
+
payload["positionCategoryId"] = opts.positionCategoryId.trim();
|
|
205
|
+
// positionType: only inject when explicitly provided (undefined = all types)
|
|
206
|
+
if (opts.positionType !== undefined)
|
|
207
|
+
payload["positionType"] = opts.positionType;
|
|
208
|
+
const response = await call("/candidate/position/campus/positionSearch/queryPositionPage", payload);
|
|
209
|
+
if (!response.ok || !response.data) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
source: "campus.pingan.com",
|
|
213
|
+
message: response.message,
|
|
214
|
+
query: payload,
|
|
215
|
+
positions: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const rows = response.data.list ?? [];
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
source: "campus.pingan.com",
|
|
222
|
+
query: payload,
|
|
223
|
+
page,
|
|
224
|
+
page_size: pageSize,
|
|
225
|
+
total: response.data.totalCount ?? rows.length,
|
|
226
|
+
total_pages: response.data.totalPage,
|
|
227
|
+
positions: rows.map(summarizePosition),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// ---------- fetchAllPositions ----------
|
|
231
|
+
export async function fetchAllPositions(opts = {}) {
|
|
232
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
|
|
233
|
+
const maxPages = Math.max(1, opts.maxPages ?? 10);
|
|
234
|
+
const bucket = [];
|
|
235
|
+
let total;
|
|
236
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
237
|
+
const result = await searchPositions({ ...opts, page, pageSize });
|
|
238
|
+
if (!result.ok) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
source: "campus.pingan.com",
|
|
242
|
+
message: result.message,
|
|
243
|
+
fetched: bucket.length,
|
|
244
|
+
positions: bucket,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (total === undefined)
|
|
248
|
+
total = result.total;
|
|
249
|
+
if (!result.positions.length)
|
|
250
|
+
break;
|
|
251
|
+
bucket.push(...result.positions);
|
|
252
|
+
if (total !== undefined && bucket.length >= total)
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
source: "campus.pingan.com",
|
|
258
|
+
total: total ?? bucket.length,
|
|
259
|
+
fetched: bucket.length,
|
|
260
|
+
positions: bucket,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// ---------- fetchPositionDetail ----------
|
|
264
|
+
export async function fetchPositionDetail(positionId) {
|
|
265
|
+
const id = (positionId ?? "").trim();
|
|
266
|
+
if (!id) {
|
|
267
|
+
return { ok: false, source: "campus.pingan.com", message: "positionId is required" };
|
|
268
|
+
}
|
|
269
|
+
const wecruitId = await getWecruitId();
|
|
270
|
+
if (!wecruitId) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
source: "campus.pingan.com",
|
|
274
|
+
post_id: id,
|
|
275
|
+
message: "could not obtain wecruitId",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const response = await call("/candidate/position/campus/positionSearch/queryPositionDetail", { positionId: id, wecruitId });
|
|
279
|
+
if (!response.ok || !response.data) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
source: "campus.pingan.com",
|
|
283
|
+
post_id: id,
|
|
284
|
+
message: response.message || "no detail returned",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const pos = response.data.position ?? {};
|
|
288
|
+
const dept = (pos.deptShowName ?? pos.deptName ?? "").trim();
|
|
289
|
+
const company = (pos.businessUnitName ?? "").trim();
|
|
290
|
+
return {
|
|
291
|
+
ok: true,
|
|
292
|
+
source: "campus.pingan.com",
|
|
293
|
+
post_id: id,
|
|
294
|
+
title: pos.positionName ?? "",
|
|
295
|
+
direction: pos.positionCategoryName ?? "",
|
|
296
|
+
project: pos.positionCategoryName ?? "",
|
|
297
|
+
recruit_label: pos.positionType ?? "",
|
|
298
|
+
description: pos.duty ?? response.data.description ?? "",
|
|
299
|
+
requirements: pos.qualification ?? "",
|
|
300
|
+
education: pos.education ?? "",
|
|
301
|
+
recruit_number: pos.recruitNumber,
|
|
302
|
+
work_cities: (pos.workCity ?? "").trim(),
|
|
303
|
+
interview_city: (pos.interviewCity ?? "").trim(),
|
|
304
|
+
bgs: dept ? `${company} / ${dept}` : company,
|
|
305
|
+
apply_url: DETAIL_PAGE(id),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// ---------- fetchDictionaries ----------
|
|
309
|
+
export async function fetchDictionaries() {
|
|
310
|
+
const wecruitId = await getWecruitId();
|
|
311
|
+
if (!wecruitId) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
source: "campus.pingan.com",
|
|
315
|
+
message: "could not obtain wecruitId",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const response = await call("/candidate/position/campus/positionSearch/queryCityCompanyCategory", { wecruitId });
|
|
319
|
+
if (!response.ok || !response.data) {
|
|
320
|
+
return { ok: false, source: "campus.pingan.com", message: response.message };
|
|
321
|
+
}
|
|
322
|
+
const d = response.data;
|
|
323
|
+
// Flatten domestic cities from alphabetically-grouped map
|
|
324
|
+
const domesticCities = [];
|
|
325
|
+
for (const entries of Object.values(d.domesticCity ?? {})) {
|
|
326
|
+
for (const e of entries) {
|
|
327
|
+
const city = e["workCity"];
|
|
328
|
+
if (typeof city === "string" && city)
|
|
329
|
+
domesticCities.push(city);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const companySectors = {};
|
|
333
|
+
for (const [sector, companies] of Object.entries(d.campusCompanyMap?.data ?? {})) {
|
|
334
|
+
companySectors[sector] = companies.map((c) => String(c["companyName"] ?? ""));
|
|
335
|
+
}
|
|
336
|
+
const positionCategories = (d.positionCategoryMap ?? []).map((c) => ({
|
|
337
|
+
id: c.idPositionCategory ?? "",
|
|
338
|
+
name: c.categoryName ?? "",
|
|
339
|
+
}));
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
source: "campus.pingan.com",
|
|
343
|
+
verified_at: new Date().toISOString(),
|
|
344
|
+
wecruitId,
|
|
345
|
+
domestic_cities: domesticCities,
|
|
346
|
+
company_sectors: companySectors,
|
|
347
|
+
position_categories: positionCategories,
|
|
348
|
+
note: "positionCategoryId values in search use the short-code from actual positions (C001, C009, etc.) " +
|
|
349
|
+
"not the UUID keys returned by positionCategoryMap.",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
// ---------- stub notices ----------
|
|
353
|
+
// campus.pingan.com has no public announcement/notice endpoint.
|
|
354
|
+
const STUB_NOTICES = {
|
|
355
|
+
ok: false,
|
|
356
|
+
source: "campus.pingan.com",
|
|
357
|
+
message: "PingAn campus: no public notices endpoint discovered",
|
|
358
|
+
};
|
|
359
|
+
export async function listNotices() {
|
|
360
|
+
return STUB_NOTICES;
|
|
361
|
+
}
|
|
362
|
+
export async function getNotice(_id) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
source: "campus.pingan.com",
|
|
366
|
+
message: "PingAn campus: no public notices endpoint discovered",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
source: "campus.pingan.com",
|
|
373
|
+
message: "PingAn campus: no public notices endpoint discovered",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
// ---------- matchResume ----------
|
|
377
|
+
export async function matchResume(text, opts = {}) {
|
|
378
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
379
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
380
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
381
|
+
if (!terms.length) {
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
source: "campus.pingan.com",
|
|
385
|
+
message: "could not extract any technical signals from the text",
|
|
386
|
+
preview: (text ?? "").slice(0, 120),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
390
|
+
const list = await searchPositions({ keyword, pageSize: 100 });
|
|
391
|
+
if (!list.ok) {
|
|
392
|
+
return { ok: false, source: "campus.pingan.com", message: list.message, positions: [] };
|
|
393
|
+
}
|
|
394
|
+
// Fetch a broader raw batch to access duty + qualification fields for scoring
|
|
395
|
+
const wecruitId = await getWecruitId();
|
|
396
|
+
const rawPosts = [];
|
|
397
|
+
if (wecruitId) {
|
|
398
|
+
const raw = await call("/candidate/position/campus/positionSearch/queryPositionPage", { wecruitId, pageNo: 1, pageSize: 100, keyWord: keyword });
|
|
399
|
+
if (raw.ok && raw.data?.list) {
|
|
400
|
+
rawPosts.push(...raw.data.list);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const rawById = new Map();
|
|
404
|
+
for (const p of rawPosts) {
|
|
405
|
+
if (p.idPosition)
|
|
406
|
+
rawById.set(p.idPosition, p);
|
|
407
|
+
}
|
|
408
|
+
const scored = [];
|
|
409
|
+
for (const p of list.positions) {
|
|
410
|
+
const rp = rawById.get(p.post_id);
|
|
411
|
+
const blob = [
|
|
412
|
+
p.title,
|
|
413
|
+
p.project,
|
|
414
|
+
p.recruit_label,
|
|
415
|
+
p.bgs,
|
|
416
|
+
p.work_cities,
|
|
417
|
+
rp?.duty ?? "",
|
|
418
|
+
rp?.qualification ?? "",
|
|
419
|
+
].join(" ");
|
|
420
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
421
|
+
if (score > 0) {
|
|
422
|
+
scored.push({
|
|
423
|
+
score,
|
|
424
|
+
position: p,
|
|
425
|
+
reasons,
|
|
426
|
+
description: rp?.duty,
|
|
427
|
+
requirements: rp?.qualification,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
scored.sort((a, b) => b.score - a.score);
|
|
432
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
433
|
+
if (!shortlist.length) {
|
|
434
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({
|
|
435
|
+
score: 0,
|
|
436
|
+
position,
|
|
437
|
+
reasons: [],
|
|
438
|
+
description: rawById.get(position.post_id)?.duty,
|
|
439
|
+
requirements: rawById.get(position.post_id)?.qualification,
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
443
|
+
const mr = s.reasons.length > 0
|
|
444
|
+
? s.reasons.slice(0, 5)
|
|
445
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
446
|
+
return {
|
|
447
|
+
...s.position,
|
|
448
|
+
description: s.description,
|
|
449
|
+
requirements: s.requirements,
|
|
450
|
+
match_reasons: mr,
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
ok: true,
|
|
455
|
+
source: "campus.pingan.com",
|
|
456
|
+
extracted_terms: terms,
|
|
457
|
+
city_preferences: cities,
|
|
458
|
+
matches,
|
|
459
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
460
|
+
"The only authority on selection is HR.",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
import { buildBespokeApplySchema as _buildBespokeApplySchema_pingan } from "./apply.js";
|
|
464
|
+
export async function fetchApplicationSchema(postId) {
|
|
465
|
+
const id = (postId ?? "").trim();
|
|
466
|
+
if (!id)
|
|
467
|
+
return { ok: false, source: "campus.pingan.com", message: "post_id is required" };
|
|
468
|
+
let title = "";
|
|
469
|
+
let applyUrl = "https://campus.pingan.com";
|
|
470
|
+
try {
|
|
471
|
+
const detail = (await fetchPositionDetail(id));
|
|
472
|
+
if (detail?.ok === false) {
|
|
473
|
+
return { ok: false, source: "campus.pingan.com", message: detail.message ?? "post not found" };
|
|
474
|
+
}
|
|
475
|
+
title = detail?.title ?? "";
|
|
476
|
+
if (detail?.apply_url)
|
|
477
|
+
applyUrl = detail.apply_url;
|
|
478
|
+
}
|
|
479
|
+
catch { }
|
|
480
|
+
return {
|
|
481
|
+
ok: true,
|
|
482
|
+
schema: _buildBespokeApplySchema_pingan({
|
|
483
|
+
source: "campus.pingan.com",
|
|
484
|
+
postId: id,
|
|
485
|
+
jobTitle: title,
|
|
486
|
+
applyUrl,
|
|
487
|
+
submitEndpoint: "https://campus.pingan.com/recruit/api/applyJob",
|
|
488
|
+
submitKind: "multipart-session",
|
|
489
|
+
endpointVerified: true,
|
|
490
|
+
submitNotes: "Ping An — POST /recruit/api/applyJob with session cookie. Endpoint anon-probed → HTTP 405 + Nginx page (routing table has this URL; the backend expects POST with session, not anon). Body shape still needs validation.",
|
|
491
|
+
}),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 商汤 (SenseTime) careers adapter for `job-pro`.
|
|
2
|
+
//
|
|
3
|
+
// ============================================================
|
|
4
|
+
// API DISCOVERY (probed 2026-05-16 via puppeteer-core network capture)
|
|
5
|
+
//
|
|
6
|
+
// hr.sensetime.com hosts a Beisen Wecruit (北森招聘云) tenant. The published
|
|
7
|
+
// SPA bundles at `/SU…/pb/<channel>.html` ALWAYS return nginx 405 on
|
|
8
|
+
// anonymous POST, regardless of headers; that path is GET-only at the LB.
|
|
9
|
+
//
|
|
10
|
+
// The SPA's real XHR target (uncovered by intercepting page traffic in a
|
|
11
|
+
// headless Chrome instance) is on a sibling `/wecruit/...` prefix:
|
|
12
|
+
//
|
|
13
|
+
// POST https://hr.sensetime.com/wecruit/positionInfo/listPosition/<SU…>
|
|
14
|
+
// ?iSaJAx=isAjax&request_locale=zh_CN&t=<unix-ms>
|
|
15
|
+
//
|
|
16
|
+
// Content-Type: application/x-www-form-urlencoded (NOT JSON)
|
|
17
|
+
// Body: isFrompb=true&recruitType=2&pageSize=15¤tPage=1
|
|
18
|
+
//
|
|
19
|
+
// Anonymous, no token, no cookie, no captcha. Probed 2026-05-16: the
|
|
20
|
+
// social channel `SU60fa3bdabef57c1023fc1cbc` returns ~89 pages × 12 ≈
|
|
21
|
+
// 1068 active social-hire positions across SenseTime and its subsidiaries.
|
|
22
|
+
//
|
|
23
|
+
// hr.sensetime.com root redirects to the social channel (302); the campus
|
|
24
|
+
// SU referenced in earlier reconnaissance notes (`SU6710d7c21c240e54e1f82a1b`)
|
|
25
|
+
// has been reassigned to a different tenant ("安徽新华发行集团" appears in
|
|
26
|
+
// its responses), so we only wire the social channel. If SenseTime
|
|
27
|
+
// rebroadcasts a campus channel later, add it to the `channels` array.
|
|
28
|
+
//
|
|
29
|
+
// See cli/src/wecruit.ts for the shared factory.
|
|
30
|
+
import { createAdapter } from "./wecruit.js";
|
|
31
|
+
const adapter = createAdapter({
|
|
32
|
+
host: "hr.sensetime.com",
|
|
33
|
+
label: "SenseTime",
|
|
34
|
+
channels: [
|
|
35
|
+
{
|
|
36
|
+
channelId: "SU60fa3bdabef57c1023fc1cbc",
|
|
37
|
+
recruitType: "social",
|
|
38
|
+
pagePath: "social",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
export const searchPositions = adapter.searchPositions;
|
|
43
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
44
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
45
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
46
|
+
export const listNotices = adapter.listNotices;
|
|
47
|
+
export const getNotice = adapter.getNotice;
|
|
48
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
49
|
+
export const matchResume = adapter.matchResume;
|
|
50
|
+
export const checkResume = adapter.checkResume;
|
|
51
|
+
export const fetchApplicationSchema = adapter.fetchApplicationSchema;
|