job-pro 0.7.0 → 0.7.2
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/alibaba.js +2 -2
- package/dist/baichuan.js +38 -123
- package/dist/bilibili.js +43 -0
- package/dist/byd.js +333 -112
- package/dist/cambricon.js +379 -39
- package/dist/deepseek.js +373 -25
- package/dist/galaxyuniversal.js +375 -23
- package/dist/horizonrobotics.js +40 -61
- package/dist/iflytek.js +299 -57
- package/dist/index.js +55 -50
- package/dist/megvii.js +387 -150
- package/dist/mihoyo.js +241 -67
- package/dist/moonshot.js +387 -54
- package/dist/oppo.js +212 -56
- package/dist/pdd.js +278 -129
- package/dist/sensetime.js +47 -183
- package/dist/sf.js +249 -36
- package/dist/stepfun.js +333 -105
- package/dist/vivo.js +291 -41
- package/dist/wecruit.js +385 -0
- package/dist/weibo.js +259 -88
- package/dist/zerooneai.js +40 -37
- package/package.json +3 -2
package/dist/pdd.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Thin client for 拼多多 (PDD / Pinduoduo) campus-recruiting portal at careers.pinduoduo.com.
|
|
2
2
|
//
|
|
3
3
|
// ============================================================
|
|
4
|
-
// API discovery (probed 2026-05
|
|
4
|
+
// API discovery (re-probed 2026-05; bundles under pfile.pddpic.com/ei-pub):
|
|
5
5
|
//
|
|
6
6
|
// The frontend is a Next.js SPA hosted at careers.pinduoduo.com/campus/.
|
|
7
7
|
// All XHR calls go through a sourceSDK.wrappedRequest helper (module 96211)
|
|
@@ -9,52 +9,56 @@
|
|
|
9
9
|
//
|
|
10
10
|
// https://careers.pinduoduo.com/api/careers/<relative-url>
|
|
11
11
|
//
|
|
12
|
-
// ----
|
|
12
|
+
// ---- PUBLIC endpoints (no auth required, verified 2026-05) ----
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
14
|
+
// POST /api/careers/api/recruit/position/list
|
|
15
|
+
// Body: { pageSize:<int>, page:<int> } (keyword/recruitType IGNORED server-side)
|
|
16
|
+
// Response: { success:true, result:{ list:[Position], total:"<int>" } }
|
|
17
|
+
// Position: { id (uuid), name, code, workLocation, workLocationName,
|
|
18
|
+
// job (eng key), jobName (zh), releaseTime (ms),
|
|
19
|
+
// jobDuty (full JD HTML/text), labelList[], recruitTypeName }
|
|
20
|
+
// Pagination: page size is FIXED at 10 regardless of pageSize param.
|
|
21
|
+
// Total returned across all pages (e.g. 30 for grad, 7 for intern).
|
|
19
22
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// Response: { success:true, result:{ guid, momentTitle, momentContent:<html> } }
|
|
23
|
+
// POST /api/careers/api/recruit/position/train/list
|
|
24
|
+
// Same shape as /list but only intern (实习生 / 2027届研发实习生) positions.
|
|
23
25
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
26
|
+
// POST /api/careers/api/recruit/position/detail/type
|
|
27
|
+
// Body: {} → returns the job-type dictionary
|
|
28
|
+
// Result: [{job:"technology", jobName:"技术"}, {job:"general", jobName:"职能"},
|
|
29
|
+
// {job:"product", jobName:"产品"}, {job:"language", jobName:"语言"},
|
|
30
|
+
// {job:"market", jobName:"市场营销"}, {job:"visual", jobName:"视觉类"},
|
|
31
|
+
// {job:"investment", jobName:"运营"}, {job:"vegetable", jobName:"区域业务"}]
|
|
28
32
|
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
33
|
+
// POST /api/careers/api/campus/moment/list (notices / 公告)
|
|
34
|
+
// Body: { pageSize:<int>, page:<int> }
|
|
35
|
+
// Response: { success:true, result:{ list:[MomentItem], total:<int> } }
|
|
36
|
+
// MomentItem: { guid, momentTitle, momentLabel:[string], publishDate:<ms>, topFlag:bool }
|
|
31
37
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// POST /api/careers/api/recruit/site/query
|
|
36
|
-
// POST /api/careers/api/campus/area/full/list
|
|
37
|
-
// POST /api/careers/api/campus/education/major/query
|
|
38
|
-
// POST /api/careers/api/recruit/qa/list
|
|
39
|
-
// POST /api/careers/api/recruit/queryByShortLink
|
|
38
|
+
// POST /api/careers/api/campus/moment/detail
|
|
39
|
+
// Body: { guid:<string> }
|
|
40
|
+
// Response: { success:true, result:{ guid, momentTitle, momentContent:<html> } }
|
|
40
41
|
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// from PDD's yak/openresty CDN layer. These require a dynamically
|
|
44
|
-
// generated Anti-Content header produced by PDD's JS risk-control SDK.
|
|
42
|
+
// POST /api/careers/api/campus/trip/list (校招行程 / on-campus events)
|
|
43
|
+
// Body: {} → { tripList:null|[...], explainTrip:{ recruitmentTripType, tripContent:<html> } }
|
|
45
44
|
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
45
|
+
// POST /api/careers/api/recruit/qa/common/list (FAQ)
|
|
46
|
+
// Body: {} → [{ question, questionCode }] (only ~2 items publicly)
|
|
47
|
+
//
|
|
48
|
+
// POST /api/careers/api/campus/careers/enum (enum map, empty for anon)
|
|
49
|
+
//
|
|
50
|
+
// ---- AUTH-REQUIRED endpoints (HTTP 401 without login token) ----
|
|
51
|
+
// /api/recruit/position/queryPosition (rich search with all filters)
|
|
52
|
+
// /api/recruit/position/querySecondPosition
|
|
53
|
+
// /api/recruit/site/query, /campus/area/full/list, /campus/education/major/query
|
|
54
|
+
// /api/recruit/qa/list, /api/recruit/queryByShortLink
|
|
51
55
|
//
|
|
52
56
|
// ---- RecruitType taxonomy (from JS bundle, probed 2026-05) ----
|
|
53
57
|
// headquarters = 管培生 (headquarters management trainee)
|
|
54
58
|
// region = 区域业务管培生 (regional business management trainee)
|
|
55
59
|
// technical_session = 技术专场 (technical session / R&D)
|
|
56
60
|
// warehouse_trainee = 仓储类管培生 (warehouse management trainee)
|
|
57
|
-
// yunhu_plan = 云弧计划 (LLM / elite tech talent program
|
|
61
|
+
// yunhu_plan = 云弧计划 (LLM / elite tech talent program)
|
|
58
62
|
// intern = 实习生 (intern, 2027届)
|
|
59
63
|
//
|
|
60
64
|
// ---- Campus batches active at time of probing (2026-05) ----
|
|
@@ -63,6 +67,17 @@
|
|
|
63
67
|
// 云弧计划 (elite LLM/AI talent program, 2026) → route /grad?recruitType=yunhu_plan
|
|
64
68
|
//
|
|
65
69
|
// ============================================================
|
|
70
|
+
// IMPLEMENTATION NOTES
|
|
71
|
+
// - searchPositions: uses /api/recruit/position/list (grad track). Server
|
|
72
|
+
// ignores `keyword` and `pageSize`; we filter client-side by keyword and
|
|
73
|
+
// slice the response to honour the requested pageSize. Server returns
|
|
74
|
+
// fixed pages of 10.
|
|
75
|
+
// - fetchAllPositions: paginates through both /list and /train/list to
|
|
76
|
+
// surface every public position.
|
|
77
|
+
// - fetchPositionDetail: there is no per-position detail endpoint; the
|
|
78
|
+
// /list response already includes the full jobDuty. We scan the list to
|
|
79
|
+
// locate the matching record by id.
|
|
80
|
+
// ============================================================
|
|
66
81
|
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
67
82
|
export { checkResume };
|
|
68
83
|
const API_ROOT = "https://careers.pinduoduo.com/api/careers";
|
|
@@ -120,70 +135,158 @@ async function call(path, body) {
|
|
|
120
135
|
message: ok ? "ok" : (payload.errorMsg || `errorCode ${payload.errorCode}`),
|
|
121
136
|
};
|
|
122
137
|
}
|
|
138
|
+
const DETAIL_URL = (id) => `${GRAD_PAGE}?id=${encodeURIComponent(id)}`;
|
|
139
|
+
function summarizePosition(raw) {
|
|
140
|
+
const id = raw.id ?? "";
|
|
141
|
+
return {
|
|
142
|
+
post_id: id,
|
|
143
|
+
title: raw.name ?? "",
|
|
144
|
+
project: raw.jobName ?? "",
|
|
145
|
+
recruit_label: raw.recruitTypeName ?? "",
|
|
146
|
+
bgs: (raw.labelList ?? []).join(" / "),
|
|
147
|
+
work_cities: raw.workLocationName ?? raw.workLocation ?? "",
|
|
148
|
+
apply_url: id ? DETAIL_URL(id) : GRAD_PAGE,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Server returns 10 items/page regardless of pageSize. Treat that as the upstream chunk.
|
|
152
|
+
const UPSTREAM_PAGE_SIZE = 10;
|
|
153
|
+
function asNumber(value) {
|
|
154
|
+
if (typeof value === "number")
|
|
155
|
+
return value;
|
|
156
|
+
if (typeof value === "string") {
|
|
157
|
+
const n = parseInt(value, 10);
|
|
158
|
+
return Number.isFinite(n) ? n : 0;
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
123
162
|
// ---------- searchPositions ----------
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
163
|
+
// Uses the public /api/recruit/position/list endpoint. The server ignores
|
|
164
|
+
// `keyword` and `pageSize` (always returns 10 items/page in releaseTime order),
|
|
165
|
+
// so we paginate upstream, then filter+slice client-side to honour the
|
|
166
|
+
// caller-requested keyword/pageSize.
|
|
127
167
|
export async function searchPositions(opts = {}) {
|
|
128
168
|
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
129
169
|
const page = Math.max(1, opts.page ?? 1);
|
|
130
|
-
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
170
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60).toLowerCase();
|
|
171
|
+
const includeIntern = opts.recruitType === "intern";
|
|
172
|
+
// Walk upstream until we have enough filtered rows to satisfy the requested page.
|
|
173
|
+
const collected = [];
|
|
174
|
+
let upstreamTotal = 0;
|
|
175
|
+
const need = page * pageSize;
|
|
176
|
+
const maxUpstreamPages = 10; // safety cap (10 pages × 10 = 100 positions)
|
|
177
|
+
const endpoints = includeIntern
|
|
178
|
+
? ["/api/recruit/position/train/list"]
|
|
179
|
+
: ["/api/recruit/position/list"];
|
|
180
|
+
for (const path of endpoints) {
|
|
181
|
+
for (let p = 1; p <= maxUpstreamPages; p++) {
|
|
182
|
+
const response = await call(path, {
|
|
183
|
+
pageSize: UPSTREAM_PAGE_SIZE,
|
|
184
|
+
page: p,
|
|
185
|
+
});
|
|
186
|
+
if (!response.ok || !response.data) {
|
|
187
|
+
// Surface auth-style failures as ok:false.
|
|
188
|
+
if (collected.length === 0) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
source: "careers.pinduoduo.com",
|
|
192
|
+
query: { pageSize, page, keyword: keyword || undefined, recruitType: opts.recruitType },
|
|
193
|
+
page,
|
|
194
|
+
page_size: pageSize,
|
|
195
|
+
total: 0,
|
|
196
|
+
positions: [],
|
|
197
|
+
message: response.message,
|
|
198
|
+
apply_at: GRAD_PAGE,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
upstreamTotal = Math.max(upstreamTotal, asNumber(response.data.total));
|
|
204
|
+
const batch = response.data.list ?? [];
|
|
205
|
+
if (!batch.length)
|
|
206
|
+
break;
|
|
207
|
+
for (const item of batch) {
|
|
208
|
+
if (!keyword) {
|
|
209
|
+
collected.push(item);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const hay = `${item.name ?? ""} ${item.recruitTypeName ?? ""} ${item.jobName ?? ""} ${item.workLocationName ?? ""} ${item.jobDuty ?? ""}`.toLowerCase();
|
|
213
|
+
if (hay.includes(keyword))
|
|
214
|
+
collected.push(item);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Continue paginating until we have enough or exhausted the upstream pool.
|
|
218
|
+
if (collected.length >= need && p * UPSTREAM_PAGE_SIZE >= upstreamTotal)
|
|
219
|
+
break;
|
|
220
|
+
if (batch.length < UPSTREAM_PAGE_SIZE)
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
154
223
|
}
|
|
155
|
-
const
|
|
224
|
+
const start = (page - 1) * pageSize;
|
|
225
|
+
const slice = collected.slice(start, start + pageSize);
|
|
156
226
|
return {
|
|
157
|
-
ok:
|
|
227
|
+
ok: true,
|
|
158
228
|
source: "careers.pinduoduo.com",
|
|
159
|
-
query:
|
|
229
|
+
query: { pageSize, page, keyword: keyword || undefined, recruitType: opts.recruitType },
|
|
160
230
|
page,
|
|
161
231
|
page_size: pageSize,
|
|
162
|
-
total:
|
|
163
|
-
positions:
|
|
164
|
-
message: isAuth
|
|
165
|
-
? "PDD position API requires a registered account login token (HTTP 401). " +
|
|
166
|
-
"The endpoint POST /api/careers/api/recruit/position/queryPosition was reached " +
|
|
167
|
-
"but rejected unauthenticated requests. Apply at: " + GRAD_PAGE
|
|
168
|
-
: response.message,
|
|
232
|
+
total: keyword ? collected.length : upstreamTotal || collected.length,
|
|
233
|
+
positions: slice.map(summarizePosition),
|
|
169
234
|
apply_at: GRAD_PAGE,
|
|
170
235
|
};
|
|
171
236
|
}
|
|
172
237
|
// ---------- fetchAllPositions ----------
|
|
238
|
+
// Walks both grad (/list) and intern (/train/list) tracks until exhaustion.
|
|
173
239
|
export async function fetchAllPositions(opts = {}) {
|
|
174
|
-
|
|
175
|
-
const
|
|
240
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 50));
|
|
241
|
+
const maxPages = Math.max(1, opts.maxPages ?? 20);
|
|
242
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60).toLowerCase();
|
|
243
|
+
const bucket = [];
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
let total = 0;
|
|
246
|
+
for (const path of ["/api/recruit/position/list", "/api/recruit/position/train/list"]) {
|
|
247
|
+
let pathTotal = 0;
|
|
248
|
+
for (let p = 1; p <= maxPages; p++) {
|
|
249
|
+
const response = await call(path, {
|
|
250
|
+
pageSize: UPSTREAM_PAGE_SIZE,
|
|
251
|
+
page: p,
|
|
252
|
+
});
|
|
253
|
+
if (!response.ok || !response.data)
|
|
254
|
+
break;
|
|
255
|
+
pathTotal = Math.max(pathTotal, asNumber(response.data.total));
|
|
256
|
+
const batch = response.data.list ?? [];
|
|
257
|
+
if (!batch.length)
|
|
258
|
+
break;
|
|
259
|
+
for (const item of batch) {
|
|
260
|
+
const id = item.id ?? "";
|
|
261
|
+
if (!id || seen.has(id))
|
|
262
|
+
continue;
|
|
263
|
+
seen.add(id);
|
|
264
|
+
if (keyword) {
|
|
265
|
+
const hay = `${item.name ?? ""} ${item.jobName ?? ""} ${item.recruitTypeName ?? ""} ${item.workLocationName ?? ""} ${item.jobDuty ?? ""}`.toLowerCase();
|
|
266
|
+
if (!hay.includes(keyword))
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
bucket.push(item);
|
|
270
|
+
}
|
|
271
|
+
if (batch.length < UPSTREAM_PAGE_SIZE)
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
total += pathTotal;
|
|
275
|
+
if (bucket.length >= pageSize * maxPages)
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
176
278
|
return {
|
|
177
|
-
ok:
|
|
279
|
+
ok: true,
|
|
178
280
|
source: "careers.pinduoduo.com",
|
|
179
|
-
total:
|
|
180
|
-
fetched:
|
|
181
|
-
positions:
|
|
182
|
-
message: result.message,
|
|
281
|
+
total: keyword ? bucket.length : total || bucket.length,
|
|
282
|
+
fetched: bucket.length,
|
|
283
|
+
positions: bucket.map(summarizePosition),
|
|
183
284
|
apply_at: GRAD_PAGE,
|
|
184
285
|
};
|
|
185
286
|
}
|
|
186
287
|
// ---------- fetchPositionDetail ----------
|
|
288
|
+
// There is no per-position detail endpoint; the list response already carries
|
|
289
|
+
// the full jobDuty. We scan grad + intern lists for the matching uuid.
|
|
187
290
|
export async function fetchPositionDetail(postId) {
|
|
188
291
|
const id = (postId ?? "").trim();
|
|
189
292
|
if (!id) {
|
|
@@ -193,28 +296,55 @@ export async function fetchPositionDetail(postId) {
|
|
|
193
296
|
message: "post_id is required",
|
|
194
297
|
};
|
|
195
298
|
}
|
|
196
|
-
|
|
197
|
-
|
|
299
|
+
for (const path of ["/api/recruit/position/list", "/api/recruit/position/train/list"]) {
|
|
300
|
+
for (let p = 1; p <= 10; p++) {
|
|
301
|
+
const response = await call(path, {
|
|
302
|
+
pageSize: UPSTREAM_PAGE_SIZE,
|
|
303
|
+
page: p,
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok || !response.data)
|
|
306
|
+
break;
|
|
307
|
+
const batch = response.data.list ?? [];
|
|
308
|
+
if (!batch.length)
|
|
309
|
+
break;
|
|
310
|
+
const hit = batch.find((row) => row.id === id);
|
|
311
|
+
if (hit) {
|
|
312
|
+
const summary = summarizePosition(hit);
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
source: "careers.pinduoduo.com",
|
|
316
|
+
...summary,
|
|
317
|
+
description: hit.jobDuty ?? "",
|
|
318
|
+
requirements: "",
|
|
319
|
+
work_cities: hit.workLocationName ? [hit.workLocationName] : [],
|
|
320
|
+
release_time: hit.releaseTime
|
|
321
|
+
? new Date(hit.releaseTime).toISOString().slice(0, 10)
|
|
322
|
+
: "",
|
|
323
|
+
code: hit.code ?? "",
|
|
324
|
+
labels: hit.labelList ?? [],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (batch.length < UPSTREAM_PAGE_SIZE)
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
198
331
|
return {
|
|
199
332
|
ok: false,
|
|
200
333
|
source: "careers.pinduoduo.com",
|
|
201
334
|
post_id: id,
|
|
202
|
-
description: "",
|
|
203
|
-
requirements: "",
|
|
204
|
-
work_cities: [],
|
|
205
335
|
apply_url: GRAD_PAGE,
|
|
206
|
-
message: "
|
|
207
|
-
|
|
336
|
+
message: "Position not found in current public list. The position may have been " +
|
|
337
|
+
"closed or moved to an auth-only track. " +
|
|
338
|
+
`Visit ${GRAD_PAGE} to browse current openings.`,
|
|
208
339
|
};
|
|
209
340
|
}
|
|
210
|
-
// ---------- fetchDictionaries ----------
|
|
211
|
-
// The /api/careers/api/campus/careers/enum endpoint is publicly accessible
|
|
212
|
-
// but returns an empty enumMap. Recruit types are documented in the header
|
|
213
|
-
// comment above (extracted from the JS bundle).
|
|
214
341
|
export async function fetchDictionaries() {
|
|
215
|
-
const
|
|
342
|
+
const [enumResp, typeResp] = await Promise.all([
|
|
343
|
+
call("/api/campus/careers/enum", {}),
|
|
344
|
+
call("/api/recruit/position/detail/type", {}),
|
|
345
|
+
]);
|
|
216
346
|
return {
|
|
217
|
-
ok:
|
|
347
|
+
ok: enumResp.ok || typeResp.ok,
|
|
218
348
|
source: "careers.pinduoduo.com",
|
|
219
349
|
api_host: API_ROOT,
|
|
220
350
|
verified_at: new Date().toISOString(),
|
|
@@ -226,12 +356,14 @@ export async function fetchDictionaries() {
|
|
|
226
356
|
{ value: "technical_session", label: "技术专场", note: "Technical session / R&D" },
|
|
227
357
|
{ value: "warehouse_trainee", label: "仓储类管培生", note: "Warehouse management trainee" },
|
|
228
358
|
{ value: "yunhu_plan", label: "云弧计划", note: "LLM/AI elite talent program (≈ ByteDance 顶尖实习)" },
|
|
359
|
+
{ value: "intern", label: "实习生", note: "Intern (2027届)" },
|
|
229
360
|
],
|
|
361
|
+
job_types: (typeResp.data ?? []).map((t) => ({ value: t.job ?? "", label: t.jobName ?? "" })),
|
|
230
362
|
current_batch: "2026届春季校招 / 2027届研发实习生",
|
|
231
363
|
grad_page: GRAD_PAGE,
|
|
232
364
|
intern_page: INTERN_PAGE,
|
|
233
|
-
enum_map:
|
|
234
|
-
note: "Position
|
|
365
|
+
enum_map: enumResp.data?.enumMap ?? {},
|
|
366
|
+
note: "Position list is public via /api/recruit/position/list — see header comment.",
|
|
235
367
|
};
|
|
236
368
|
}
|
|
237
369
|
function formatNotice(item) {
|
|
@@ -375,11 +507,11 @@ export async function findNoticesByQuestion(question, opts = {}) {
|
|
|
375
507
|
};
|
|
376
508
|
}
|
|
377
509
|
// ---------- matchResume ----------
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
// to manually browse matching positions.
|
|
510
|
+
// Pulls every public position (grad + intern), scores each against the resume's
|
|
511
|
+
// extracted terms and city preferences, and returns the top-N matches.
|
|
381
512
|
export async function matchResume(text, opts = {}) {
|
|
382
513
|
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
514
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
383
515
|
if (!terms.length) {
|
|
384
516
|
return {
|
|
385
517
|
ok: false,
|
|
@@ -388,46 +520,63 @@ export async function matchResume(text, opts = {}) {
|
|
|
388
520
|
preview: (text ?? "").slice(0, 120),
|
|
389
521
|
};
|
|
390
522
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
recruitType: "yunhu_plan",
|
|
403
|
-
label: "云弧计划 (LLM Elite)",
|
|
404
|
-
url: `${GRAD_PAGE}?recruitType=yunhu_plan`,
|
|
405
|
-
reason: "LLM/AI signals detected: " + terms.filter((t) => ["llm", "rag", "transformer", "bert", "gpt", "diffusion", "大模型"].includes(t.toLowerCase())).slice(0, 3).join(", "),
|
|
406
|
-
});
|
|
523
|
+
const all = await fetchAllPositions({ maxPages: 10, pageSize: UPSTREAM_PAGE_SIZE });
|
|
524
|
+
if (!all.ok) {
|
|
525
|
+
return {
|
|
526
|
+
ok: false,
|
|
527
|
+
source: "careers.pinduoduo.com",
|
|
528
|
+
extracted_terms: terms,
|
|
529
|
+
city_preferences: cities,
|
|
530
|
+
matches: [],
|
|
531
|
+
message: all.message ?? "failed to fetch positions",
|
|
532
|
+
apply_at: GRAD_PAGE,
|
|
533
|
+
};
|
|
407
534
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
535
|
+
// Score against the raw positions we already have via fetchAllPositions's caller.
|
|
536
|
+
// Re-pull as raw to retain jobDuty for scoring.
|
|
537
|
+
const rawCorpus = [];
|
|
538
|
+
for (const path of ["/api/recruit/position/list", "/api/recruit/position/train/list"]) {
|
|
539
|
+
for (let p = 1; p <= 10; p++) {
|
|
540
|
+
const response = await call(path, {
|
|
541
|
+
pageSize: UPSTREAM_PAGE_SIZE,
|
|
542
|
+
page: p,
|
|
543
|
+
});
|
|
544
|
+
if (!response.ok || !response.data)
|
|
545
|
+
break;
|
|
546
|
+
const batch = response.data.list ?? [];
|
|
547
|
+
if (!batch.length)
|
|
548
|
+
break;
|
|
549
|
+
rawCorpus.push(...batch);
|
|
550
|
+
if (batch.length < UPSTREAM_PAGE_SIZE)
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
415
553
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
554
|
+
const scored = [];
|
|
555
|
+
for (const raw of rawCorpus) {
|
|
556
|
+
const hay = `${raw.name ?? ""} ${raw.jobName ?? ""} ${raw.recruitTypeName ?? ""} ${raw.jobDuty ?? ""}`.toLowerCase();
|
|
557
|
+
const matched = terms.filter((t) => hay.includes(t.toLowerCase()));
|
|
558
|
+
const overlap = scoreOverlap(hay, terms, cities).score;
|
|
559
|
+
const city_match = cities.length === 0 ? false :
|
|
560
|
+
cities.some((c) => (raw.workLocationName ?? raw.workLocation ?? "").includes(c));
|
|
561
|
+
if (!matched.length && !city_match)
|
|
562
|
+
continue;
|
|
563
|
+
const score = overlap * 100 + matched.length * 5 + (city_match ? 8 : 0);
|
|
564
|
+
scored.push({ raw, score, matched_terms: matched, city_match });
|
|
565
|
+
}
|
|
566
|
+
scored.sort((a, b) => b.score - a.score);
|
|
567
|
+
const matches = scored.slice(0, topN).map((s) => ({
|
|
568
|
+
...summarizePosition(s.raw),
|
|
569
|
+
score: s.score,
|
|
570
|
+
matched_terms: s.matched_terms,
|
|
571
|
+
city_match: s.city_match,
|
|
572
|
+
}));
|
|
422
573
|
return {
|
|
423
574
|
ok: true,
|
|
424
575
|
source: "careers.pinduoduo.com",
|
|
425
576
|
extracted_terms: terms,
|
|
426
577
|
city_preferences: cities,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
suggested_tracks: suggested.slice(0, opts.topN ?? 3),
|
|
430
|
-
matches: [],
|
|
578
|
+
total_scanned: rawCorpus.length,
|
|
579
|
+
matches,
|
|
431
580
|
apply_at: GRAD_PAGE,
|
|
432
581
|
};
|
|
433
582
|
}
|