@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/iqiyi.js
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// Thin client for iQIYI's public recruiting API at careers.iqiyi.com.
|
|
2
|
+
//
|
|
3
|
+
// iQIYI runs three separate portals on careers.iqiyi.com, all powered by
|
|
4
|
+
// Feishu/Lark ATSX SaaS recruiting product (same engine as jobs.bytedance.com).
|
|
5
|
+
// The portal is determined by the "website-path" request header.
|
|
6
|
+
//
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Endpoint inventory (probed 2026-05, JS bundle 4026.f23f1edc.js):
|
|
9
|
+
//
|
|
10
|
+
// POST https://careers.iqiyi.com/api/v1/search/job/posts
|
|
11
|
+
// Headers: website-path, portal-platform: "pc", Referer (matching portal)
|
|
12
|
+
// Payload: { keyword, limit, offset, portal_type:3, portal_entrance:1, language:"zh",
|
|
13
|
+
// location_code_list, job_function_id_list, recruitment_id_list }
|
|
14
|
+
// Response: { code:0, data:{ job_post_list:[...], count:<int> } }
|
|
15
|
+
//
|
|
16
|
+
// GET https://careers.iqiyi.com/api/v1/config/job/filters/{website_path}
|
|
17
|
+
// Returns city_list, recruitment_type_list, job_function_list, etc.
|
|
18
|
+
// Note: job_type_list and job_subject_list are null/empty for this tenant.
|
|
19
|
+
//
|
|
20
|
+
// GET https://careers.iqiyi.com/api/v1/job/posts/{post_id}
|
|
21
|
+
// Returns { code:0, data:{ job_post_detail:{...}, recommend_job_post_List:[...] } }
|
|
22
|
+
//
|
|
23
|
+
// ============================================================
|
|
24
|
+
// Portal / website-path contexts (probed 2026-05):
|
|
25
|
+
//
|
|
26
|
+
// "job" → 社招官网 (social/experienced hire) ~82 posts
|
|
27
|
+
// Referer: https://careers.iqiyi.com/job
|
|
28
|
+
// recruit_type.parent.id="1" (社招), recruit_type.id="101" (全职)
|
|
29
|
+
// Detail URL: https://careers.iqiyi.com/job/position/{id}/detail
|
|
30
|
+
//
|
|
31
|
+
// "campus" → 应届生校招官网 (campus / new-grad) ~14 posts
|
|
32
|
+
// Referer: https://careers.iqiyi.com/campus/
|
|
33
|
+
// recruit_type.id="201" (正式), parent.id="2" (校招)
|
|
34
|
+
// Detail URL: https://careers.iqiyi.com/campus/position/{id}/detail
|
|
35
|
+
//
|
|
36
|
+
// "intern" → 实习生官网 (intern) ~92 posts
|
|
37
|
+
// Referer: https://careers.iqiyi.com/intern
|
|
38
|
+
// recruit_type.id="202" (实习), parent.id="2" (校招)
|
|
39
|
+
// Detail URL: https://careers.iqiyi.com/intern/position/{id}/detail
|
|
40
|
+
//
|
|
41
|
+
// ============================================================
|
|
42
|
+
// Filter taxonomy (from GET /api/v1/config/job/filters/job, probed 2026-05):
|
|
43
|
+
//
|
|
44
|
+
// DIMENSION 1 — job_function_id_list (职位类别, 2-level hierarchy)
|
|
45
|
+
// Parent "技术" id:7434839649275742501
|
|
46
|
+
// 研发 id:7434839421294070035
|
|
47
|
+
// 算法 id:7434839934659873034
|
|
48
|
+
// Parent "产品" id:7434840292215736586
|
|
49
|
+
// Parent "设计" id:7434840107360061732
|
|
50
|
+
// Parent "运营" id:7434840970217654564
|
|
51
|
+
// 产品运营 id:7434840444482308364
|
|
52
|
+
// 内容运营 id:7434841054879451446
|
|
53
|
+
// 数据分析 id:7434840662343174454
|
|
54
|
+
// Parent "内容制作" id:7434840477089040652
|
|
55
|
+
// 导演 id:7434841298710907145
|
|
56
|
+
// 责编 id:7434841298711005449
|
|
57
|
+
// 评估策划 id:7434841764354066739
|
|
58
|
+
// 视频制作 id:7434841763418573068
|
|
59
|
+
// Parent "经纪" id:7434841763417753895
|
|
60
|
+
// Parent "市场&商务" id:7434841893740267786
|
|
61
|
+
// 商务 id:7434841893487692059
|
|
62
|
+
// 公关 id:7434842142922819881
|
|
63
|
+
// 市场活动 id:7434842142922869033
|
|
64
|
+
// 商业市场 id:7434841764362307867
|
|
65
|
+
// 广告投放 id:7512307407088208164
|
|
66
|
+
// 内容宣推 id:7434842241422215475
|
|
67
|
+
// Parent "销售" id:7434843070048717094
|
|
68
|
+
// 销售 id:7434842895259879743
|
|
69
|
+
// 销售策划 id:7434842715382843660
|
|
70
|
+
// Parent "游戏" id:7434844424062535947
|
|
71
|
+
// 游戏美术 id:7434845034699147539
|
|
72
|
+
// Parent "客服、审核与运营支持" id:7512307407469840681
|
|
73
|
+
// 运营支持 id:7512307631945828619
|
|
74
|
+
// 客服 id:7512307201551616306
|
|
75
|
+
// Parent "财务" id:7434843924135184679
|
|
76
|
+
// Parent "人力资源" id:7434844421612734757
|
|
77
|
+
// Parent "管理" id:7434845114970130738
|
|
78
|
+
//
|
|
79
|
+
// DIMENSION 2 — location_code_list (工作地点, city codes)
|
|
80
|
+
// CT_11=北京 CT_125=上海 CT_190=重庆 CT_78=开封 CT_71=金华
|
|
81
|
+
// CT_172=扬州 CT_98=曼谷 CT_94=洛杉矶
|
|
82
|
+
//
|
|
83
|
+
// DIMENSION 3 — recruitment_id_list (招聘类型, for campus/intern)
|
|
84
|
+
// "201" = 正式 (campus / new-grad)
|
|
85
|
+
// "202" = 实习 (intern)
|
|
86
|
+
// "101" = 全职 (full-time, used in social hire context)
|
|
87
|
+
//
|
|
88
|
+
// ============================================================
|
|
89
|
+
// ---- PositionSummary field mapping (iQIYI → canonical) ----
|
|
90
|
+
// post_id ← item.id (stringified)
|
|
91
|
+
// title ← item.title
|
|
92
|
+
// project ← item.job_function.name (最接近 Tencent projectName)
|
|
93
|
+
// recruit_label ← item.recruit_type.name (e.g. "全职" / "正式" / "实习")
|
|
94
|
+
// bgs ← "" (iQIYI does not expose BG/事业群 in public search)
|
|
95
|
+
// work_cities ← city_list joined with " / " or city_info.name
|
|
96
|
+
// apply_url ← https://careers.iqiyi.com/{website_path}/position/{id}/detail
|
|
97
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
98
|
+
export { checkResume };
|
|
99
|
+
const API_ROOT = "https://careers.iqiyi.com/api/v1";
|
|
100
|
+
// Portal paths and their Referer URLs
|
|
101
|
+
const PORTAL = {
|
|
102
|
+
job: {
|
|
103
|
+
referer: "https://careers.iqiyi.com/job",
|
|
104
|
+
listPage: "https://careers.iqiyi.com/job",
|
|
105
|
+
detailPage: (id) => `https://careers.iqiyi.com/job/position/${encodeURIComponent(id)}/detail`,
|
|
106
|
+
},
|
|
107
|
+
campus: {
|
|
108
|
+
referer: "https://careers.iqiyi.com/campus/",
|
|
109
|
+
listPage: "https://careers.iqiyi.com/campus/",
|
|
110
|
+
detailPage: (id) => `https://careers.iqiyi.com/campus/position/${encodeURIComponent(id)}/detail`,
|
|
111
|
+
},
|
|
112
|
+
intern: {
|
|
113
|
+
referer: "https://careers.iqiyi.com/intern",
|
|
114
|
+
listPage: "https://careers.iqiyi.com/intern",
|
|
115
|
+
detailPage: (id) => `https://careers.iqiyi.com/intern/position/${encodeURIComponent(id)}/detail`,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
const DEFAULT_HEADERS = {
|
|
119
|
+
"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",
|
|
120
|
+
Accept: "application/json, text/plain, */*",
|
|
121
|
+
"portal-platform": "pc",
|
|
122
|
+
};
|
|
123
|
+
async function callPost(path, body, portalPath) {
|
|
124
|
+
const url = `${API_ROOT}${path}`;
|
|
125
|
+
const portal = PORTAL[portalPath];
|
|
126
|
+
let response;
|
|
127
|
+
try {
|
|
128
|
+
response = await fetch(url, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
...DEFAULT_HEADERS,
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
"website-path": portalPath,
|
|
134
|
+
Referer: portal.referer,
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify(body),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
147
|
+
}
|
|
148
|
+
let payload;
|
|
149
|
+
try {
|
|
150
|
+
payload = (await response.json());
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: payload.code === 0,
|
|
157
|
+
data: payload.data,
|
|
158
|
+
message: payload.message || (payload.code === 0 ? "ok" : "upstream error"),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function callGet(path, portalPath) {
|
|
162
|
+
const url = `${API_ROOT}${path}`;
|
|
163
|
+
const portal = PORTAL[portalPath];
|
|
164
|
+
let response;
|
|
165
|
+
try {
|
|
166
|
+
response = await fetch(url, {
|
|
167
|
+
headers: {
|
|
168
|
+
...DEFAULT_HEADERS,
|
|
169
|
+
"website-path": portalPath,
|
|
170
|
+
Referer: portal.referer,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
182
|
+
}
|
|
183
|
+
let payload;
|
|
184
|
+
try {
|
|
185
|
+
payload = (await response.json());
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: payload.code === 0,
|
|
192
|
+
data: payload.data,
|
|
193
|
+
message: payload.message || (payload.code === 0 ? "ok" : "upstream error"),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function summarizePosition(item, portalPath) {
|
|
197
|
+
const id = String(item.id ?? "");
|
|
198
|
+
const portal = PORTAL[portalPath];
|
|
199
|
+
// Build work_cities: prefer city_list for multi-city, fall back to city_info
|
|
200
|
+
const cityList = item.city_list ?? [];
|
|
201
|
+
let work_cities;
|
|
202
|
+
if (cityList.length > 1) {
|
|
203
|
+
work_cities = cityList.map((c) => c.name ?? "").filter(Boolean).join(" / ");
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
work_cities = item.city_info?.name ?? (cityList[0]?.name ?? "");
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
post_id: id,
|
|
210
|
+
title: item.title ?? "",
|
|
211
|
+
project: item.job_function?.name ?? "",
|
|
212
|
+
recruit_label: item.recruit_type?.name ?? "",
|
|
213
|
+
bgs: "",
|
|
214
|
+
work_cities,
|
|
215
|
+
apply_url: id ? portal.detailPage(id) : portal.listPage,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ---------- searchPositions ----------
|
|
219
|
+
export async function searchPositions(opts = {}) {
|
|
220
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
221
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
222
|
+
const offset = (page - 1) * pageSize;
|
|
223
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
224
|
+
const portalPath = opts.portal ?? "job";
|
|
225
|
+
const asStringList = (v) => {
|
|
226
|
+
if (v === undefined)
|
|
227
|
+
return undefined;
|
|
228
|
+
const arr = Array.isArray(v) ? v : [v];
|
|
229
|
+
return arr.map(String);
|
|
230
|
+
};
|
|
231
|
+
const payload = {
|
|
232
|
+
keyword,
|
|
233
|
+
limit: pageSize,
|
|
234
|
+
offset,
|
|
235
|
+
portal_type: 3,
|
|
236
|
+
portal_entrance: 1,
|
|
237
|
+
language: "zh",
|
|
238
|
+
};
|
|
239
|
+
const jobFunctionIdList = asStringList(opts.jobFunctionIdList);
|
|
240
|
+
if (jobFunctionIdList?.length) {
|
|
241
|
+
payload.job_function_id_list = jobFunctionIdList;
|
|
242
|
+
}
|
|
243
|
+
const cityCodeList = asStringList(opts.cityCodeList);
|
|
244
|
+
if (cityCodeList?.length) {
|
|
245
|
+
payload.location_code_list = cityCodeList;
|
|
246
|
+
}
|
|
247
|
+
const recruitmentIdList = asStringList(opts.recruitmentIdList);
|
|
248
|
+
if (recruitmentIdList?.length) {
|
|
249
|
+
payload.recruitment_id_list = recruitmentIdList;
|
|
250
|
+
}
|
|
251
|
+
const response = await callPost("/search/job/posts", payload, portalPath);
|
|
252
|
+
if (!response.ok || !response.data) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
message: response.message,
|
|
256
|
+
source: "careers.iqiyi.com",
|
|
257
|
+
portal: portalPath,
|
|
258
|
+
query: payload,
|
|
259
|
+
positions: [],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const rows = response.data.job_post_list ?? [];
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
source: "careers.iqiyi.com",
|
|
266
|
+
portal: portalPath,
|
|
267
|
+
query: payload,
|
|
268
|
+
page,
|
|
269
|
+
page_size: pageSize,
|
|
270
|
+
total: response.data.count ?? rows.length,
|
|
271
|
+
positions: rows.map((r) => summarizePosition(r, portalPath)),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// ---------- fetchAllPositions ----------
|
|
275
|
+
export async function fetchAllPositions(opts = {}) {
|
|
276
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
|
|
277
|
+
const maxPages = Math.max(1, opts.maxPages ?? 5);
|
|
278
|
+
const portalPath = opts.portal ?? "job";
|
|
279
|
+
const bucket = [];
|
|
280
|
+
let total;
|
|
281
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
282
|
+
const result = await searchPositions({ ...opts, page, pageSize, portal: portalPath });
|
|
283
|
+
if (!result.ok) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
message: result.message,
|
|
287
|
+
source: "careers.iqiyi.com",
|
|
288
|
+
portal: portalPath,
|
|
289
|
+
fetched: bucket.length,
|
|
290
|
+
positions: bucket,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (total === undefined)
|
|
294
|
+
total = result.total;
|
|
295
|
+
if (!result.positions.length)
|
|
296
|
+
break;
|
|
297
|
+
bucket.push(...result.positions);
|
|
298
|
+
if (total !== undefined && bucket.length >= total)
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
source: "careers.iqiyi.com",
|
|
304
|
+
portal: portalPath,
|
|
305
|
+
total: total ?? bucket.length,
|
|
306
|
+
fetched: bucket.length,
|
|
307
|
+
positions: bucket,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// ---------- fetchPositionDetail ----------
|
|
311
|
+
export async function fetchPositionDetail(postId, portal = "job") {
|
|
312
|
+
const id = (postId ?? "").trim();
|
|
313
|
+
if (!id) {
|
|
314
|
+
return { ok: false, source: "careers.iqiyi.com", message: "post_id is required" };
|
|
315
|
+
}
|
|
316
|
+
const response = await callGet(`/job/posts/${encodeURIComponent(id)}`, portal);
|
|
317
|
+
if (!response.ok || !response.data) {
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
source: "careers.iqiyi.com",
|
|
321
|
+
post_id: id,
|
|
322
|
+
message: response.message || "no detail returned",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const raw = response.data.job_post_detail;
|
|
326
|
+
if (!raw) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
source: "careers.iqiyi.com",
|
|
330
|
+
post_id: id,
|
|
331
|
+
message: "job_post_detail missing in response",
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const cityList = raw.city_list ?? raw.city_info_list_for_delivery ?? [];
|
|
335
|
+
const work_cities = cityList.length
|
|
336
|
+
? cityList.map((c) => c.name ?? "").filter(Boolean).join(" / ")
|
|
337
|
+
: (raw.city_info?.name ?? "");
|
|
338
|
+
const summary = summarizePosition(raw, portal);
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
source: "careers.iqiyi.com",
|
|
342
|
+
portal,
|
|
343
|
+
post_id: String(raw.id ?? id),
|
|
344
|
+
title: raw.title ?? "",
|
|
345
|
+
direction: raw.sub_title ?? "",
|
|
346
|
+
project: raw.job_function?.name ?? "",
|
|
347
|
+
recruit_label: raw.recruit_type?.name ?? "",
|
|
348
|
+
description: raw.description ?? "",
|
|
349
|
+
requirements: raw.requirement ?? "",
|
|
350
|
+
work_cities,
|
|
351
|
+
apply_url: summary.apply_url,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
// ---------- fetchDictionaries ----------
|
|
355
|
+
export async function fetchDictionaries(portal = "job") {
|
|
356
|
+
const response = await callGet(`/config/job/filters/${portal}`, portal);
|
|
357
|
+
if (!response.ok || !response.data) {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
source: "careers.iqiyi.com",
|
|
361
|
+
portal,
|
|
362
|
+
message: response.message,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const d = response.data;
|
|
366
|
+
const jobFunctions = (d.job_function_list ?? []).map((f) => ({
|
|
367
|
+
id: f.id ?? "",
|
|
368
|
+
name: f.name ?? "",
|
|
369
|
+
parent_id: f.parent?.id ?? null,
|
|
370
|
+
children: (f.children ?? []).map((c) => ({
|
|
371
|
+
id: c.id ?? "",
|
|
372
|
+
name: c.name ?? "",
|
|
373
|
+
})),
|
|
374
|
+
}));
|
|
375
|
+
const cities = (d.city_list ?? []).map((c) => ({
|
|
376
|
+
code: c.code ?? "",
|
|
377
|
+
name: c.name ?? "",
|
|
378
|
+
en_name: c.en_name ?? "",
|
|
379
|
+
}));
|
|
380
|
+
const recruitmentTypes = (d.recruitment_type_list ?? []).map((r) => ({
|
|
381
|
+
id: r.id ?? "",
|
|
382
|
+
name: r.name ?? "",
|
|
383
|
+
en_name: r.en_name ?? "",
|
|
384
|
+
parent_id: r.parent?.id ?? null,
|
|
385
|
+
}));
|
|
386
|
+
return {
|
|
387
|
+
ok: true,
|
|
388
|
+
source: "careers.iqiyi.com",
|
|
389
|
+
portal,
|
|
390
|
+
verified_at: new Date().toISOString(),
|
|
391
|
+
jobFunctions,
|
|
392
|
+
cities,
|
|
393
|
+
recruitmentTypes,
|
|
394
|
+
portals: [
|
|
395
|
+
{ path: "job", name: "社招官网", note: "experienced hire (~82 posts)" },
|
|
396
|
+
{ path: "campus", name: "应届生校招官网", note: "campus / new-grad (~14 posts)" },
|
|
397
|
+
{ path: "intern", name: "实习生官网", note: "intern (~92 posts)" },
|
|
398
|
+
],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
// ---------- stub notices ----------
|
|
402
|
+
// careers.iqiyi.com has no public structured notices/announcements endpoint.
|
|
403
|
+
const STUB_SOURCE = "careers.iqiyi.com";
|
|
404
|
+
const STUB_MSG = "iQIYI: no public notices endpoint on careers.iqiyi.com";
|
|
405
|
+
export async function listNotices() {
|
|
406
|
+
return { ok: false, source: STUB_SOURCE, message: STUB_MSG };
|
|
407
|
+
}
|
|
408
|
+
export async function getNotice(_id) {
|
|
409
|
+
return { ok: false, source: STUB_SOURCE, message: STUB_MSG };
|
|
410
|
+
}
|
|
411
|
+
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
412
|
+
return { ok: false, source: STUB_SOURCE, message: STUB_MSG };
|
|
413
|
+
}
|
|
414
|
+
// ---------- matchResume ----------
|
|
415
|
+
export async function matchResume(text, opts = {}) {
|
|
416
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
417
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
418
|
+
const portal = opts.portal ?? "job";
|
|
419
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
420
|
+
if (!terms.length) {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
source: STUB_SOURCE,
|
|
424
|
+
message: "could not extract any technical signals from the text",
|
|
425
|
+
preview: (text ?? "").slice(0, 120),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
429
|
+
const list = await searchPositions({ keyword, pageSize: 100, portal });
|
|
430
|
+
if (!list.ok) {
|
|
431
|
+
return { ok: false, source: STUB_SOURCE, message: list.message, positions: [] };
|
|
432
|
+
}
|
|
433
|
+
// Broaden pool if keyword search returns few results
|
|
434
|
+
let allPositions = list.positions;
|
|
435
|
+
if (allPositions.length < candidates) {
|
|
436
|
+
const broad = await searchPositions({ pageSize: 100, portal });
|
|
437
|
+
if (broad.ok) {
|
|
438
|
+
const seen = new Set(allPositions.map((p) => p.post_id));
|
|
439
|
+
for (const p of broad.positions) {
|
|
440
|
+
if (!seen.has(p.post_id)) {
|
|
441
|
+
allPositions = [...allPositions, p];
|
|
442
|
+
seen.add(p.post_id);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const scored = [];
|
|
448
|
+
for (const p of allPositions) {
|
|
449
|
+
const blob = [p.title, p.project, p.recruit_label, p.work_cities].join(" ");
|
|
450
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
451
|
+
if (score > 0) {
|
|
452
|
+
scored.push({ score, position: p, reasons });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
scored.sort((a, b) => b.score - a.score);
|
|
456
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
457
|
+
if (!shortlist.length) {
|
|
458
|
+
shortlist = allPositions.slice(0, candidates).map((position) => ({
|
|
459
|
+
score: 0,
|
|
460
|
+
position,
|
|
461
|
+
reasons: [],
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
465
|
+
const mr = s.reasons.length > 0
|
|
466
|
+
? s.reasons.slice(0, 5)
|
|
467
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
468
|
+
return {
|
|
469
|
+
...s.position,
|
|
470
|
+
description: s.description,
|
|
471
|
+
requirements: s.requirements,
|
|
472
|
+
match_reasons: mr,
|
|
473
|
+
};
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
ok: true,
|
|
477
|
+
source: STUB_SOURCE,
|
|
478
|
+
portal,
|
|
479
|
+
extracted_terms: terms,
|
|
480
|
+
city_preferences: cities,
|
|
481
|
+
matches,
|
|
482
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
483
|
+
"The only authority on selection is HR.",
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
487
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
488
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
489
|
+
host: "careers.iqiyi.com",
|
|
490
|
+
source: "careers.iqiyi.com",
|
|
491
|
+
channel: "campus",
|
|
492
|
+
applyUrlPrefix: "https://careers.iqiyi.com/campus/position",
|
|
493
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
494
|
+
});
|