@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/netease.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// Thin client for NetEase's (网易) public recruiting API at hr.163.com.
|
|
2
|
+
//
|
|
3
|
+
// Both campus-recruiting (校园/实习) and social-hire (社招) positions are served
|
|
4
|
+
// from the same host. This adapter targets campus + intern postings (workType "1").
|
|
5
|
+
//
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Endpoint inventory (probed 2026-05, commons.288fd140.chunk.js):
|
|
8
|
+
//
|
|
9
|
+
// POST https://hr.163.com/api/hr163/position/queryPage
|
|
10
|
+
// Payload: { pageNum, pageSize, workType, keyword, ... }
|
|
11
|
+
// Response: { code:200, data:{ total, pages, list:[...], lastPage } }
|
|
12
|
+
// Verified fields in payload:
|
|
13
|
+
// workType "0"=社招 (social hire) "1"=校园/实习 (campus+intern)
|
|
14
|
+
// keyword free-text search — the only filter that actually narrows results
|
|
15
|
+
// pageNum accepted but IGNORED — server always returns same top N records
|
|
16
|
+
// pageSize works; max=200 (code 402 if exceeded)
|
|
17
|
+
// All other filter params (positionTypeCode, firstPostTypeCode,
|
|
18
|
+
// workPlaceId, workPlaceList, etc.) are accepted with 200 but have NO
|
|
19
|
+
// effect on the result set without an authenticated session cookie.
|
|
20
|
+
//
|
|
21
|
+
// GET https://hr.163.com/api/hr163/position/query?id=<id>
|
|
22
|
+
// Returns full JD fields for one position ID.
|
|
23
|
+
// No auth required; same shape as list items plus description/requirement.
|
|
24
|
+
//
|
|
25
|
+
// GET https://hr.163.com/api/hr163/options/positionType/queryItemList
|
|
26
|
+
// Returns the positionType dictionary (职位类别).
|
|
27
|
+
// id/name pairs — see DIMENSION 1 below.
|
|
28
|
+
//
|
|
29
|
+
// GET https://hr.163.com/api/hr163/position/queryPositionMetric
|
|
30
|
+
// Returns aggregate counts: positionCount, cityCount, firstDepartmentCount.
|
|
31
|
+
//
|
|
32
|
+
// GET https://campus.163.com/api/campuspc/position/getJobList [NOTE: auth-gated]
|
|
33
|
+
// The campus.163.com SPA (校园招聘) exposes a dedicated campus portal with
|
|
34
|
+
// BU/city/positionType filters — params: workPlaceId, positionType, firstBuId,
|
|
35
|
+
// keyword, pageNum, pageSize (GET with query params, axios passes as params).
|
|
36
|
+
// However the endpoint returns code:406 "当前用户未登录" for all filter dictionary
|
|
37
|
+
// endpoints, and getJobList returns total:0 for unauthenticated requests.
|
|
38
|
+
// ▶ Not usable without credentials; we fall back to hr.163.com.
|
|
39
|
+
//
|
|
40
|
+
// ============================================================
|
|
41
|
+
// Pagination caveat:
|
|
42
|
+
// pageNum is sent but IGNORED by the server without auth.
|
|
43
|
+
// The API returns the top N (up to pageSize ≤ 200) positions sorted by relevance.
|
|
44
|
+
// fetchAllPositions() transparently makes multiple keyword-scoped calls when
|
|
45
|
+
// the caller requests many pages, but because the underlying sort is fixed, pages
|
|
46
|
+
// beyond the first will repeat the same records. We document this honestly.
|
|
47
|
+
//
|
|
48
|
+
// ============================================================
|
|
49
|
+
// DIMENSION 1 — positionType codes (GET /options/positionType/queryItemList):
|
|
50
|
+
// 01=技术 02=游戏策划 03=游戏程序 04=游戏艺术 05=游戏测试
|
|
51
|
+
// 06=产品 07=人工智能 08=运营 11=用户体验及设计 12=项目管理
|
|
52
|
+
// 16=市场渠道 21=销售 26=内容 31=客服 41=电商 51=职能支持
|
|
53
|
+
// 56=高管 57=教育 58=企业服务 00,99=其他
|
|
54
|
+
//
|
|
55
|
+
// DIMENSION 2 — workType:
|
|
56
|
+
// "0" = 社招 (social/experienced hire) ~1952 positions
|
|
57
|
+
// "1" = 校园/实习 (campus new-grad + intern) ~417 positions
|
|
58
|
+
//
|
|
59
|
+
// DIMENSION 3 — workPlaceList city codes (observed in list responses):
|
|
60
|
+
// 1=北京 2=上海 138=广州 229=杭州
|
|
61
|
+
// (NOTE: server ignores this filter without auth — keyword is the only filter)
|
|
62
|
+
//
|
|
63
|
+
// DIMENSION 4 — product/firstDep groupings observed in campus data:
|
|
64
|
+
// P008=网易游戏(雷火) P041=网易游戏(互娱) P001=网易严选
|
|
65
|
+
// firstDepName examples: 雷火事业群 / 音乐事业部 / 有道事业群 / 伏羲机器人 / 网易伏羲 / 严选事业部
|
|
66
|
+
//
|
|
67
|
+
// ============================================================
|
|
68
|
+
// ---- PositionSummary field mapping (NetEase → canonical) ----
|
|
69
|
+
// post_id ← item.id (stringified)
|
|
70
|
+
// title ← item.name
|
|
71
|
+
// project ← item.firstPostTypeName (职位类别, e.g. "游戏程序" / "技术" / "人工智能")
|
|
72
|
+
// recruit_label ← item.workType === "1" ? "校园/实习" : "社招" (API has no sub-label)
|
|
73
|
+
// bgs ← item.firstDepName (一级部门/事业群, closest to BG)
|
|
74
|
+
// work_cities ← item.workPlaceNameList joined with " / "
|
|
75
|
+
// apply_url ← https://hr.163.com/job-detail?id=${id}
|
|
76
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
77
|
+
export { checkResume };
|
|
78
|
+
const API_ROOT = "https://hr.163.com/api/hr163";
|
|
79
|
+
const CAMPUS_PAGE = "https://hr.163.com/job-list?workType=1";
|
|
80
|
+
const DETAIL_PAGE = (id) => `https://hr.163.com/job-detail?id=${encodeURIComponent(id)}`;
|
|
81
|
+
const SOURCE = "hr.163.com";
|
|
82
|
+
const DEFAULT_HEADERS = {
|
|
83
|
+
"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",
|
|
84
|
+
Accept: "application/json, text/plain, */*",
|
|
85
|
+
Referer: "https://hr.163.com/",
|
|
86
|
+
};
|
|
87
|
+
// ---------- low-level helpers ----------
|
|
88
|
+
async function get(path) {
|
|
89
|
+
const url = `${API_ROOT}${path}`;
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
response = await fetch(url, { headers: DEFAULT_HEADERS });
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
102
|
+
}
|
|
103
|
+
let payload;
|
|
104
|
+
try {
|
|
105
|
+
payload = (await response.json());
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ok: payload.code === 200,
|
|
112
|
+
data: payload.data ?? undefined,
|
|
113
|
+
message: payload.msg ?? (payload.code === 200 ? "ok" : "upstream error"),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function post(path, body) {
|
|
117
|
+
const url = `${API_ROOT}${path}`;
|
|
118
|
+
let response;
|
|
119
|
+
try {
|
|
120
|
+
response = await fetch(url, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { ...DEFAULT_HEADERS, "Content-Type": "application/json" },
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
134
|
+
}
|
|
135
|
+
let payload;
|
|
136
|
+
try {
|
|
137
|
+
payload = (await response.json());
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
ok: payload.code === 200,
|
|
144
|
+
data: payload.data ?? undefined,
|
|
145
|
+
message: payload.msg ?? (payload.code === 200 ? "ok" : "upstream error"),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function summarizePosition(item) {
|
|
149
|
+
const id = String(item.id ?? "");
|
|
150
|
+
const workCities = (item.workPlaceNameList ?? [])
|
|
151
|
+
.map((c) => c.trim())
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join(" / ");
|
|
154
|
+
return {
|
|
155
|
+
post_id: id,
|
|
156
|
+
title: item.name ?? "",
|
|
157
|
+
project: item.firstPostTypeName ?? "",
|
|
158
|
+
recruit_label: item.workType === "1" ? "校园/实习" : "社招",
|
|
159
|
+
bgs: item.firstDepName ?? "",
|
|
160
|
+
work_cities: workCities,
|
|
161
|
+
apply_url: id ? DETAIL_PAGE(id) : CAMPUS_PAGE,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ---------- searchPositions ----------
|
|
165
|
+
export async function searchPositions(opts = {}) {
|
|
166
|
+
const pageSize = Math.max(1, Math.min(200, opts.pageSize ?? 20));
|
|
167
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
168
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 80);
|
|
169
|
+
const workType = opts.workType ?? "1";
|
|
170
|
+
const payload = {
|
|
171
|
+
pageNum: page,
|
|
172
|
+
pageSize,
|
|
173
|
+
workType,
|
|
174
|
+
keyword,
|
|
175
|
+
};
|
|
176
|
+
const response = await post("/position/queryPage", payload);
|
|
177
|
+
if (!response.ok || !response.data) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
source: SOURCE,
|
|
181
|
+
message: response.message,
|
|
182
|
+
query: payload,
|
|
183
|
+
positions: [],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const rows = response.data.list ?? [];
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
source: SOURCE,
|
|
190
|
+
query: payload,
|
|
191
|
+
page,
|
|
192
|
+
page_size: pageSize,
|
|
193
|
+
total: response.data.total ?? rows.length,
|
|
194
|
+
positions: rows.map(summarizePosition),
|
|
195
|
+
note: "pageNum is ignored by the server without auth; results are always top-N by relevance. " +
|
|
196
|
+
"Use `keyword` to narrow the result set.",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// ---------- fetchAllPositions ----------
|
|
200
|
+
// Because pageNum is ignored, we cannot truly paginate.
|
|
201
|
+
// We fetch the maximum allowed pageSize=200 in one call and return it.
|
|
202
|
+
// When keyword is provided, narrow set may fit in one call.
|
|
203
|
+
export async function fetchAllPositions(opts = {}) {
|
|
204
|
+
const pageSize = 200; // server max
|
|
205
|
+
const workType = opts.workType ?? "1";
|
|
206
|
+
const keyword = (opts.keyword ?? "").trim();
|
|
207
|
+
const result = await searchPositions({ keyword, pageSize, workType, page: 1 });
|
|
208
|
+
if (!result.ok) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
source: SOURCE,
|
|
212
|
+
message: result.message,
|
|
213
|
+
fetched: 0,
|
|
214
|
+
positions: [],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
ok: true,
|
|
219
|
+
source: SOURCE,
|
|
220
|
+
total: result.total,
|
|
221
|
+
fetched: result.positions.length,
|
|
222
|
+
positions: result.positions,
|
|
223
|
+
note: "fetchAllPositions returns up to 200 positions in a single call (server max). " +
|
|
224
|
+
"True multi-page iteration is not available without authentication. " +
|
|
225
|
+
`Reported total: ${result.total}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ---------- fetchPositionDetail ----------
|
|
229
|
+
export async function fetchPositionDetail(postId) {
|
|
230
|
+
const id = (postId ?? "").trim();
|
|
231
|
+
if (!id) {
|
|
232
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
233
|
+
}
|
|
234
|
+
const response = await get(`/position/query?id=${encodeURIComponent(id)}`);
|
|
235
|
+
if (!response.ok || !response.data) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
source: SOURCE,
|
|
239
|
+
post_id: id,
|
|
240
|
+
message: response.message || "no detail returned",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const raw = response.data;
|
|
244
|
+
const workCities = (raw.workPlaceNameList ?? []).map((c) => c.trim()).filter(Boolean);
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
source: SOURCE,
|
|
248
|
+
post_id: String(raw.id ?? id),
|
|
249
|
+
title: raw.name ?? "",
|
|
250
|
+
project: raw.firstPostTypeName ?? "",
|
|
251
|
+
recruit_label: raw.workType === "1" ? "校园/实习" : "社招",
|
|
252
|
+
bgs: raw.firstDepName ?? "",
|
|
253
|
+
product: raw.productName ?? raw.product ?? "",
|
|
254
|
+
req_education: raw.reqEducationName ?? "",
|
|
255
|
+
req_work_years: raw.reqWorkYearsName ?? "",
|
|
256
|
+
description: raw.description ?? "",
|
|
257
|
+
requirements: raw.requirement ?? "",
|
|
258
|
+
work_cities: workCities,
|
|
259
|
+
recruit_cities: workCities, // API does not separate work city from interview city
|
|
260
|
+
apply_url: DETAIL_PAGE(String(raw.id ?? id)),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export async function fetchDictionaries() {
|
|
264
|
+
const response = await get("/options/positionType/queryItemList");
|
|
265
|
+
const positionTypes = response.ok
|
|
266
|
+
? (response.data ?? []).map((item) => ({
|
|
267
|
+
id: item.id ?? "",
|
|
268
|
+
name: item.name ?? "",
|
|
269
|
+
}))
|
|
270
|
+
: [];
|
|
271
|
+
// Static known city codes (observed in campus responses 2026-05)
|
|
272
|
+
const cities = [
|
|
273
|
+
{ code: 1, name: "北京市" },
|
|
274
|
+
{ code: 2, name: "上海市" },
|
|
275
|
+
{ code: 138, name: "广州市" },
|
|
276
|
+
{ code: 229, name: "杭州市" },
|
|
277
|
+
];
|
|
278
|
+
// Static workType values
|
|
279
|
+
const workTypes = [
|
|
280
|
+
{ value: "1", label: "校园/实习", note: "campus new-grad + intern (~417 posts)" },
|
|
281
|
+
{ value: "0", label: "社招", note: "social/experienced hire (~1952 posts)" },
|
|
282
|
+
];
|
|
283
|
+
return {
|
|
284
|
+
ok: response.ok,
|
|
285
|
+
source: SOURCE,
|
|
286
|
+
verified_at: new Date().toISOString(),
|
|
287
|
+
campus_only: false,
|
|
288
|
+
note: "City and BU dictionaries are static (derived from observed data 2026-05). " +
|
|
289
|
+
"However, city/BU filters are NOT effective without authentication — " +
|
|
290
|
+
"only `keyword` actually narrows results in unauthenticated calls.",
|
|
291
|
+
positionTypes,
|
|
292
|
+
cities,
|
|
293
|
+
workTypes,
|
|
294
|
+
message: response.ok ? "ok" : response.message,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// ---------- notices (stub) ----------
|
|
298
|
+
// hr.163.com has no public announcement/notice endpoint.
|
|
299
|
+
const STUB_MSG = "NetEase: no public notices endpoint on hr.163.com";
|
|
300
|
+
export async function listNotices() {
|
|
301
|
+
return { ok: false, source: SOURCE, message: STUB_MSG };
|
|
302
|
+
}
|
|
303
|
+
export async function getNotice(_id) {
|
|
304
|
+
return { ok: false, source: SOURCE, message: STUB_MSG };
|
|
305
|
+
}
|
|
306
|
+
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
307
|
+
return { ok: false, source: SOURCE, message: STUB_MSG };
|
|
308
|
+
}
|
|
309
|
+
// ---------- matchResume ----------
|
|
310
|
+
// Mirror bytedance/tencent algorithm:
|
|
311
|
+
// 1. Extract signals from resume text.
|
|
312
|
+
// 2. Search with top-3 terms as keyword (the only working filter).
|
|
313
|
+
// 3. Score each post against title + project + bgs + work_cities + description + requirement.
|
|
314
|
+
// 4. Return top N matches with reasons.
|
|
315
|
+
export async function matchResume(text, opts = {}) {
|
|
316
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
317
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
318
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
319
|
+
if (!terms.length) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
source: SOURCE,
|
|
323
|
+
message: "could not extract any technical signals from the text",
|
|
324
|
+
preview: (text ?? "").slice(0, 120),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
328
|
+
const listResult = await searchPositions({ keyword, pageSize: 100, workType: "1" });
|
|
329
|
+
if (!listResult.ok) {
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
source: SOURCE,
|
|
333
|
+
message: listResult.message,
|
|
334
|
+
positions: [],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const scored = [];
|
|
338
|
+
const shortlist = listResult.positions.slice(0, candidates);
|
|
339
|
+
for (const p of shortlist) {
|
|
340
|
+
// Quick score from summary fields first
|
|
341
|
+
const summaryBlob = [p.title, p.project, p.bgs, p.work_cities, p.recruit_label].join(" ");
|
|
342
|
+
const { score: quickScore, reasons: quickReasons } = scoreOverlap(summaryBlob, terms, cities);
|
|
343
|
+
// Fetch detail for JD text
|
|
344
|
+
const detail = await fetchPositionDetail(p.post_id);
|
|
345
|
+
let description;
|
|
346
|
+
let requirements;
|
|
347
|
+
let extraScore = 0;
|
|
348
|
+
let extraReasons = [];
|
|
349
|
+
if (detail.ok) {
|
|
350
|
+
description = detail.description;
|
|
351
|
+
requirements = detail.requirements;
|
|
352
|
+
const jdBlob = [detail.description, detail.requirements].join(" ");
|
|
353
|
+
const extra = scoreOverlap(jdBlob, terms, cities);
|
|
354
|
+
extraScore = extra.score;
|
|
355
|
+
extraReasons = extra.reasons;
|
|
356
|
+
}
|
|
357
|
+
const totalScore = quickScore + extraScore;
|
|
358
|
+
const allReasons = [...new Set([...quickReasons, ...extraReasons])].slice(0, 5);
|
|
359
|
+
if (totalScore > 0 || scored.length < topN) {
|
|
360
|
+
scored.push({ score: totalScore, position: p, reasons: allReasons, description, requirements });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
scored.sort((a, b) => b.score - a.score);
|
|
364
|
+
let finalList = scored.slice(0, topN);
|
|
365
|
+
if (!finalList.length) {
|
|
366
|
+
// Fall back: return first topN from list without enrichment
|
|
367
|
+
finalList = listResult.positions.slice(0, topN).map((position) => ({
|
|
368
|
+
score: 0,
|
|
369
|
+
position,
|
|
370
|
+
reasons: [],
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
const matches = finalList.map((s) => {
|
|
374
|
+
const mr = s.reasons.length > 0
|
|
375
|
+
? s.reasons
|
|
376
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
377
|
+
return {
|
|
378
|
+
...s.position,
|
|
379
|
+
description: s.description,
|
|
380
|
+
requirements: s.requirements,
|
|
381
|
+
match_reasons: mr,
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
source: SOURCE,
|
|
387
|
+
extracted_terms: terms,
|
|
388
|
+
city_preferences: cities,
|
|
389
|
+
matches,
|
|
390
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
391
|
+
"The only authority on selection is HR.",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
import { buildBespokeApplySchema as _buildBespokeApplySchema_netease } from "./apply.js";
|
|
395
|
+
export async function fetchApplicationSchema(postId) {
|
|
396
|
+
const id = (postId ?? "").trim();
|
|
397
|
+
if (!id)
|
|
398
|
+
return { ok: false, source: "hr.163.com", message: "post_id is required" };
|
|
399
|
+
let title = "";
|
|
400
|
+
let applyUrl = "https://hr.163.com";
|
|
401
|
+
try {
|
|
402
|
+
const detail = (await fetchPositionDetail(id));
|
|
403
|
+
if (detail?.ok === false) {
|
|
404
|
+
return { ok: false, source: "hr.163.com", message: detail.message ?? "post not found" };
|
|
405
|
+
}
|
|
406
|
+
title = detail?.title ?? "";
|
|
407
|
+
if (detail?.apply_url)
|
|
408
|
+
applyUrl = detail.apply_url;
|
|
409
|
+
}
|
|
410
|
+
catch { }
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
schema: _buildBespokeApplySchema_netease({
|
|
414
|
+
source: "hr.163.com",
|
|
415
|
+
postId: id,
|
|
416
|
+
jobTitle: title,
|
|
417
|
+
applyUrl,
|
|
418
|
+
submitEndpoint: "https://hr.163.com/post-app/apply.do",
|
|
419
|
+
submitKind: "multipart-session",
|
|
420
|
+
endpointVerified: true,
|
|
421
|
+
submitNotes: "NetEase — POST /post-app/apply.do with session cookie. Endpoint anon-probed → HTTP 405 (Nginx routing table has this .do path; the servlet container rejects the request due to wrong Content-Type / missing form fields, not 404). Body shape still needs validation against a real candidate session.",
|
|
422
|
+
}),
|
|
423
|
+
};
|
|
424
|
+
}
|
package/dist/nio.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Thin adapter for NIO / 蔚来 campus recruiting via Feishu Recruiting (ATSX).
|
|
2
|
+
//
|
|
3
|
+
// NIO self-hosts the Feishu Recruiting platform at:
|
|
4
|
+
// https://nio.jobs.feishu.cn/
|
|
5
|
+
//
|
|
6
|
+
// API (probed 2026-05):
|
|
7
|
+
// POST https://nio.jobs.feishu.cn/api/v1/search/job/posts
|
|
8
|
+
// Headers: portal-channel: campus, portal-platform: pc, website-path: campus
|
|
9
|
+
// Total: ~771 posts (正式/new-grad only; internship channel returns "site not exist")
|
|
10
|
+
// GET https://nio.jobs.feishu.cn/api/v1/config/job/filters/campus
|
|
11
|
+
//
|
|
12
|
+
// Field notes:
|
|
13
|
+
// - job_category is null; project ← job_function.name
|
|
14
|
+
// - city_info is null; work_cities ← city_list
|
|
15
|
+
// - No internship channel (returns code -9000003 "site not exist")
|
|
16
|
+
//
|
|
17
|
+
// apply_url pattern: https://nio.jobs.feishu.cn/campus/position/<id>/detail
|
|
18
|
+
import { createAdapter } from "./feishu.js";
|
|
19
|
+
export const { searchPositions, fetchAllPositions, fetchPositionDetail, fetchDictionaries, listNotices, getNotice, findNoticesByQuestion, matchResume, checkResume, fetchApplicationSchema, } = createAdapter({
|
|
20
|
+
host: "nio.jobs.feishu.cn",
|
|
21
|
+
channel: "campus",
|
|
22
|
+
label: "NIO / 蔚来",
|
|
23
|
+
applyUrlPrefix: "https://nio.jobs.feishu.cn/campus/position",
|
|
24
|
+
});
|