job-pro 0.7.2 → 0.7.4
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 +21 -393
- package/dist/cdp.js +191 -0
- package/dist/deepseek.js +23 -386
- package/dist/galaxyuniversal.js +22 -388
- package/dist/geely.js +29 -57
- package/dist/index.js +1 -1
- package/dist/lilith.js +250 -135
- package/dist/megvii.js +25 -455
- package/dist/moka.js +412 -0
- package/dist/moonshot.js +22 -395
- package/dist/stepfun.js +22 -381
- package/package.json +5 -3
package/dist/lilith.js
CHANGED
|
@@ -1,175 +1,290 @@
|
|
|
1
|
-
// 莉莉丝游戏 (Lilith Games)
|
|
1
|
+
// 莉莉丝游戏 (Lilith Games) careers adapter — Feishu portal_type=6 via CDP.
|
|
2
2
|
//
|
|
3
3
|
// ============================================================
|
|
4
|
-
// API DISCOVERY (probed 2026-05-
|
|
4
|
+
// API DISCOVERY (probed 2026-05-16)
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
//
|
|
6
|
+
// Lilith's careers feed is hosted at `lilithgames.jobs.feishu.cn` (Feishu
|
|
7
|
+
//招聘 / ATSX). It looks like a standard Feishu tenant on the surface, BUT
|
|
8
|
+
// the `/api/v1/search/job/posts` POST is rejected with `HTTP 405` from
|
|
9
|
+
// ByteDance Tengine for any anonymous caller — Lilith's tenant is one of
|
|
10
|
+
// the few that requires the in-browser `_signature` anti-bot token. The
|
|
11
|
+
// signature is computed by `verifycenter` (`lf-cdn-tos.bytescm.com/.../rc-verifycenter`)
|
|
12
|
+
// at runtime and appended to the URL query string + headers; it's
|
|
13
|
+
// session-bound and short-lived.
|
|
9
14
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
15
|
+
// Reverse-engineering verifycenter is non-trivial. We work around it by
|
|
16
|
+
// using `puppeteer-core` to drive the user's real Chrome (see cli/src/cdp.ts):
|
|
17
|
+
// navigate to the careers page, wait for the SPA's own `search/job/posts`
|
|
18
|
+
// XHR, and read the JSON straight off the network response. Same data
|
|
19
|
+
// shape as `cli/src/feishu.ts`, just sourced through a real browser.
|
|
13
20
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// ============================================================
|
|
19
|
-
// Feishu Recruitment API (reverse-engineered from the saas-career JS bundle,
|
|
20
|
-
// chunk 4026.f23f1edc.js, fetched 2026-05-14)
|
|
21
|
-
//
|
|
22
|
-
// POST https://lilithgames.jobs.feishu.cn/api/v1/search/job/posts
|
|
23
|
-
// Headers: Content-Type: application/json
|
|
24
|
-
// Referer: https://lilithgames.jobs.feishu.cn/career
|
|
25
|
-
// Payload:
|
|
26
|
-
// {
|
|
27
|
-
// keyword: string, // search term
|
|
28
|
-
// limit: number, // page size
|
|
29
|
-
// offset: number, // (current-1)*limit
|
|
30
|
-
// job_hot_flag: undefined,
|
|
31
|
-
// portal_type: 6, // SaasCareer portal type
|
|
32
|
-
// job_category_id_list: string[], // category filter
|
|
33
|
-
// tag_id_list: string[],
|
|
34
|
-
// location_code_list: string[], // CT_11=北京, CT_125=上海, etc.
|
|
35
|
-
// subject_id_list: string[],
|
|
36
|
-
// recruitment_id_list: string[],
|
|
37
|
-
// job_function_id_list: string[],
|
|
38
|
-
// storefront_id_list: string[],
|
|
39
|
-
// }
|
|
40
|
-
// Response: { code: 0, data: { job_post_list: RawJobPost[], count: number }, message: "ok" }
|
|
41
|
-
//
|
|
42
|
-
// Raw job post field mapping (from N() mapper in bundle):
|
|
43
|
-
// id → post_id
|
|
44
|
-
// title → title
|
|
45
|
-
// job_category.name → project
|
|
46
|
-
// recruit_type.name → recruit_label
|
|
47
|
-
// department_info → bgs (Lilith does not expose BG in the public payload)
|
|
48
|
-
// city_info.name → work_cities (or city_info_list_for_delivery for multi-city)
|
|
49
|
-
//
|
|
50
|
-
// ============================================================
|
|
51
|
-
// NETWORK ACCESSIBILITY (probed 2026-05-14)
|
|
52
|
-
//
|
|
53
|
-
// lilithgames.jobs.feishu.cn resolves to 198.18.1.152 (IANA RFC 2544
|
|
54
|
-
// benchmarking range). All feishu.cn/larksuite.com subdomains resolve
|
|
55
|
-
// into 198.18.0.0/15 from the current environment, indicating a
|
|
56
|
-
// DNS-level network block. TLS connects but every HTTP path (including
|
|
57
|
-
// /api/v1/search/job/posts) is answered by a ByteDance headhunter
|
|
58
|
-
// platform stub page rather than the Feishu Recruitment API.
|
|
59
|
-
// The Feishu API is structurally identical to ByteDance's campus API
|
|
60
|
-
// (same city-code format CT_XX, same payload shape, same response envelope)
|
|
61
|
-
// but is NOT callable without a network path that bypasses the block.
|
|
62
|
-
//
|
|
63
|
-
// VERDICT: API is fully discovered but unreachable from this environment.
|
|
64
|
-
// This adapter is an honest stub that returns ok:false with a clear
|
|
65
|
-
// message. The apply_url values point to the live portal.
|
|
66
|
-
//
|
|
67
|
-
// ============================================================
|
|
68
|
-
// PositionSummary field mapping (canonical keys, matches all other adapters)
|
|
69
|
-
// post_id — string job identifier
|
|
70
|
-
// title — position title
|
|
71
|
-
// project — job category (job_category.name)
|
|
72
|
-
// recruit_label — recruit type label (recruit_type.name)
|
|
73
|
-
// bgs — business group (not exposed in public API payload, always "")
|
|
74
|
-
// work_cities — work location (city_info.name)
|
|
75
|
-
// apply_url — deep link to the Feishu Recruitment job detail
|
|
76
|
-
// ============================================================
|
|
21
|
+
// Probed 2026-05-16: portal_type=6, channel id 7055353811552127239, default
|
|
22
|
+
// limit=10. The career page filters by `location_code_list` query string;
|
|
23
|
+
// we pass through search options the same way.
|
|
77
24
|
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
25
|
+
import { withPage } from "./cdp.js";
|
|
78
26
|
export { checkResume };
|
|
79
27
|
const SOURCE = "lilithgames.jobs.feishu.cn";
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// ---------- searchPositions ----------
|
|
91
|
-
export async function searchPositions(_opts = {}) {
|
|
28
|
+
const HOST = "https://lilithgames.jobs.feishu.cn";
|
|
29
|
+
const CAREER_PAGE = `${HOST}/career/`;
|
|
30
|
+
const DETAIL_PAGE = (id) => `${HOST}/career/${encodeURIComponent(id)}/detail`;
|
|
31
|
+
function summarize(item) {
|
|
32
|
+
const id = String(item.id ?? "");
|
|
33
|
+
const cityList = item.city_list ?? [];
|
|
34
|
+
const work_cities = cityList.length > 1
|
|
35
|
+
? cityList.map((c) => c.name ?? "").filter(Boolean).join(" / ")
|
|
36
|
+
: cityList[0]?.name ?? item.city_info?.name ?? "";
|
|
37
|
+
const project = item.job_category?.name ?? item.job_function?.name ?? "";
|
|
92
38
|
return {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
39
|
+
post_id: id,
|
|
40
|
+
title: item.title ?? "",
|
|
41
|
+
project,
|
|
42
|
+
recruit_label: item.recruit_type?.name ?? "",
|
|
43
|
+
bgs: "",
|
|
44
|
+
work_cities,
|
|
45
|
+
apply_url: id ? DETAIL_PAGE(id) : CAREER_PAGE,
|
|
98
46
|
};
|
|
99
47
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
apply_url: CAREER_PAGE,
|
|
107
|
-
fetched: 0,
|
|
108
|
-
positions: [],
|
|
109
|
-
};
|
|
48
|
+
function STUB_MESSAGE(reason) {
|
|
49
|
+
return ("Lilith Games (莉莉丝): feishu portal_type=6 requires a browser-minted " +
|
|
50
|
+
"`_signature` ByteDance anti-bot token. " +
|
|
51
|
+
`Could not run the browser fallback: ${reason}. ` +
|
|
52
|
+
"Install Google Chrome (or set $JOB_PRO_CHROME=/path/to/chrome) and " +
|
|
53
|
+
"ensure puppeteer-core is installed (it ships with this CLI by default).");
|
|
110
54
|
}
|
|
111
|
-
|
|
112
|
-
|
|
55
|
+
async function searchViaBrowser(opts) {
|
|
56
|
+
const limit = Math.max(1, Math.min(50, opts.pageSize ?? 10));
|
|
57
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
58
|
+
const cityCode = (opts.cityCode ?? "").trim();
|
|
59
|
+
// The career page URL itself drives the SPA's initial XHR with the
|
|
60
|
+
// matching filters baked in. We construct a URL that yields the desired
|
|
61
|
+
// search response without needing post-load interactions.
|
|
62
|
+
const params = new URLSearchParams({
|
|
63
|
+
keywords: keyword,
|
|
64
|
+
location: cityCode,
|
|
65
|
+
project: "",
|
|
66
|
+
type: "",
|
|
67
|
+
category: "",
|
|
68
|
+
current: String(opts.page ?? 1),
|
|
69
|
+
limit: String(limit),
|
|
70
|
+
functionCategory: "",
|
|
71
|
+
});
|
|
72
|
+
const targetUrl = `${CAREER_PAGE}?${params.toString()}`;
|
|
73
|
+
const r = await withPage(async (page) => {
|
|
74
|
+
// We arm a response waiter BEFORE goto so we don't miss the XHR.
|
|
75
|
+
// The Feishu SPA fires multiple identical XHRs (one for filters, one
|
|
76
|
+
// for the actual search); we filter to the one that includes
|
|
77
|
+
// `search/job/posts` in the URL AND has non-zero content-length.
|
|
78
|
+
const responsePromise = page.waitForResponse((resp) => {
|
|
79
|
+
const u = resp.url();
|
|
80
|
+
return resp.status() === 200 && /\/api\/v1\/search\/job\/posts/.test(u);
|
|
81
|
+
}, { timeout: 25000 });
|
|
82
|
+
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
83
|
+
const resp = await responsePromise;
|
|
84
|
+
return (await resp.json());
|
|
85
|
+
});
|
|
86
|
+
if (!r.ok) {
|
|
87
|
+
return { ok: false, message: STUB_MESSAGE(r.error.message) };
|
|
88
|
+
}
|
|
89
|
+
const env = r.value;
|
|
90
|
+
if (env.code !== 0 || !env.data) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
message: `upstream returned code=${env.code} (${env.message ?? "unknown"})`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const rawJobs = env.data.job_post_list ?? [];
|
|
113
97
|
return {
|
|
114
|
-
ok:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
98
|
+
ok: true,
|
|
99
|
+
result: {
|
|
100
|
+
ok: true,
|
|
101
|
+
total: env.data.count ?? rawJobs.length,
|
|
102
|
+
positions: rawJobs.map(summarize),
|
|
103
|
+
rawJobs,
|
|
104
|
+
},
|
|
118
105
|
};
|
|
119
106
|
}
|
|
120
|
-
// ----------
|
|
121
|
-
export async function
|
|
107
|
+
// ---------- public API ----------
|
|
108
|
+
export async function searchPositions(opts = {}) {
|
|
109
|
+
const r = await searchViaBrowser(opts);
|
|
110
|
+
if (!r.ok) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
source: SOURCE,
|
|
114
|
+
message: r.message,
|
|
115
|
+
query: opts,
|
|
116
|
+
positions: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
122
119
|
return {
|
|
123
|
-
ok:
|
|
120
|
+
ok: true,
|
|
124
121
|
source: SOURCE,
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
query: opts,
|
|
123
|
+
page: opts.page ?? 1,
|
|
124
|
+
page_size: opts.pageSize ?? 10,
|
|
125
|
+
total: r.result.total,
|
|
126
|
+
positions: r.result.positions,
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
export async function fetchAllPositions(opts = {}) {
|
|
130
|
+
const limit = Math.max(1, Math.min(50, opts.pageSize ?? 30));
|
|
131
|
+
const maxPages = Math.max(1, opts.maxPages ?? 20);
|
|
132
|
+
const bucket = [];
|
|
133
|
+
let total = 0;
|
|
134
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
135
|
+
const r = await searchViaBrowser({ ...opts, page, pageSize: limit });
|
|
136
|
+
if (!r.ok) {
|
|
137
|
+
if (bucket.length === 0) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
source: SOURCE,
|
|
141
|
+
message: r.message,
|
|
142
|
+
total: 0,
|
|
143
|
+
fetched: 0,
|
|
144
|
+
positions: [],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (page === 1)
|
|
150
|
+
total = r.result.total;
|
|
151
|
+
if (!r.result.positions.length)
|
|
152
|
+
break;
|
|
153
|
+
bucket.push(...r.result.positions);
|
|
154
|
+
if (bucket.length >= total)
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
131
157
|
return {
|
|
132
|
-
ok:
|
|
158
|
+
ok: true,
|
|
133
159
|
source: SOURCE,
|
|
134
|
-
|
|
160
|
+
total,
|
|
161
|
+
fetched: bucket.length,
|
|
162
|
+
positions: bucket,
|
|
135
163
|
};
|
|
136
164
|
}
|
|
137
|
-
|
|
165
|
+
// fetchPositionDetail: Feishu has no per-id REST endpoint; scan via search.
|
|
166
|
+
export async function fetchPositionDetail(postId) {
|
|
167
|
+
const id = (postId ?? "").trim();
|
|
168
|
+
if (!id)
|
|
169
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
170
|
+
const limit = 50;
|
|
171
|
+
const maxPages = 10;
|
|
172
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
173
|
+
const r = await searchViaBrowser({ page, pageSize: limit });
|
|
174
|
+
if (!r.ok)
|
|
175
|
+
return { ok: false, source: SOURCE, post_id: id, message: r.message };
|
|
176
|
+
const found = r.result.rawJobs.find((p) => String(p.id) === id);
|
|
177
|
+
if (found) {
|
|
178
|
+
const summary = summarize(found);
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
source: SOURCE,
|
|
182
|
+
post_id: id,
|
|
183
|
+
title: found.title ?? "",
|
|
184
|
+
project: summary.project,
|
|
185
|
+
recruit_label: summary.recruit_label,
|
|
186
|
+
description: found.description ?? "",
|
|
187
|
+
requirements: found.requirement ?? "",
|
|
188
|
+
work_cities: found.city_list ?? (found.city_info ? [found.city_info] : []),
|
|
189
|
+
apply_url: summary.apply_url,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (r.result.rawJobs.length < limit)
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
138
195
|
return {
|
|
139
196
|
ok: false,
|
|
140
197
|
source: SOURCE,
|
|
141
|
-
|
|
198
|
+
post_id: id,
|
|
199
|
+
message: `post ${id} not found in browser-driven search (scanned up to ${maxPages * limit} posts)`,
|
|
142
200
|
};
|
|
143
201
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
202
|
+
// fetchDictionaries: synthesize from one page of results.
|
|
203
|
+
let _dictCache = null;
|
|
204
|
+
export async function fetchDictionaries() {
|
|
205
|
+
if (_dictCache !== null)
|
|
206
|
+
return _dictCache;
|
|
207
|
+
const r = await searchViaBrowser({ pageSize: 50 });
|
|
208
|
+
if (!r.ok) {
|
|
209
|
+
const result = { ok: false, source: SOURCE, message: r.message };
|
|
210
|
+
_dictCache = result;
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
const cats = new Set();
|
|
214
|
+
const cities = new Set();
|
|
215
|
+
for (const j of r.result.rawJobs) {
|
|
216
|
+
const name = j.job_category?.name ?? j.job_function?.name;
|
|
217
|
+
if (name)
|
|
218
|
+
cats.add(name);
|
|
219
|
+
for (const c of j.city_list ?? [])
|
|
220
|
+
if (c.name)
|
|
221
|
+
cities.add(c.name);
|
|
222
|
+
if (j.city_info?.name)
|
|
223
|
+
cities.add(j.city_info.name);
|
|
224
|
+
}
|
|
225
|
+
const result = {
|
|
226
|
+
ok: true,
|
|
147
227
|
source: SOURCE,
|
|
148
|
-
|
|
228
|
+
total: r.result.total,
|
|
229
|
+
sample_categories: [...cats].sort(),
|
|
230
|
+
sample_cities: [...cities].sort(),
|
|
149
231
|
};
|
|
232
|
+
_dictCache = result;
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
const NOTICES_MSG = "Lilith Games (莉莉丝): no public notices endpoint on Feishu tenant";
|
|
236
|
+
export async function listNotices() {
|
|
237
|
+
return { ok: false, source: SOURCE, message: NOTICES_MSG, notices: [] };
|
|
238
|
+
}
|
|
239
|
+
export async function getNotice(noticeId) {
|
|
240
|
+
return { ok: false, source: SOURCE, message: NOTICES_MSG, notice_id: noticeId };
|
|
241
|
+
}
|
|
242
|
+
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
243
|
+
return { ok: false, source: SOURCE, question, message: NOTICES_MSG, matches: [] };
|
|
150
244
|
}
|
|
151
|
-
// ---------- matchResume ----------
|
|
152
|
-
// Resume matching cannot fetch live position data.
|
|
153
|
-
// We surface signals extracted from the resume and direct the user to
|
|
154
|
-
// the Lilith Games career portal for manual search.
|
|
155
245
|
export async function matchResume(text, opts = {}) {
|
|
156
|
-
|
|
246
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
247
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
157
248
|
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
249
|
+
if (!terms.length) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
source: SOURCE,
|
|
253
|
+
message: "could not extract any technical signals from the text",
|
|
254
|
+
preview: (text ?? "").slice(0, 120),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
258
|
+
const r = await searchViaBrowser({ keyword, pageSize: 50 });
|
|
259
|
+
if (!r.ok) {
|
|
260
|
+
return { ok: false, source: SOURCE, message: r.message, positions: [] };
|
|
261
|
+
}
|
|
262
|
+
const scored = [];
|
|
263
|
+
for (const p of r.result.positions) {
|
|
264
|
+
const blob = [p.title, p.project, p.recruit_label, p.work_cities].join(" ");
|
|
265
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
266
|
+
if (score > 0)
|
|
267
|
+
scored.push({ score, position: p, reasons });
|
|
268
|
+
}
|
|
269
|
+
scored.sort((a, b) => b.score - a.score);
|
|
270
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
271
|
+
if (!shortlist.length) {
|
|
272
|
+
shortlist = r.result.positions.slice(0, candidates).map((position) => ({ score: 0, position, reasons: [] }));
|
|
273
|
+
}
|
|
274
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
275
|
+
const mr = s.reasons.length > 0
|
|
276
|
+
? s.reasons.slice(0, 5)
|
|
277
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
278
|
+
return { ...s.position, match_reasons: mr };
|
|
279
|
+
});
|
|
158
280
|
return {
|
|
159
|
-
ok:
|
|
281
|
+
ok: true,
|
|
160
282
|
source: SOURCE,
|
|
161
|
-
message: STUB_MSG,
|
|
162
|
-
apply_url: CAREER_PAGE,
|
|
163
283
|
extracted_terms: terms,
|
|
164
284
|
city_preferences: cities,
|
|
285
|
+
matches,
|
|
286
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
287
|
+
"The only authority on selection is HR.",
|
|
165
288
|
};
|
|
166
289
|
}
|
|
167
|
-
// Export helpers so callers that import from this module can use them.
|
|
168
290
|
export { extractResumeSignals, scoreOverlap };
|
|
169
|
-
// Expose portal page URLs for external reference.
|
|
170
|
-
export const PORTAL_URLS = {
|
|
171
|
-
social: CAREER_PAGE,
|
|
172
|
-
campus: CAMPUS_PAGE,
|
|
173
|
-
intern: INTERN_PAGE,
|
|
174
|
-
homepage: "https://jobs.lilith.com/",
|
|
175
|
-
};
|