@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/adapter.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Canonical contract every company adapter must satisfy.
|
|
2
|
+
//
|
|
3
|
+
// Previously the dispatcher leaned on `type CompanyAdapter = typeof tencent`
|
|
4
|
+
// plus `as unknown as CompanyAdapter` casts on every entry in the ADAPTERS
|
|
5
|
+
// map. That silenced every shape mismatch — if an adapter's return value
|
|
6
|
+
// drifted, TypeScript was happy and the bug surfaced at runtime.
|
|
7
|
+
//
|
|
8
|
+
// This module defines an explicit method-signature interface so adapters
|
|
9
|
+
// can be wired with `satisfies Record<string, CompanyAdapter>` and any
|
|
10
|
+
// future drift becomes a compile error.
|
|
11
|
+
//
|
|
12
|
+
// The result types are intentionally permissive (`Promise<unknown>`-shaped):
|
|
13
|
+
// adapter-specific success payloads have rich, per-company keys (Tencent has
|
|
14
|
+
// recruitment fields Feishu doesn't, etc.) that we don't want to flatten here.
|
|
15
|
+
// The contract is "this method exists and is async"; the per-company JSON
|
|
16
|
+
// shape is documented in each adapter's source.
|
|
17
|
+
export {};
|
package/dist/agibot.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// Thin client for 智元机器人 (Agibot / AGIBOT Innovation) campus & social recruiting.
|
|
2
|
+
//
|
|
3
|
+
// ============================================================
|
|
4
|
+
// API DISCOVERY (probed 2026-05)
|
|
5
|
+
//
|
|
6
|
+
// Infrastructure:
|
|
7
|
+
// The corporate site www.agibot.com links to the Lark Hire (飞书招聘) SaaS portal:
|
|
8
|
+
// https://agirobot.jobs.feishu.cn/
|
|
9
|
+
// which hosts four separate recruiting portals:
|
|
10
|
+
// /index — 高端岗位 (senior / executive) website_id: 7314554416651995443
|
|
11
|
+
// /socialrecruitment — 社会招聘 (social / experienced) website_id: 7212468858346785082
|
|
12
|
+
// /campusrecruitment — 校园招聘 (campus / new-grad) website_id: 7212468542670309689
|
|
13
|
+
// /internrecruitment — 实习招聘 (intern)
|
|
14
|
+
//
|
|
15
|
+
// Dead ends probed:
|
|
16
|
+
// https://www.zhiyuan-robot.com/careers — returns 404 (redirects to agibot.com.cn)
|
|
17
|
+
// https://careers.agibot.com/ — connection refused / no server
|
|
18
|
+
// https://hr.agibot.com/ — connection refused / no server
|
|
19
|
+
// Moka orgId 145143 — auth-gated, not publicly accessible
|
|
20
|
+
//
|
|
21
|
+
// WORKING APPROACH — Lark Hire SaaS JSON API:
|
|
22
|
+
// All four portals share a single unauthenticated POST endpoint:
|
|
23
|
+
// POST https://agirobot.jobs.feishu.cn/api/v1/search/job/posts
|
|
24
|
+
// The API returns all 661+ positions (social + campus + intern combined) without
|
|
25
|
+
// any portal-type filter; the Referer header does not affect which posts are returned.
|
|
26
|
+
//
|
|
27
|
+
// Discovered by reverse-engineering the webpack bundle
|
|
28
|
+
// lf-package-cn.feishucdn.com/…/saas-career/static/js/4026.f23f1edc.js:
|
|
29
|
+
// Module 59235 sets eW = "" (relative host), so i = "/api/v1".
|
|
30
|
+
// getPositionList = i + "/search/job/posts" (POST, page_index + page_size)
|
|
31
|
+
// getPositionDetail = i + "/job/posts/" + id (GET)
|
|
32
|
+
// getPositionFilter = i + "/config/job/filters/" + path (GET)
|
|
33
|
+
//
|
|
34
|
+
// API call details (POST /api/v1/search/job/posts):
|
|
35
|
+
// Request body: { keyword, page_size, page_index, ... }
|
|
36
|
+
// Response: { code:0, data:{ job_post_list:[...], count:<int> } }
|
|
37
|
+
// count: 661 (all portals combined, 2026-05 snapshot)
|
|
38
|
+
//
|
|
39
|
+
// Note: department_id is always null in public search results — no BG/部门 field available.
|
|
40
|
+
//
|
|
41
|
+
// ============================================================
|
|
42
|
+
// PositionSummary field mapping (canonical keys — matches all other adapters):
|
|
43
|
+
// post_id — item.id (string)
|
|
44
|
+
// title — item.title
|
|
45
|
+
// project — item.job_category.name (e.g. "研发" / "智能制造 / 工业互联网")
|
|
46
|
+
// recruit_label — item.recruit_type.name + " / " + item.recruit_type.parent.name
|
|
47
|
+
// (e.g. "全职 / 社招" / "实习 / 校招")
|
|
48
|
+
// bgs — "" (department_id is always null in public API)
|
|
49
|
+
// work_cities — city_list[].name joined with " / " (e.g. "上海" / "北京 / 上海")
|
|
50
|
+
// apply_url — https://agirobot.jobs.feishu.cn/socialrecruitment/position/{id}/detail
|
|
51
|
+
// ============================================================
|
|
52
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
53
|
+
export { checkResume };
|
|
54
|
+
const SOURCE = "agirobot.jobs.feishu.cn";
|
|
55
|
+
const API_ROOT = "https://agirobot.jobs.feishu.cn/api/v1";
|
|
56
|
+
const PORTAL_BASE = "https://agirobot.jobs.feishu.cn";
|
|
57
|
+
const LIST_PAGE = `${PORTAL_BASE}/socialrecruitment`;
|
|
58
|
+
const DETAIL_URL = (id) => `${PORTAL_BASE}/socialrecruitment/position/${encodeURIComponent(id)}/detail`;
|
|
59
|
+
const DEFAULT_HEADERS = {
|
|
60
|
+
"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",
|
|
61
|
+
Accept: "application/json, text/plain, */*",
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
Origin: PORTAL_BASE,
|
|
64
|
+
Referer: LIST_PAGE,
|
|
65
|
+
};
|
|
66
|
+
async function call(path, body) {
|
|
67
|
+
const url = `${API_ROOT}${path}`;
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetch(url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: DEFAULT_HEADERS,
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
84
|
+
}
|
|
85
|
+
let payload;
|
|
86
|
+
try {
|
|
87
|
+
payload = (await response.json());
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
ok: payload.code === 0,
|
|
94
|
+
data: payload.data,
|
|
95
|
+
message: payload.message || (payload.code === 0 ? "ok" : "upstream error"),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function summarizePosition(item) {
|
|
99
|
+
const id = String(item.id ?? "");
|
|
100
|
+
// work_cities: prefer city_list for multi-city; fall back to city_info
|
|
101
|
+
const cityList = item.city_list ?? [];
|
|
102
|
+
let work_cities;
|
|
103
|
+
if (cityList.length >= 1) {
|
|
104
|
+
work_cities = cityList.map((c) => c.name ?? "").filter(Boolean).join(" / ");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
work_cities = item.city_info?.name ?? "";
|
|
108
|
+
}
|
|
109
|
+
// recruit_label: "全职 / 社招" or "实习 / 校招" style
|
|
110
|
+
const rt = item.recruit_type;
|
|
111
|
+
const rtName = rt?.name ?? "";
|
|
112
|
+
const rtParent = rt?.parent?.name ?? "";
|
|
113
|
+
const recruit_label = rtParent ? `${rtName} / ${rtParent}` : rtName;
|
|
114
|
+
return {
|
|
115
|
+
post_id: id,
|
|
116
|
+
title: item.title ?? "",
|
|
117
|
+
project: item.job_category?.name ?? "",
|
|
118
|
+
recruit_label,
|
|
119
|
+
bgs: "", // department_id is always null in public search results
|
|
120
|
+
work_cities,
|
|
121
|
+
apply_url: id ? DETAIL_URL(id) : LIST_PAGE,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// ---------- searchPositions ----------
|
|
125
|
+
export async function searchPositions(opts = {}) {
|
|
126
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
127
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
128
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
129
|
+
const payload = {
|
|
130
|
+
keyword,
|
|
131
|
+
page_size: pageSize,
|
|
132
|
+
page_index: page,
|
|
133
|
+
};
|
|
134
|
+
const response = await call("/search/job/posts", payload);
|
|
135
|
+
if (!response.ok || !response.data) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
message: response.message,
|
|
139
|
+
source: SOURCE,
|
|
140
|
+
query: payload,
|
|
141
|
+
positions: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const rows = response.data.job_post_list ?? [];
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
source: SOURCE,
|
|
148
|
+
query: payload,
|
|
149
|
+
page,
|
|
150
|
+
page_size: pageSize,
|
|
151
|
+
total: response.data.count ?? rows.length,
|
|
152
|
+
positions: rows.map(summarizePosition),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// ---------- fetchAllPositions ----------
|
|
156
|
+
export async function fetchAllPositions(opts = {}) {
|
|
157
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
|
|
158
|
+
const maxPages = Math.max(1, opts.maxPages ?? 10); // up to 1000 posts
|
|
159
|
+
const bucket = [];
|
|
160
|
+
let total;
|
|
161
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
162
|
+
const result = await searchPositions({ ...opts, page, pageSize });
|
|
163
|
+
if (!result.ok) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
message: result.message,
|
|
167
|
+
source: SOURCE,
|
|
168
|
+
fetched: bucket.length,
|
|
169
|
+
positions: bucket,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (total === undefined)
|
|
173
|
+
total = result.total;
|
|
174
|
+
if (!result.positions.length)
|
|
175
|
+
break;
|
|
176
|
+
bucket.push(...result.positions);
|
|
177
|
+
if (total !== undefined && bucket.length >= total)
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
source: SOURCE,
|
|
183
|
+
total: total ?? bucket.length,
|
|
184
|
+
fetched: bucket.length,
|
|
185
|
+
positions: bucket,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export async function fetchPositionDetail(postId) {
|
|
189
|
+
const id = (postId ?? "").trim();
|
|
190
|
+
if (!id) {
|
|
191
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
192
|
+
}
|
|
193
|
+
const url = `${API_ROOT}/job/posts/${encodeURIComponent(id)}`;
|
|
194
|
+
let response;
|
|
195
|
+
try {
|
|
196
|
+
response = await fetch(url, {
|
|
197
|
+
method: "GET",
|
|
198
|
+
headers: { ...DEFAULT_HEADERS, Referer: DETAIL_URL(id) },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
source: SOURCE,
|
|
205
|
+
post_id: id,
|
|
206
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
source: SOURCE,
|
|
213
|
+
post_id: id,
|
|
214
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
let payload;
|
|
218
|
+
try {
|
|
219
|
+
payload = (await response.json());
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
source: SOURCE,
|
|
225
|
+
post_id: id,
|
|
226
|
+
message: `bad JSON: ${err instanceof Error ? err.message : err}`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (payload.code !== 0 || !payload.data?.job_post_detail) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
source: SOURCE,
|
|
233
|
+
post_id: id,
|
|
234
|
+
message: payload.message ?? "upstream error",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const d = payload.data.job_post_detail;
|
|
238
|
+
const cities = (d.city_list ?? []).map((c) => c.name ?? "").filter(Boolean);
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
source: SOURCE,
|
|
242
|
+
post_id: String(d.id ?? id),
|
|
243
|
+
title: d.title ?? "",
|
|
244
|
+
direction: d.sub_title ?? "",
|
|
245
|
+
project: d.job_category?.name ?? "",
|
|
246
|
+
recruit_label: d.recruit_type?.name ?? "",
|
|
247
|
+
description: d.description ?? "",
|
|
248
|
+
requirements: d.requirement ?? "",
|
|
249
|
+
work_cities: cities,
|
|
250
|
+
apply_url: DETAIL_URL(String(d.id ?? id)),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
export async function fetchDictionaries() {
|
|
254
|
+
const url = `${API_ROOT}/config/job/filters/index`;
|
|
255
|
+
let response;
|
|
256
|
+
try {
|
|
257
|
+
response = await fetch(url, { method: "GET", headers: DEFAULT_HEADERS });
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
source: SOURCE,
|
|
263
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
source: SOURCE,
|
|
270
|
+
message: `HTTP ${response.status}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
let payload;
|
|
274
|
+
try {
|
|
275
|
+
payload = (await response.json());
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
source: SOURCE,
|
|
281
|
+
message: `bad JSON: ${err instanceof Error ? err.message : err}`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (payload.code !== 0 || !payload.data) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
source: SOURCE,
|
|
288
|
+
message: payload.message ?? "upstream error",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const d = payload.data;
|
|
292
|
+
const jobCategories = (d.job_type_list ?? []).map((c) => ({
|
|
293
|
+
id: c.id ?? "",
|
|
294
|
+
name: c.name ?? "",
|
|
295
|
+
en_name: c.en_name ?? "",
|
|
296
|
+
depth: c.depth ?? 1,
|
|
297
|
+
parent_id: c.parent?.id ?? null,
|
|
298
|
+
}));
|
|
299
|
+
const cities = (d.city_list ?? []).map((c) => ({
|
|
300
|
+
code: c.code ?? "",
|
|
301
|
+
name: c.name ?? "",
|
|
302
|
+
en_name: c.en_name ?? "",
|
|
303
|
+
}));
|
|
304
|
+
return {
|
|
305
|
+
ok: true,
|
|
306
|
+
source: SOURCE,
|
|
307
|
+
portal: PORTAL_BASE,
|
|
308
|
+
portals: {
|
|
309
|
+
index: `${PORTAL_BASE}/index`,
|
|
310
|
+
social: `${PORTAL_BASE}/socialrecruitment`,
|
|
311
|
+
campus: `${PORTAL_BASE}/campusrecruitment`,
|
|
312
|
+
intern: `${PORTAL_BASE}/internrecruitment`,
|
|
313
|
+
},
|
|
314
|
+
note: "All four Agibot recruiting portals share a single public API endpoint at " +
|
|
315
|
+
"/api/v1/search/job/posts. department_id is always null in public results " +
|
|
316
|
+
"(no BG/部门 exposed). Total ~661 positions across social + campus + intern.",
|
|
317
|
+
jobCategories,
|
|
318
|
+
cities,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// ---------- notices (no public endpoint) ----------
|
|
322
|
+
const NOTICES_STUB = {
|
|
323
|
+
ok: false,
|
|
324
|
+
source: SOURCE,
|
|
325
|
+
message: "Agibot: no public notices or announcement endpoint available",
|
|
326
|
+
};
|
|
327
|
+
export async function listNotices() {
|
|
328
|
+
return NOTICES_STUB;
|
|
329
|
+
}
|
|
330
|
+
export async function getNotice(_id) {
|
|
331
|
+
return NOTICES_STUB;
|
|
332
|
+
}
|
|
333
|
+
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
334
|
+
return NOTICES_STUB;
|
|
335
|
+
}
|
|
336
|
+
// ---------- matchResume ----------
|
|
337
|
+
export async function matchResume(text, opts = {}) {
|
|
338
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
339
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
340
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
341
|
+
if (!terms.length) {
|
|
342
|
+
return {
|
|
343
|
+
ok: false,
|
|
344
|
+
source: SOURCE,
|
|
345
|
+
message: "could not extract any technical signals from the text",
|
|
346
|
+
preview: (text ?? "").slice(0, 120),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
350
|
+
const list = await searchPositions({ keyword, page: 1, pageSize: 100 });
|
|
351
|
+
if (!list.ok) {
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
source: SOURCE,
|
|
355
|
+
message: list.message,
|
|
356
|
+
positions: [],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const scored = [];
|
|
360
|
+
for (const p of list.positions) {
|
|
361
|
+
const blob = [p.title, p.project, p.recruit_label, p.work_cities, p.post_id].join(" ");
|
|
362
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
363
|
+
if (score > 0)
|
|
364
|
+
scored.push({ score, position: p, reasons });
|
|
365
|
+
}
|
|
366
|
+
scored.sort((a, b) => b.score - a.score);
|
|
367
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
368
|
+
if (!shortlist.length) {
|
|
369
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({
|
|
370
|
+
score: 0,
|
|
371
|
+
position,
|
|
372
|
+
reasons: [],
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
376
|
+
const mr = s.reasons.length > 0
|
|
377
|
+
? s.reasons.slice(0, 5)
|
|
378
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
379
|
+
return { ...s.position, match_reasons: mr };
|
|
380
|
+
});
|
|
381
|
+
return {
|
|
382
|
+
ok: true,
|
|
383
|
+
source: SOURCE,
|
|
384
|
+
extracted_terms: terms,
|
|
385
|
+
city_preferences: cities,
|
|
386
|
+
matches,
|
|
387
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
388
|
+
"The only authority on selection is HR.",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
392
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
393
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
394
|
+
host: "agirobot.jobs.feishu.cn",
|
|
395
|
+
source: "agirobot.jobs.feishu.cn",
|
|
396
|
+
channel: "campus",
|
|
397
|
+
applyUrlPrefix: "https://agirobot.jobs.feishu.cn/campus/position",
|
|
398
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
399
|
+
});
|