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/byd.js
CHANGED
|
@@ -1,160 +1,381 @@
|
|
|
1
|
-
//
|
|
1
|
+
// BYD (比亚迪) recruiting adapter — job.byd.com.
|
|
2
2
|
//
|
|
3
3
|
// ============================================================
|
|
4
|
-
//
|
|
5
|
-
// chunk-e8fe.d262cda1.js, chunk-ac75.7dee0692.js, chunk-a7e5.62aed375.js,
|
|
6
|
-
// chunk-76ac.cedb4013.js, chunk-dbeb.0075e53e.js):
|
|
4
|
+
// API DISCOVERY (probed 2026-05-15)
|
|
7
5
|
//
|
|
8
|
-
//
|
|
9
|
-
// https://job.byd.com/ → redirects to https://job.byd.com/portal/pc/
|
|
10
|
-
// https://careers.byd.com/ → Vite/Vue marketing page (static, no job listings)
|
|
11
|
-
// https://job.byd.com/portal/pc/ → main Vue SPA (webpack, ElementUI)
|
|
6
|
+
// The job.byd.com SPA exposes two distinct API namespaces:
|
|
12
7
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
8
|
+
// /portal/api/... → authenticated; every endpoint returns
|
|
9
|
+
// {"code":4001,"msg":"Token无效或已过期"}
|
|
10
|
+
// for unauthenticated requests.
|
|
11
|
+
// /portal/api/portal-api/... → ANONYMOUS public endpoints used by the SPA's
|
|
12
|
+
// home/experienced/campus landing flows. These
|
|
13
|
+
// return job listings, notices, materials, and
|
|
14
|
+
// recruit topics without any token.
|
|
17
15
|
//
|
|
18
|
-
//
|
|
19
|
-
// POST /portal/api/school/queryJobList → campus job list
|
|
20
|
-
// POST /portal/api/position/queryList → position list (also skiller/social)
|
|
21
|
-
// POST /portal/api/position/queryDetail → position detail
|
|
22
|
-
// POST /portal/api/other-info/notice/query-list → campus notices
|
|
23
|
-
// POST /portal/api/position/schedule/query-list → campus schedule / timeline
|
|
24
|
-
// GET /portal/api/siteInfo/faq → FAQ
|
|
25
|
-
// POST /portal/api/common/queryCodeTree → code dictionary
|
|
16
|
+
// The working anonymous search endpoint is:
|
|
26
17
|
//
|
|
27
|
-
//
|
|
28
|
-
// HTTP 200, body: {"code":4001,"timestamp":...,"msg":"Token无效或已过期: Not Authenticated"}
|
|
18
|
+
// POST /portal/api/portal-api/position/queryList
|
|
29
19
|
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// (POST /portal/api/account/login, then GET /portal/api/account/user-info).
|
|
33
|
-
// There is NO public/anonymous browsing API — even the FAQ and code-tree
|
|
34
|
-
// endpoints are gated behind a valid token.
|
|
20
|
+
// Required headers: a normal Chrome User-Agent, Content-Type application/json,
|
|
21
|
+
// a job.byd.com Referer, and `lang: en_US` (vivo accepts both en_US and zh_CN).
|
|
35
22
|
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
23
|
+
// Body shape:
|
|
24
|
+
// {
|
|
25
|
+
// positionTypeArr: [], // 职位类型 codes
|
|
26
|
+
// positionProvinceArr: [], // 省 codes
|
|
27
|
+
// positionCityArr: [], // 市 codes
|
|
28
|
+
// positionOrgArr: [], // 事业群 codes
|
|
29
|
+
// vagueCondition: "", // free-text keyword (matches title)
|
|
30
|
+
// searchType: 1, // 1 = title search
|
|
31
|
+
// zpType: "00251", // 招聘类型 — see table below
|
|
32
|
+
// pageNum: 0, // zero-based
|
|
33
|
+
// pageSize: 20
|
|
34
|
+
// }
|
|
44
35
|
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
36
|
+
// `zpType` controls the recruit channel:
|
|
37
|
+
// "00251" 社招 (Experienced; 1647+ live postings)
|
|
38
|
+
// "00252" 技师 (Technician — empty as of probe)
|
|
39
|
+
// "00253" 操作工 (Operator / blue-collar — empty as of probe)
|
|
40
|
+
// (Campus 校招 listings live behind a separate `school/*` flow that is fully
|
|
41
|
+
// auth-gated; the public anon channel exposes social hire only.)
|
|
51
42
|
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
43
|
+
// Response envelope: {"code":0, "data":{"total":N, "data":[...]}}.
|
|
44
|
+
//
|
|
45
|
+
// Endpoint inventory (anonymous):
|
|
46
|
+
// POST /portal/api/portal-api/position/queryList → paginated jobs
|
|
47
|
+
// GET /portal/api/portal-api/material/getMaterial?ids=… → site materials
|
|
48
|
+
// POST /portal/api/portal-api/other-info/notice/query-list → notices
|
|
49
|
+
// POST /portal/api/portal-api/other-info/resource/query-list→ downloadables
|
|
50
|
+
// GET /portal/api/portal-api/common/queryCodeTree?ids=… → filter taxonomy
|
|
51
|
+
// POST /portal/api/portal-api/common/queryDeptTree → org tree
|
|
52
|
+
// POST /portal/api/portal-api/Recruitment/getMessageList → marketing msgs
|
|
53
|
+
// GET /portal/api/portal-api/resumeSend/school-topic/info?zpNature=…
|
|
54
|
+
// → campus topics
|
|
62
55
|
//
|
|
63
56
|
// ============================================================
|
|
64
57
|
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
65
58
|
export { checkResume };
|
|
66
59
|
const SOURCE = "job.byd.com";
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
const API_ROOT = "https://job.byd.com";
|
|
61
|
+
const SITE_ROOT = "https://job.byd.com/portal/pc/";
|
|
62
|
+
const DETAIL_PAGE = (id) => `https://job.byd.com/portal/pc/#/social/detail?positionCode=${encodeURIComponent(id)}`;
|
|
63
|
+
const DEFAULT_HEADERS = {
|
|
64
|
+
"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",
|
|
65
|
+
Accept: "application/json, text/plain, */*",
|
|
66
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
67
|
+
Referer: SITE_ROOT,
|
|
68
|
+
Origin: API_ROOT,
|
|
69
|
+
lang: "zh_CN",
|
|
70
|
+
};
|
|
71
|
+
async function call(method, path, opts = {}) {
|
|
72
|
+
let url = `${API_ROOT}${path}`;
|
|
73
|
+
if (opts.query) {
|
|
74
|
+
const params = new URLSearchParams();
|
|
75
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
76
|
+
if (v !== undefined && v !== "")
|
|
77
|
+
params.set(k, String(v));
|
|
78
|
+
}
|
|
79
|
+
const qs = params.toString();
|
|
80
|
+
if (qs)
|
|
81
|
+
url += (path.includes("?") ? "&" : "?") + qs;
|
|
82
|
+
}
|
|
83
|
+
const headers = { ...DEFAULT_HEADERS };
|
|
84
|
+
let body;
|
|
85
|
+
if (opts.body !== undefined) {
|
|
86
|
+
body = JSON.stringify(opts.body);
|
|
87
|
+
headers["Content-Type"] = "application/json;charset=UTF-8";
|
|
88
|
+
}
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await fetch(url, { method, headers, body });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
101
|
+
}
|
|
102
|
+
let payload;
|
|
103
|
+
try {
|
|
104
|
+
payload = (await response.json());
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
108
|
+
}
|
|
74
109
|
return {
|
|
75
|
-
ok:
|
|
76
|
-
|
|
77
|
-
message:
|
|
78
|
-
campus_page: CAMPUS_PAGE,
|
|
79
|
-
positions: [],
|
|
110
|
+
ok: payload.code === 0,
|
|
111
|
+
data: payload.data,
|
|
112
|
+
message: payload.msg || payload.message || (payload.code === 0 ? "ok" : "upstream error"),
|
|
80
113
|
};
|
|
81
114
|
}
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
function summarize(item) {
|
|
116
|
+
const id = String(item.positionCode ?? item.id ?? "");
|
|
117
|
+
const city = [item.province, item.city].filter(Boolean).join("·");
|
|
84
118
|
return {
|
|
85
|
-
|
|
119
|
+
post_id: id,
|
|
120
|
+
title: (item.positionName ?? "").trim(),
|
|
121
|
+
project: (item.fatherOrgAliasName ?? item.fatherOrgName ?? "").trim(),
|
|
122
|
+
recruit_label: "社招",
|
|
123
|
+
bgs: (item.orgAliasName ?? item.orgName ?? "").trim(),
|
|
124
|
+
work_cities: city,
|
|
125
|
+
apply_url: id ? DETAIL_PAGE(id) : SITE_ROOT,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ---------- searchPositions ----------
|
|
129
|
+
export async function searchPositions(opts = {}) {
|
|
130
|
+
const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 20));
|
|
131
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
132
|
+
const body = {
|
|
133
|
+
positionTypeArr: opts.positionTypeIds ?? [],
|
|
134
|
+
positionProvinceArr: opts.provinceCodes ?? [],
|
|
135
|
+
positionCityArr: opts.cityCodes ?? [],
|
|
136
|
+
positionOrgArr: opts.orgCodes ?? [],
|
|
137
|
+
vagueCondition: (opts.keyword ?? "").trim().slice(0, 60),
|
|
138
|
+
searchType: 1,
|
|
139
|
+
zpType: opts.zpType ?? "00251",
|
|
140
|
+
pageNum: page - 1, // BYD uses 0-based
|
|
141
|
+
pageSize,
|
|
142
|
+
};
|
|
143
|
+
const r = await call("POST", "/portal/api/portal-api/position/queryList", { body });
|
|
144
|
+
if (!r.ok || !r.data) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
source: SOURCE,
|
|
148
|
+
message: r.message,
|
|
149
|
+
query: body,
|
|
150
|
+
positions: [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const rows = r.data.data ?? [];
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
86
156
|
source: SOURCE,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
157
|
+
query: body,
|
|
158
|
+
page,
|
|
159
|
+
page_size: pageSize,
|
|
160
|
+
total: r.data.total ?? rows.length,
|
|
161
|
+
positions: rows.map(summarize),
|
|
91
162
|
};
|
|
92
163
|
}
|
|
93
|
-
//
|
|
94
|
-
export async function
|
|
95
|
-
|
|
164
|
+
// ---------- fetchAllPositions ----------
|
|
165
|
+
export async function fetchAllPositions(opts = {}) {
|
|
166
|
+
const pageSize = Math.max(1, Math.min(50, opts.pageSize ?? 50));
|
|
167
|
+
const maxPages = Math.max(1, opts.maxPages ?? 40);
|
|
168
|
+
const bucket = [];
|
|
169
|
+
let total;
|
|
170
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
171
|
+
const r = await searchPositions({
|
|
172
|
+
keyword: opts.keyword,
|
|
173
|
+
page,
|
|
174
|
+
pageSize,
|
|
175
|
+
zpType: opts.zpType,
|
|
176
|
+
});
|
|
177
|
+
if (!r.ok) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
source: SOURCE,
|
|
181
|
+
message: r.message,
|
|
182
|
+
total: 0,
|
|
183
|
+
fetched: bucket.length,
|
|
184
|
+
positions: bucket,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (total === undefined)
|
|
188
|
+
total = r.total;
|
|
189
|
+
if (!r.positions.length)
|
|
190
|
+
break;
|
|
191
|
+
bucket.push(...r.positions);
|
|
192
|
+
if (total !== undefined && bucket.length >= total)
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
source: SOURCE,
|
|
198
|
+
total: total ?? bucket.length,
|
|
199
|
+
fetched: bucket.length,
|
|
200
|
+
positions: bucket,
|
|
201
|
+
};
|
|
96
202
|
}
|
|
97
|
-
//
|
|
98
|
-
|
|
203
|
+
// ---------- fetchPositionDetail ----------
|
|
204
|
+
//
|
|
205
|
+
// The detail endpoint /portal/api/position/queryDetail requires auth, but the
|
|
206
|
+
// public list endpoint returns enough info per row that we surface a "row+link"
|
|
207
|
+
// detail instead of a fully gated 4001 stub.
|
|
208
|
+
export async function fetchPositionDetail(postId) {
|
|
209
|
+
const id = (postId ?? "").trim();
|
|
210
|
+
if (!id)
|
|
211
|
+
return { ok: false, source: SOURCE, message: "post_id is required", post_id: id };
|
|
212
|
+
// Page through the social-hire list looking for the row. This is the best we
|
|
213
|
+
// can do without a logged-in JWT; in practice the row is usually within the
|
|
214
|
+
// first few hundred records and matchResume already pages through the full
|
|
215
|
+
// catalogue.
|
|
216
|
+
const r = await searchPositions({ keyword: id, pageSize: 5 });
|
|
217
|
+
const hit = r.ok ? r.positions.find((p) => p.post_id === id) : undefined;
|
|
218
|
+
if (!hit) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
source: SOURCE,
|
|
222
|
+
message: "Position detail endpoint (POST /portal/api/position/queryDetail) requires a logged-in JWT. " +
|
|
223
|
+
"Public anon API can list positions but not return per-position bodies.",
|
|
224
|
+
post_id: id,
|
|
225
|
+
apply_url: DETAIL_PAGE(id),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
99
228
|
return {
|
|
100
|
-
ok:
|
|
229
|
+
ok: true,
|
|
101
230
|
source: SOURCE,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
],
|
|
231
|
+
post_id: hit.post_id,
|
|
232
|
+
title: hit.title,
|
|
233
|
+
project: hit.project,
|
|
234
|
+
bgs: hit.bgs,
|
|
235
|
+
recruit_label: hit.recruit_label,
|
|
236
|
+
work_cities: hit.work_cities,
|
|
237
|
+
description: "",
|
|
238
|
+
requirements: "",
|
|
239
|
+
apply_url: hit.apply_url,
|
|
240
|
+
note: "Description and requirements are not available without authentication; " +
|
|
241
|
+
"visit apply_url for the full posting after login.",
|
|
114
242
|
};
|
|
115
243
|
}
|
|
116
|
-
//
|
|
117
|
-
export async function
|
|
244
|
+
// ---------- fetchDictionaries ----------
|
|
245
|
+
export async function fetchDictionaries() {
|
|
246
|
+
const [codeTree, deptTree] = await Promise.all([
|
|
247
|
+
call("GET", "/portal/api/portal-api/common/queryCodeTree", {
|
|
248
|
+
query: { ids: "0009,0030" },
|
|
249
|
+
}),
|
|
250
|
+
call("POST", "/portal/api/portal-api/common/queryDeptTree", { body: {} }),
|
|
251
|
+
]);
|
|
118
252
|
return {
|
|
119
|
-
ok:
|
|
253
|
+
ok: codeTree.ok || deptTree.ok,
|
|
120
254
|
source: SOURCE,
|
|
121
|
-
|
|
255
|
+
api_host: API_ROOT,
|
|
256
|
+
verified_at: new Date().toISOString(),
|
|
257
|
+
code_tree: codeTree.data ?? null,
|
|
258
|
+
dept_tree: deptTree.data ?? null,
|
|
259
|
+
zp_types: {
|
|
260
|
+
"00251": "社招 (Experienced)",
|
|
261
|
+
"00252": "技师 (Technician)",
|
|
262
|
+
"00253": "操作工 (Operator)",
|
|
263
|
+
},
|
|
264
|
+
note: "Campus (校招) jobs are not exposed by the anon public API — the school/* " +
|
|
265
|
+
"endpoints all require a JWT bearer token.",
|
|
122
266
|
};
|
|
123
267
|
}
|
|
124
|
-
|
|
125
|
-
|
|
268
|
+
export async function listNotices() {
|
|
269
|
+
const r = await call("POST", "/portal/api/portal-api/other-info/notice/query-list", { body: { pageNum: 0, pageSize: 30 } });
|
|
270
|
+
if (!r.ok)
|
|
271
|
+
return { ok: false, source: SOURCE, message: r.message, notices: [] };
|
|
272
|
+
const items = r.data?.data ?? r.data?.list ?? [];
|
|
126
273
|
return {
|
|
127
|
-
ok:
|
|
274
|
+
ok: true,
|
|
128
275
|
source: SOURCE,
|
|
129
|
-
|
|
276
|
+
count: items.length,
|
|
277
|
+
notices: items.map((n) => ({
|
|
278
|
+
id: String(n.id ?? ""),
|
|
279
|
+
title: n.title ?? n.noticeTitle ?? "",
|
|
280
|
+
publish_time: n.publishTime ?? n.createTime ?? "",
|
|
281
|
+
tag: n.noticeType ?? "",
|
|
282
|
+
detail_url: SITE_ROOT,
|
|
283
|
+
})),
|
|
130
284
|
};
|
|
131
285
|
}
|
|
132
|
-
|
|
133
|
-
|
|
286
|
+
export async function getNotice(noticeId) {
|
|
287
|
+
const id = (noticeId ?? "").trim();
|
|
288
|
+
if (!id)
|
|
289
|
+
return { ok: false, source: SOURCE, message: "notice_id is required" };
|
|
290
|
+
const all = await listNotices();
|
|
291
|
+
if (!all.ok)
|
|
292
|
+
return { ok: false, source: SOURCE, message: all.message };
|
|
293
|
+
const hit = all.notices.find((n) => n.id === id);
|
|
294
|
+
if (!hit)
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
source: SOURCE,
|
|
298
|
+
message: `notice ${id} not in the latest /notice/query-list page`,
|
|
299
|
+
};
|
|
300
|
+
return { ok: true, source: SOURCE, ...hit, content_html: "" };
|
|
301
|
+
}
|
|
302
|
+
export async function findNoticesByQuestion(question, opts = {}) {
|
|
303
|
+
const listing = await listNotices();
|
|
304
|
+
if (!listing.ok)
|
|
305
|
+
return { ok: false, source: SOURCE, message: listing.message, matches: [] };
|
|
306
|
+
const tokens = [];
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
const text = (question ?? "").trim();
|
|
309
|
+
for (const m of text.match(/[A-Za-z0-9]{2,}/g) ?? []) {
|
|
310
|
+
const k = m.toLowerCase();
|
|
311
|
+
if (!seen.has(k)) {
|
|
312
|
+
seen.add(k);
|
|
313
|
+
tokens.push(k);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const run of text.match(/[一-鿿]+/g) ?? []) {
|
|
317
|
+
for (let i = 0; i < run.length - 1; i++) {
|
|
318
|
+
const bigram = run.slice(i, i + 2);
|
|
319
|
+
if (!seen.has(bigram)) {
|
|
320
|
+
seen.add(bigram);
|
|
321
|
+
tokens.push(bigram);
|
|
322
|
+
}
|
|
323
|
+
if (tokens.length >= 40)
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const topK = Math.max(1, opts.topK ?? 3);
|
|
328
|
+
const scored = listing.notices
|
|
329
|
+
.map((n) => {
|
|
330
|
+
const hay = `${n.title} ${n.tag}`.toLowerCase();
|
|
331
|
+
const score = tokens.filter((t) => hay.includes(t)).length;
|
|
332
|
+
return { score, notice: n };
|
|
333
|
+
})
|
|
334
|
+
.filter((s) => s.score > 0)
|
|
335
|
+
.sort((a, b) => b.score - a.score);
|
|
134
336
|
return {
|
|
135
|
-
ok:
|
|
337
|
+
ok: true,
|
|
136
338
|
source: SOURCE,
|
|
137
|
-
|
|
339
|
+
question,
|
|
340
|
+
question_time: opts.questionTime,
|
|
341
|
+
matched_tokens: tokens,
|
|
342
|
+
matches: scored.slice(0, topK).map((s) => ({ ...s.notice, excerpt: "" })),
|
|
138
343
|
};
|
|
139
344
|
}
|
|
140
|
-
//
|
|
141
|
-
// Resume matching is best-effort using extractResumeSignals/scoreOverlap from
|
|
142
|
-
// tencent.ts, but since the position listing API is gated, we can only return
|
|
143
|
-
// a stub with the extracted signals and a pointer to the campus page.
|
|
345
|
+
// ---------- matchResume ----------
|
|
144
346
|
export async function matchResume(text, opts = {}) {
|
|
145
|
-
// Extract signals so the caller knows what was parsed from the resume
|
|
146
347
|
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
147
|
-
|
|
348
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
349
|
+
const candidates = Math.max(topN, opts.candidates ?? 200);
|
|
350
|
+
const all = await fetchAllPositions({
|
|
351
|
+
pageSize: 50,
|
|
352
|
+
maxPages: Math.ceil(candidates / 50),
|
|
353
|
+
});
|
|
354
|
+
if (!all.ok) {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
source: SOURCE,
|
|
358
|
+
message: all.message,
|
|
359
|
+
extracted_terms: terms,
|
|
360
|
+
city_preferences: cities,
|
|
361
|
+
matches: [],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const scored = [];
|
|
365
|
+
for (const p of all.positions) {
|
|
366
|
+
const haystack = `${p.title} ${p.project} ${p.bgs} ${p.work_cities}`;
|
|
367
|
+
const score = scoreOverlap(haystack, terms, cities).score;
|
|
368
|
+
if (score > 0)
|
|
369
|
+
scored.push({ score, position: p });
|
|
370
|
+
}
|
|
371
|
+
scored.sort((a, b) => b.score - a.score);
|
|
148
372
|
return {
|
|
149
|
-
ok:
|
|
373
|
+
ok: true,
|
|
150
374
|
source: SOURCE,
|
|
151
|
-
message: "BYD: cannot search positions — API requires authentication. " +
|
|
152
|
-
`Extracted resume signals: [${terms.slice(0, 10).join(", ")}]. ` +
|
|
153
|
-
"Visit the campus page to search manually.",
|
|
154
|
-
campus_page: CAMPUS_PAGE,
|
|
155
375
|
extracted_terms: terms,
|
|
156
376
|
city_preferences: cities,
|
|
377
|
+
candidate_pool: all.positions.length,
|
|
378
|
+
matches: scored.slice(0, topN).map((s) => s.position),
|
|
157
379
|
};
|
|
158
380
|
}
|
|
159
|
-
// ---- re-export helpers so the tencent resume signals are accessible ----
|
|
160
381
|
export { extractResumeSignals, scoreOverlap };
|