job-pro 0.7.1 → 0.7.3
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/antgroup.js +318 -157
- package/dist/cambricon.js +27 -59
- package/dist/geely.js +29 -57
- package/dist/horizonrobotics.js +43 -92
- package/dist/index.js +1 -1
- package/dist/moka.js +412 -0
- package/dist/sensetime.js +47 -183
- package/dist/wecruit.js +385 -0
- package/package.json +3 -2
package/dist/antgroup.js
CHANGED
|
@@ -1,201 +1,362 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 蚂蚁集团 (Ant Group) careers adapter for `job-pro`.
|
|
2
2
|
//
|
|
3
3
|
// ============================================================
|
|
4
|
-
// API
|
|
4
|
+
// API DISCOVERY (probed 2026-05-16 via puppeteer-core network capture)
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// JS bundles: gw.alipayobjects.com/render/p/yuyan/180020010001257966/umi.6f081e74.js
|
|
9
|
-
// render.alipay.com/p/yuyan/180020010001257966/p__CampusRecruitment__CRList__index.*.async.js
|
|
10
|
-
// render.alipay.com/p/yuyan/180020010001257966/p__CampusRecruitment__CRFullList__index.*.async.js
|
|
11
|
-
// Gateway host: talent.antgroup.com (Spanner CDN/WAF, Alipay's proprietary gateway)
|
|
12
|
-
// Backend host: antwork-prod.antgroup-inc.cn (actual API server)
|
|
6
|
+
// `talent.antgroup.com` is an Ant Bigfish SPA. Its public-facing job feed
|
|
7
|
+
// is served by `hrcareersweb.antgroup.com` with two anonymous endpoints:
|
|
13
8
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// POST /api/campus/position/search — paginated job search
|
|
18
|
-
// POST /api/campus/position/detail — single position detail
|
|
19
|
-
// POST /api/campus/position/queryDept — dept tree for a position group
|
|
20
|
-
// POST /api/campus/positionGroup/queryBatchConfig — batch config
|
|
21
|
-
// POST /api/campus/positionGroup/queryBatchDetailById — batch detail
|
|
22
|
-
// POST /api/searchCondition/list — filter taxonomy (categories, cities, depts)
|
|
23
|
-
// POST /api/searchCondition/listPositionGroup
|
|
24
|
-
// POST /api/searchCondition/listTalentPlan
|
|
25
|
-
//
|
|
26
|
-
// Canonical position detail URL: /campus-position?positionId=<id>
|
|
27
|
-
//
|
|
28
|
-
// ============================================================
|
|
29
|
-
// AUTH STATUS — GATED (Alipay OAuth / buservice SDK):
|
|
30
|
-
//
|
|
31
|
-
// EVERY endpoint (including /api/campus/position/search and
|
|
32
|
-
// /api/searchCondition/list) requires an authenticated Alipay/Ant Group
|
|
33
|
-
// session. Without login, the backend returns:
|
|
34
|
-
//
|
|
35
|
-
// { "buserviceErrorCode": "USER_NOT_LOGIN",
|
|
36
|
-
// "buserviceErrorMsg": "https://pubbuservice.alipay.com/…" }
|
|
37
|
-
//
|
|
38
|
-
// The buservice middleware intercepts ALL routes as a catch-all auth gate
|
|
39
|
-
// before any controller logic runs. There is no guest/anonymous tier.
|
|
40
|
-
//
|
|
41
|
-
// The talent.antgroup.com Spanner gateway additionally returns 405 Method
|
|
42
|
-
// Not Allowed for POST requests that lack valid Alipay session cookies,
|
|
43
|
-
// preventing even the USER_NOT_LOGIN response from being seen in most cases.
|
|
44
|
-
// Direct calls to antwork-prod.antgroup-inc.cn reveal the auth error clearly.
|
|
9
|
+
// POST /api/campus/position/search — 467 校招 / 实习 positions
|
|
10
|
+
// POST /api/social/position/search — 922 社招 positions
|
|
45
11
|
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// ALIPAYJSESSIONID=<token>; domain=.antgroup.com
|
|
51
|
-
// _CHIPS-ALIPAYJSESSIONID=<same_token>; samesite=none; partitioned
|
|
52
|
-
// spanner=<signed_value>; path=/; secure
|
|
53
|
-
//
|
|
54
|
-
// These cookies are required for CORS (Access-Control-Allow-Credentials: true)
|
|
55
|
-
// but the buservice SDK then validates the session against Alipay's auth
|
|
56
|
-
// infrastructure — a simple GET-derived cookie has no authenticated user.
|
|
57
|
-
// Unlike Alibaba's portal (campus-talent.alibaba.com) which only needs an
|
|
58
|
-
// XSRF-TOKEN for public search, Ant Group's portal requires full Alipay OAuth.
|
|
59
|
-
//
|
|
60
|
-
// ============================================================
|
|
61
|
-
// Ant Group vs Alibaba — KEY DIFFERENCES:
|
|
12
|
+
// Both accept JSON `{ key, pageIndex, pageSize, channel?, language, … }`
|
|
13
|
+
// and return:
|
|
14
|
+
// { success:true, errorMsg:"成功", content:[…RawPosition], totalCount,
|
|
15
|
+
// pageSize, currentPage }
|
|
62
16
|
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// Backend host: antwork-prod.antgroup-inc.cn vs campus-talent.alibaba.com
|
|
67
|
-
// Auth MW: buservice SDK (blocks all) vs Spring XSRF (only mutating ops)
|
|
17
|
+
// The `channel` field is required only on the social endpoint
|
|
18
|
+
// (`"group_official_site"`). The `ctoken=…` query parameter that the
|
|
19
|
+
// browser SPA appends is NOT required for unauthenticated reads.
|
|
68
20
|
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// searchCondition/list returns: searchItems with types "workCity", "category", "dept", "recruitType"
|
|
73
|
-
// Position fields: id, categoryName, workLocations, graduationTime, circleNames (BU)
|
|
74
|
-
//
|
|
75
|
-
// ============================================================
|
|
76
|
-
// ---- PositionSummary field mapping (Ant Group → canonical) ----
|
|
77
|
-
// post_id ← item.id (stringified)
|
|
78
|
-
// title ← item.name
|
|
79
|
-
// project ← item.categoryName ?? "" (e.g. "技术类", "产品类")
|
|
80
|
-
// recruit_label ← item.recruitType ?? "" (e.g. "实习生", "校招生")
|
|
81
|
-
// bgs ← item.circleNames?.[0] ?? "" (BU / business unit)
|
|
82
|
-
// work_cities ← item.workLocations?.join(" / ") ?? ""
|
|
83
|
-
// apply_url ← https://talent.antgroup.com/campus-position?positionId=<id>
|
|
21
|
+
// queryCollections / favoritePosition / login-required endpoints return
|
|
22
|
+
// `errorCode:"LOGIN_EXPIRED"` for anonymous callers — those are user
|
|
23
|
+
// dashboard surfaces, not the public search.
|
|
84
24
|
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
85
|
-
export {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
25
|
+
export { checkResume };
|
|
26
|
+
const SOURCE = "hrcareersweb.antgroup.com";
|
|
27
|
+
const API_ROOT = "https://hrcareersweb.antgroup.com/api";
|
|
28
|
+
const CAMPUS_PAGE = "https://talent.antgroup.com/campus-list";
|
|
29
|
+
const SOCIAL_PAGE = "https://talent.antgroup.com/off-campus-position";
|
|
30
|
+
const DETAIL_URL = (recruitType, id) => recruitType === "campus"
|
|
31
|
+
? `https://talent.antgroup.com/campus-list?positionId=${encodeURIComponent(id)}`
|
|
32
|
+
: `https://talent.antgroup.com/off-campus-position-detail?positionId=${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
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
38
|
+
Origin: "https://talent.antgroup.com",
|
|
39
|
+
};
|
|
40
|
+
async function post(path, body, referer) {
|
|
41
|
+
let response;
|
|
42
|
+
try {
|
|
43
|
+
response = await fetch(`${API_ROOT}${path}`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { ...DEFAULT_HEADERS, Referer: referer },
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
return { ok: false, message: `HTTP ${response.status}` };
|
|
54
|
+
let env;
|
|
55
|
+
try {
|
|
56
|
+
env = (await response.json());
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
60
|
+
}
|
|
61
|
+
if (env.success !== true) {
|
|
62
|
+
return { ok: false, message: env.errorMsg ?? `errorCode=${env.errorCode ?? "?"}` };
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, content: env.content, totalCount: env.totalCount ?? 0, message: "ok" };
|
|
65
|
+
}
|
|
66
|
+
function summarize(item, recruitType) {
|
|
67
|
+
const id = String(item.id ?? item.code ?? "");
|
|
68
|
+
const locs = Array.isArray(item.workLocations) ? item.workLocations.filter(Boolean).join(" / ") : "";
|
|
69
|
+
return {
|
|
70
|
+
post_id: id,
|
|
71
|
+
title: (item.name ?? "").trim(),
|
|
72
|
+
project: item.project?.trim() ||
|
|
73
|
+
item.categoryName?.trim() ||
|
|
74
|
+
(Array.isArray(item.categories) ? item.categories.filter(Boolean).join(" / ") : ""),
|
|
75
|
+
recruit_label: (item.positionType ?? "").trim() || (recruitType === "campus" ? "校招" : "社招"),
|
|
76
|
+
bgs: (item.department ?? "").trim(),
|
|
77
|
+
work_cities: locs,
|
|
78
|
+
apply_url: id ? DETAIL_URL(recruitType, id) : recruitType === "campus" ? CAMPUS_PAGE : SOCIAL_PAGE,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function searchSingle(recruitType, opts) {
|
|
82
|
+
const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 20));
|
|
99
83
|
const page = Math.max(1, opts.page ?? 1);
|
|
100
|
-
const
|
|
101
|
-
const
|
|
84
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
85
|
+
const body = {
|
|
86
|
+
key: keyword,
|
|
102
87
|
pageIndex: page,
|
|
103
88
|
pageSize,
|
|
104
|
-
channel,
|
|
105
89
|
language: "zh",
|
|
106
90
|
};
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
91
|
+
if (recruitType === "social") {
|
|
92
|
+
body.channel = "group_official_site";
|
|
93
|
+
body.regions = "";
|
|
94
|
+
body.categories = "";
|
|
95
|
+
body.subCategories = "";
|
|
96
|
+
body.bgCode = opts.bgCode ?? "";
|
|
97
|
+
body.socialQrCode = "";
|
|
98
|
+
}
|
|
99
|
+
const referer = recruitType === "campus" ? CAMPUS_PAGE : SOCIAL_PAGE;
|
|
100
|
+
const r = await post(`/${recruitType}/position/search`, body, referer);
|
|
101
|
+
if (!r.ok) {
|
|
102
|
+
return { ok: false, total: 0, positions: [], message: r.message };
|
|
103
|
+
}
|
|
115
104
|
return {
|
|
116
|
-
ok:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
105
|
+
ok: true,
|
|
106
|
+
total: r.totalCount ?? 0,
|
|
107
|
+
positions: (r.content ?? []).map((p) => summarize(p, recruitType)),
|
|
108
|
+
message: "ok",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// ---------- searchPositions ----------
|
|
112
|
+
export async function searchPositions(opts = {}) {
|
|
113
|
+
const recruitType = opts.recruitType ?? "all";
|
|
114
|
+
const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 20));
|
|
115
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
116
|
+
if (recruitType === "campus" || recruitType === "social") {
|
|
117
|
+
const r = await searchSingle(recruitType, opts);
|
|
118
|
+
if (!r.ok) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
source: SOURCE,
|
|
122
|
+
message: r.message,
|
|
123
|
+
query: { recruitType, page, pageSize, keyword: opts.keyword ?? "" },
|
|
124
|
+
positions: [],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
source: SOURCE,
|
|
130
|
+
query: { recruitType, page, pageSize, keyword: opts.keyword ?? "" },
|
|
131
|
+
page,
|
|
132
|
+
page_size: pageSize,
|
|
133
|
+
total: r.total,
|
|
134
|
+
positions: r.positions,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// "all" → ask both endpoints for the same page
|
|
138
|
+
const [campus, social] = await Promise.all([
|
|
139
|
+
searchSingle("campus", opts),
|
|
140
|
+
searchSingle("social", opts),
|
|
141
|
+
]);
|
|
142
|
+
const positions = [...campus.positions, ...social.positions];
|
|
143
|
+
const total = (campus.ok ? campus.total : 0) + (social.ok ? social.total : 0);
|
|
144
|
+
if (!campus.ok && !social.ok) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
source: SOURCE,
|
|
148
|
+
message: campus.message,
|
|
149
|
+
query: { recruitType: "all", page, pageSize, keyword: opts.keyword ?? "" },
|
|
150
|
+
positions: [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
source: SOURCE,
|
|
156
|
+
query: { recruitType: "all", page, pageSize, keyword: opts.keyword ?? "" },
|
|
120
157
|
page,
|
|
121
158
|
page_size: pageSize,
|
|
122
|
-
total
|
|
123
|
-
positions
|
|
159
|
+
total,
|
|
160
|
+
positions,
|
|
124
161
|
};
|
|
125
162
|
}
|
|
126
|
-
// ---------- fetchAllPositions
|
|
163
|
+
// ---------- fetchAllPositions ----------
|
|
127
164
|
export async function fetchAllPositions(opts = {}) {
|
|
165
|
+
const recruitType = opts.recruitType ?? "all";
|
|
166
|
+
const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 30));
|
|
167
|
+
const maxPages = Math.max(1, opts.maxPages ?? 40);
|
|
168
|
+
async function drain(rt) {
|
|
169
|
+
const bucket = [];
|
|
170
|
+
let total = 0;
|
|
171
|
+
let lastMsg = "ok";
|
|
172
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
173
|
+
const r = await searchSingle(rt, { ...opts, page, pageSize });
|
|
174
|
+
if (!r.ok) {
|
|
175
|
+
lastMsg = r.message;
|
|
176
|
+
if (bucket.length === 0)
|
|
177
|
+
return { ok: false, total: 0, positions: [], message: r.message };
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (total === 0)
|
|
181
|
+
total = r.total;
|
|
182
|
+
if (!r.positions.length)
|
|
183
|
+
break;
|
|
184
|
+
bucket.push(...r.positions);
|
|
185
|
+
if (bucket.length >= total)
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, total, positions: bucket, message: lastMsg };
|
|
189
|
+
}
|
|
190
|
+
if (recruitType === "campus" || recruitType === "social") {
|
|
191
|
+
const r = await drain(recruitType);
|
|
192
|
+
if (!r.ok) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
source: SOURCE,
|
|
196
|
+
message: r.message,
|
|
197
|
+
total: 0,
|
|
198
|
+
fetched: 0,
|
|
199
|
+
positions: [],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
source: SOURCE,
|
|
205
|
+
total: r.total,
|
|
206
|
+
fetched: r.positions.length,
|
|
207
|
+
positions: r.positions,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const [c, s] = await Promise.all([drain("campus"), drain("social")]);
|
|
128
211
|
return {
|
|
129
|
-
ok:
|
|
130
|
-
source:
|
|
131
|
-
|
|
132
|
-
fetched:
|
|
133
|
-
|
|
134
|
-
positions: [],
|
|
212
|
+
ok: true,
|
|
213
|
+
source: SOURCE,
|
|
214
|
+
total: (c.ok ? c.total : 0) + (s.ok ? s.total : 0),
|
|
215
|
+
fetched: c.positions.length + s.positions.length,
|
|
216
|
+
positions: [...c.positions, ...s.positions],
|
|
135
217
|
};
|
|
136
218
|
}
|
|
137
|
-
// ---------- fetchPositionDetail
|
|
219
|
+
// ---------- fetchPositionDetail ----------
|
|
220
|
+
// The list endpoint already returns description/requirement/teamDescription
|
|
221
|
+
// inline — no separate detail endpoint needed. Scan campus then social.
|
|
138
222
|
export async function fetchPositionDetail(postId) {
|
|
139
223
|
const id = (postId ?? "").trim();
|
|
140
|
-
if (!id)
|
|
141
|
-
return {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
224
|
+
if (!id)
|
|
225
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
226
|
+
for (const rt of ["campus", "social"]) {
|
|
227
|
+
const pageSize = 50;
|
|
228
|
+
const maxPages = 20;
|
|
229
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
230
|
+
const body = {
|
|
231
|
+
key: "",
|
|
232
|
+
pageIndex: page,
|
|
233
|
+
pageSize,
|
|
234
|
+
language: "zh",
|
|
235
|
+
};
|
|
236
|
+
if (rt === "social") {
|
|
237
|
+
body.channel = "group_official_site";
|
|
238
|
+
body.regions = "";
|
|
239
|
+
body.categories = "";
|
|
240
|
+
body.subCategories = "";
|
|
241
|
+
body.bgCode = "";
|
|
242
|
+
body.socialQrCode = "";
|
|
243
|
+
}
|
|
244
|
+
const referer = rt === "campus" ? CAMPUS_PAGE : SOCIAL_PAGE;
|
|
245
|
+
const r = await post(`/${rt}/position/search`, body, referer);
|
|
246
|
+
if (!r.ok)
|
|
247
|
+
break;
|
|
248
|
+
const found = (r.content ?? []).find((p) => String(p.id ?? p.code) === id);
|
|
249
|
+
if (found) {
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
source: SOURCE,
|
|
253
|
+
post_id: id,
|
|
254
|
+
title: found.name ?? "",
|
|
255
|
+
project: found.project ?? found.categoryName ?? "",
|
|
256
|
+
recruit_label: found.positionType ?? (rt === "campus" ? "校招" : "社招"),
|
|
257
|
+
department: found.department ?? "",
|
|
258
|
+
work_cities: found.workLocations ?? [],
|
|
259
|
+
publish_time: found.publishTime ?? "",
|
|
260
|
+
graduation_time: found.graduationTime ?? "",
|
|
261
|
+
experience: found.experience ?? "",
|
|
262
|
+
degree: found.degree ?? "",
|
|
263
|
+
description: found.description ?? "",
|
|
264
|
+
requirements: found.requirement ?? "",
|
|
265
|
+
team_description: found.teamDescription ?? "",
|
|
266
|
+
apply_url: DETAIL_URL(rt, id),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (r.totalCount && (r.content?.length ?? 0) < pageSize)
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
146
272
|
}
|
|
147
273
|
return {
|
|
148
274
|
ok: false,
|
|
149
|
-
source:
|
|
150
|
-
message: STUB_MESSAGE,
|
|
275
|
+
source: SOURCE,
|
|
151
276
|
post_id: id,
|
|
152
|
-
|
|
277
|
+
message: `post ${id} not found in campus or social feeds`,
|
|
153
278
|
};
|
|
154
279
|
}
|
|
155
|
-
// ---------- fetchDictionaries
|
|
280
|
+
// ---------- fetchDictionaries ----------
|
|
281
|
+
let _dictCache = null;
|
|
156
282
|
export async function fetchDictionaries() {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
283
|
+
if (_dictCache !== null)
|
|
284
|
+
return _dictCache;
|
|
285
|
+
const [depRes, regRes, catRes] = await Promise.all([
|
|
286
|
+
post("/social/category/listDept", { channel: "group_official_site", language: "zh" }, SOCIAL_PAGE),
|
|
287
|
+
post("/region/hot", { channel: "group_official_site", language: "zh" }, SOCIAL_PAGE),
|
|
288
|
+
post("/social/category/list", { channel: "group_official_site", language: "zh" }, SOCIAL_PAGE),
|
|
289
|
+
]);
|
|
290
|
+
if (!depRes.ok && !regRes.ok && !catRes.ok) {
|
|
291
|
+
const r = { ok: false, source: SOURCE, message: depRes.message };
|
|
292
|
+
_dictCache = r;
|
|
293
|
+
return r;
|
|
294
|
+
}
|
|
295
|
+
const result = {
|
|
296
|
+
ok: true,
|
|
297
|
+
source: SOURCE,
|
|
298
|
+
bgs: (depRes.content ?? []).map((d) => ({ code: d.code ?? "", name: d.name ?? "" })),
|
|
299
|
+
regions: (regRes.content ?? []).map((d) => ({ code: d.code ?? "", name: d.name ?? "" })),
|
|
300
|
+
categories: catRes.content ?? [],
|
|
165
301
|
};
|
|
302
|
+
_dictCache = result;
|
|
303
|
+
return result;
|
|
166
304
|
}
|
|
167
|
-
// ---------- notices
|
|
305
|
+
// ---------- notices ----------
|
|
306
|
+
const NOTICES_MSG = "Ant Group (蚂蚁集团): no public notices endpoint on hrcareersweb";
|
|
168
307
|
export async function listNotices() {
|
|
169
|
-
return {
|
|
170
|
-
ok: false,
|
|
171
|
-
source: PORTAL_ROOT,
|
|
172
|
-
message: "Ant Group: no public notices endpoint",
|
|
173
|
-
};
|
|
308
|
+
return { ok: false, source: SOURCE, message: NOTICES_MSG, notices: [] };
|
|
174
309
|
}
|
|
175
|
-
export async function getNotice(
|
|
176
|
-
return {
|
|
177
|
-
ok: false,
|
|
178
|
-
source: PORTAL_ROOT,
|
|
179
|
-
message: "Ant Group: no public notices endpoint",
|
|
180
|
-
};
|
|
310
|
+
export async function getNotice(noticeId) {
|
|
311
|
+
return { ok: false, source: SOURCE, message: NOTICES_MSG, notice_id: noticeId };
|
|
181
312
|
}
|
|
182
|
-
export async function findNoticesByQuestion(
|
|
183
|
-
return {
|
|
184
|
-
ok: false,
|
|
185
|
-
source: PORTAL_ROOT,
|
|
186
|
-
message: "Ant Group: no public notices endpoint",
|
|
187
|
-
matches: [],
|
|
188
|
-
};
|
|
313
|
+
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
314
|
+
return { ok: false, source: SOURCE, question, message: NOTICES_MSG, matches: [] };
|
|
189
315
|
}
|
|
190
|
-
// ---------- matchResume
|
|
316
|
+
// ---------- matchResume ----------
|
|
191
317
|
export async function matchResume(text, opts = {}) {
|
|
318
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
319
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
192
320
|
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
321
|
+
if (!terms.length) {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
source: SOURCE,
|
|
325
|
+
message: "could not extract any technical signals from the text",
|
|
326
|
+
preview: (text ?? "").slice(0, 120),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
330
|
+
const list = await searchPositions({ keyword, page: 1, pageSize: 50, recruitType: "all" });
|
|
331
|
+
if (!list.ok) {
|
|
332
|
+
return { ok: false, source: SOURCE, message: list.message, positions: [] };
|
|
333
|
+
}
|
|
334
|
+
const scored = [];
|
|
335
|
+
for (const p of list.positions) {
|
|
336
|
+
const blob = [p.title, p.project, p.recruit_label, p.bgs, p.work_cities].join(" ");
|
|
337
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
338
|
+
if (score > 0)
|
|
339
|
+
scored.push({ score, position: p, reasons });
|
|
340
|
+
}
|
|
341
|
+
scored.sort((a, b) => b.score - a.score);
|
|
342
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
343
|
+
if (!shortlist.length) {
|
|
344
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({ score: 0, position, reasons: [] }));
|
|
345
|
+
}
|
|
346
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
347
|
+
const mr = s.reasons.length > 0
|
|
348
|
+
? s.reasons.slice(0, 5)
|
|
349
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
350
|
+
return { ...s.position, match_reasons: mr };
|
|
351
|
+
});
|
|
193
352
|
return {
|
|
194
|
-
ok:
|
|
195
|
-
source:
|
|
196
|
-
message: STUB_MESSAGE,
|
|
353
|
+
ok: true,
|
|
354
|
+
source: SOURCE,
|
|
197
355
|
extracted_terms: terms,
|
|
198
356
|
city_preferences: cities,
|
|
199
|
-
matches
|
|
357
|
+
matches,
|
|
358
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
359
|
+
"The only authority on selection is HR.",
|
|
200
360
|
};
|
|
201
361
|
}
|
|
362
|
+
export { extractResumeSignals, scoreOverlap };
|
package/dist/cambricon.js
CHANGED
|
@@ -1,64 +1,32 @@
|
|
|
1
|
-
// 寒武纪 (Cambricon) —
|
|
2
|
-
//
|
|
3
|
-
// STATUS: stub-only. Cambricon's careers domains do not resolve over public
|
|
4
|
-
// DNS, and no third-party ATS tenant (Feishu, Moka, Greenhouse, Lever) is
|
|
5
|
-
// provisioned for the company. Recruiting runs through internal channels.
|
|
1
|
+
// 寒武纪 (Cambricon) careers adapter — Moka SSR + AES-128-CBC pagination.
|
|
6
2
|
//
|
|
7
3
|
// ============================================================
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// https://hr.cambricon.com — 000 (no public DNS / unreachable)
|
|
11
|
-
// https://careers.cambricon.com — 000 (no public DNS / unreachable)
|
|
12
|
-
// https://campus.cambricon.com — 000 (no public DNS / unreachable)
|
|
4
|
+
// API DISCOVERY (probed 2026-05-16)
|
|
13
5
|
//
|
|
14
|
-
//
|
|
15
|
-
// Greenhouse: cambricon — HTTP 404 (no board)
|
|
16
|
-
// Lever: cambricon — HTTP 404 (no posting)
|
|
17
|
-
// Moka: app.mokahr.com/social-recruitment/cambricon → 302 (unprovisioned)
|
|
6
|
+
// www.cambricon.com embeds links to Moka tenant URLs in its 加入我们 section:
|
|
18
7
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
8
|
+
// /campus-recruitment/cambricon/44201 ← campus + intern (main entry)
|
|
9
|
+
// /recommendation-recruitment/cambricon/42452 (referral channel, overlaps)
|
|
10
|
+
// /recommendation-recruitment/cambricon/46261 (referral channel, overlaps)
|
|
22
11
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export async function listNotices() {
|
|
45
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, notices: [] };
|
|
46
|
-
}
|
|
47
|
-
export async function getNotice(noticeId) {
|
|
48
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, notice_id: noticeId };
|
|
49
|
-
}
|
|
50
|
-
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
51
|
-
return { ok: false, source: SOURCE, question, message: STUB_MESSAGE, matches: [] };
|
|
52
|
-
}
|
|
53
|
-
export async function matchResume(text, _opts = {}) {
|
|
54
|
-
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
55
|
-
return {
|
|
56
|
-
ok: false,
|
|
57
|
-
source: SOURCE,
|
|
58
|
-
extracted_terms: terms,
|
|
59
|
-
city_preferences: cities,
|
|
60
|
-
matches: [],
|
|
61
|
-
message: STUB_MESSAGE,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
export { extractResumeSignals, scoreOverlap };
|
|
12
|
+
// No /social-recruitment/cambricon/<siteId> URL is published — Cambricon
|
|
13
|
+
// only opens 校招 / 实习 publicly through Moka. Same factory as
|
|
14
|
+
// `cli/src/moka.ts` (used by megvii / geely / etc.).
|
|
15
|
+
import { createAdapter } from "./moka.js";
|
|
16
|
+
const adapter = createAdapter({
|
|
17
|
+
orgSlug: "cambricon",
|
|
18
|
+
label: "Cambricon",
|
|
19
|
+
channels: [
|
|
20
|
+
{ siteId: 44201, kind: "campus-recruitment", recruitType: "campus" },
|
|
21
|
+
],
|
|
22
|
+
defaultRecruitType: "campus",
|
|
23
|
+
});
|
|
24
|
+
export const searchPositions = adapter.searchPositions;
|
|
25
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
26
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
27
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
28
|
+
export const listNotices = adapter.listNotices;
|
|
29
|
+
export const getNotice = adapter.getNotice;
|
|
30
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
31
|
+
export const matchResume = adapter.matchResume;
|
|
32
|
+
export const checkResume = adapter.checkResume;
|