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/weibo.js
CHANGED
|
@@ -1,135 +1,306 @@
|
|
|
1
|
-
// Weibo / Sina campus
|
|
1
|
+
// Weibo / Sina campus + social recruiting adapter.
|
|
2
2
|
//
|
|
3
3
|
// ============================================================
|
|
4
|
-
// API DISCOVERY (probed 2026-05-
|
|
4
|
+
// API DISCOVERY (probed 2026-05-15)
|
|
5
5
|
//
|
|
6
|
-
//
|
|
6
|
+
// Weibo/Sina posts every position through their Moka (北森's competitor)
|
|
7
|
+
// recruitment portal at app.mokahr.com under the `sina` tenant. The original
|
|
8
|
+
// career.sina.com.cn 302-loop was a red herring — that host just redirects
|
|
9
|
+
// into the Moka SPA at:
|
|
7
10
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// from public internet; SSL handshake fails / empty reply at TCP level).
|
|
11
|
-
// These hostnames are dead ends from outside the Sina intranet.
|
|
11
|
+
// campus: https://app.mokahr.com/campus-recruitment/sina/43536
|
|
12
|
+
// social: https://app.mokahr.com/social-recruitment/sina/43535
|
|
12
13
|
//
|
|
13
|
-
//
|
|
14
|
-
// The portal serves Weibo campus jobs at company ID 43536
|
|
15
|
-
// (/campus-recruitment/sina/43536). Every unauthenticated HTTP request
|
|
16
|
-
// receives an infinite 302 redirect loop back to itself.
|
|
17
|
-
// The only JSON endpoint that responds without auth is:
|
|
18
|
-
// GET /api/jobs → HTTP 401 {"message":"Need Login","code":1}
|
|
19
|
-
// All other /api/* paths return HTTP 404.
|
|
20
|
-
// Conclusion: fully auth-gated, no public JSON feed.
|
|
14
|
+
// Moka exposes a fully anonymous JSON endpoint for the position list:
|
|
21
15
|
//
|
|
22
|
-
//
|
|
23
|
-
// The tenant resolves and is behind Cloudflare, but the UI shell returns
|
|
24
|
-
// HTTP 500 and redirects to community.workday.com/maintenance-page.
|
|
25
|
-
// All POST attempts to /wday/cxs/weibo/<slug>/jobs return HTTP 422
|
|
26
|
-
// regardless of slug or payload shape — the correct site slug cannot be
|
|
27
|
-
// determined without a working UI page to scrape.
|
|
16
|
+
// POST https://app.mokahr.com/api/outer/ats-apply/website/jobs/v2
|
|
28
17
|
//
|
|
29
|
-
//
|
|
18
|
+
// Required body fields: `orgId` ("sina"), `siteId` (the trailing site id from
|
|
19
|
+
// the URL — 43536 campus, 43535 social), plus pagination/keyword. The response
|
|
20
|
+
// body is AES-128-CBC encrypted:
|
|
30
21
|
//
|
|
31
|
-
//
|
|
22
|
+
// {
|
|
23
|
+
// "data": <base64 ciphertext>,
|
|
24
|
+
// "necromancer": <hex string AES key>
|
|
25
|
+
// }
|
|
32
26
|
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
27
|
+
// Decryption parameters:
|
|
28
|
+
// key = utf-8 bytes of `necromancer` (per-response, 16 chars / 16 bytes)
|
|
29
|
+
// iv = utf-8 bytes of a static `aesIv` embedded in the SPA page HTML
|
|
30
|
+
// (`window.TurboApply.data.aesIv`). For the sina tenant the iv is
|
|
31
|
+
// "de7c21ed8d6f50fe" and has remained stable across page reloads.
|
|
32
|
+
// mode = CBC, padding = PKCS#7
|
|
36
33
|
//
|
|
34
|
+
// Endpoint inventory (all anon, all app.mokahr.com):
|
|
35
|
+
// POST /api/outer/ats-apply/website/jobs/v2 → paginated list
|
|
36
|
+
// POST /api/outer/ats-apply/website/group-by-job → grouped list
|
|
37
|
+
// POST /api/outer/ats-apply/website/job → single posting
|
|
38
|
+
// POST /api/outer/ats-apply/website/jobs/v2/filterFieldsAggregations
|
|
39
|
+
// → filter taxonomy
|
|
40
|
+
// POST /api/outer/ats-apply/website/manage-job-count → counts only
|
|
41
|
+
// POST /api/outer/ats-apply/privacy-policy/get → site privacy
|
|
37
42
|
// ============================================================
|
|
38
|
-
|
|
39
|
-
// post_id — string job identifier
|
|
40
|
-
// title — position title
|
|
41
|
-
// project — job category / department
|
|
42
|
-
// recruit_label — recruit type label (e.g. "校招" / "实习")
|
|
43
|
-
// bgs — business group (not exposed by Sina ATS, always "")
|
|
44
|
-
// work_cities — work location string
|
|
45
|
-
// apply_url — deep link to the job posting
|
|
46
|
-
// ============================================================
|
|
43
|
+
import { createDecipheriv } from "node:crypto";
|
|
47
44
|
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
48
45
|
export { checkResume };
|
|
49
|
-
const SOURCE = "
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
const SOURCE = "app.mokahr.com/sina";
|
|
47
|
+
const API_ROOT = "https://app.mokahr.com";
|
|
48
|
+
const ORG_ID = "sina";
|
|
49
|
+
const CAMPUS_SITE_ID = 43536;
|
|
50
|
+
const SOCIAL_SITE_ID = 43535;
|
|
51
|
+
const CAMPUS_PAGE = `https://app.mokahr.com/campus-recruitment/sina/${CAMPUS_SITE_ID}`;
|
|
52
|
+
const SOCIAL_PAGE = `https://app.mokahr.com/social-recruitment/sina/${SOCIAL_SITE_ID}`;
|
|
53
|
+
// AES IV embedded in `window.TurboApply.data.aesIv` of the sina recruitment SPA.
|
|
54
|
+
const AES_IV = "de7c21ed8d6f50fe";
|
|
55
|
+
const DEFAULT_HEADERS = {
|
|
56
|
+
"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",
|
|
57
|
+
Accept: "application/json, text/plain, */*",
|
|
58
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
Referer: CAMPUS_PAGE,
|
|
61
|
+
Origin: API_ROOT,
|
|
62
|
+
};
|
|
63
|
+
function decryptResponse(b64Cipher, hexKey) {
|
|
64
|
+
const cipherBuf = Buffer.from(b64Cipher, "base64");
|
|
65
|
+
const key = Buffer.from(hexKey, "utf-8");
|
|
66
|
+
const iv = Buffer.from(AES_IV, "utf-8");
|
|
67
|
+
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
|
68
|
+
const plain = Buffer.concat([decipher.update(cipherBuf), decipher.final()]);
|
|
69
|
+
return JSON.parse(plain.toString("utf-8"));
|
|
70
|
+
}
|
|
71
|
+
async function post(path, body, referer = CAMPUS_PAGE) {
|
|
72
|
+
let response;
|
|
73
|
+
try {
|
|
74
|
+
response = await fetch(`${API_ROOT}${path}`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { ...DEFAULT_HEADERS, Referer: referer },
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
88
|
+
}
|
|
89
|
+
let env;
|
|
90
|
+
try {
|
|
91
|
+
env = (await response.json());
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
95
|
+
}
|
|
96
|
+
// Error envelope (no ciphertext): code != 0.
|
|
97
|
+
if (env.code !== undefined && (!env.data || typeof env.data !== "string")) {
|
|
98
|
+
return { ok: false, message: env.msg || `moka error code=${env.code}` };
|
|
99
|
+
}
|
|
100
|
+
if (!env.data || !env.necromancer) {
|
|
101
|
+
return { ok: false, message: "missing ciphertext or key in moka response" };
|
|
102
|
+
}
|
|
103
|
+
let plain;
|
|
104
|
+
try {
|
|
105
|
+
plain = decryptResponse(env.data, env.necromancer);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
message: `decrypt failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!plain.success || plain.code !== 0) {
|
|
114
|
+
return { ok: false, message: plain.msg || `moka inner code=${plain.code}` };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, data: plain.data, message: plain.msg || "ok" };
|
|
117
|
+
}
|
|
118
|
+
function summarize(item, channel, siteId) {
|
|
119
|
+
const id = String(item.id ?? "");
|
|
120
|
+
const cities = (item.locations ?? [])
|
|
121
|
+
.map((l) => [l.provinceName, l.cityName].filter(Boolean).join("·"))
|
|
122
|
+
.filter((s) => s.length > 0)
|
|
123
|
+
.join(", ");
|
|
124
|
+
const label = channel === "social" ? "社招" : item.hireMode === 2 ? "校招" : "校招";
|
|
125
|
+
return {
|
|
126
|
+
post_id: id,
|
|
127
|
+
title: (item.title ?? "").trim(),
|
|
128
|
+
project: item.projectFolder?.name?.trim() ?? "",
|
|
129
|
+
recruit_label: label,
|
|
130
|
+
bgs: (item.department?.name ?? "").trim(),
|
|
131
|
+
work_cities: cities,
|
|
132
|
+
apply_url: id
|
|
133
|
+
? `https://app.mokahr.com/${channel}-recruitment/sina/${siteId}/job/${encodeURIComponent(id)}`
|
|
134
|
+
: channel === "social"
|
|
135
|
+
? SOCIAL_PAGE
|
|
136
|
+
: CAMPUS_PAGE,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
57
139
|
// ---------- searchPositions ----------
|
|
58
|
-
export async function searchPositions(
|
|
140
|
+
export async function searchPositions(opts = {}) {
|
|
141
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
142
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
143
|
+
const channel = opts.channel ?? "campus";
|
|
144
|
+
const siteId = channel === "social" ? SOCIAL_SITE_ID : CAMPUS_SITE_ID;
|
|
145
|
+
const refererPage = channel === "social" ? SOCIAL_PAGE : CAMPUS_PAGE;
|
|
146
|
+
const body = {
|
|
147
|
+
orgId: ORG_ID,
|
|
148
|
+
siteId: String(siteId),
|
|
149
|
+
limit: pageSize,
|
|
150
|
+
offset: (page - 1) * pageSize,
|
|
151
|
+
needStat: true,
|
|
152
|
+
jobIdTopList: [],
|
|
153
|
+
customFields: {},
|
|
154
|
+
site: channel,
|
|
155
|
+
locale: "zh-CN",
|
|
156
|
+
};
|
|
157
|
+
if (opts.keyword)
|
|
158
|
+
body.keyword = opts.keyword.trim().slice(0, 60);
|
|
159
|
+
const r = await post("/api/outer/ats-apply/website/jobs/v2", body, refererPage);
|
|
160
|
+
if (!r.ok || !r.data) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
source: SOURCE,
|
|
164
|
+
message: r.message,
|
|
165
|
+
query: body,
|
|
166
|
+
positions: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const rows = r.data.jobs ?? [];
|
|
59
170
|
return {
|
|
60
|
-
ok:
|
|
171
|
+
ok: true,
|
|
61
172
|
source: SOURCE,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
173
|
+
query: body,
|
|
174
|
+
page,
|
|
175
|
+
page_size: pageSize,
|
|
176
|
+
total: r.data.jobStats?.total ?? rows.length,
|
|
177
|
+
positions: rows.map((j) => summarize(j, channel, siteId)),
|
|
65
178
|
};
|
|
66
179
|
}
|
|
67
180
|
// ---------- fetchAllPositions ----------
|
|
68
|
-
export async function fetchAllPositions(
|
|
181
|
+
export async function fetchAllPositions(opts = {}) {
|
|
182
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 50));
|
|
183
|
+
const maxPages = Math.max(1, opts.maxPages ?? 20);
|
|
184
|
+
const bucket = [];
|
|
185
|
+
let total;
|
|
186
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
187
|
+
const r = await searchPositions({
|
|
188
|
+
keyword: opts.keyword,
|
|
189
|
+
page,
|
|
190
|
+
pageSize,
|
|
191
|
+
channel: opts.channel,
|
|
192
|
+
});
|
|
193
|
+
if (!r.ok) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
source: SOURCE,
|
|
197
|
+
message: r.message,
|
|
198
|
+
total: 0,
|
|
199
|
+
fetched: bucket.length,
|
|
200
|
+
positions: bucket,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (total === undefined)
|
|
204
|
+
total = r.total;
|
|
205
|
+
if (!r.positions.length)
|
|
206
|
+
break;
|
|
207
|
+
bucket.push(...r.positions);
|
|
208
|
+
if (total !== undefined && bucket.length >= total)
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
69
211
|
return {
|
|
70
|
-
ok:
|
|
212
|
+
ok: true,
|
|
71
213
|
source: SOURCE,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
positions: [],
|
|
214
|
+
total: total ?? bucket.length,
|
|
215
|
+
fetched: bucket.length,
|
|
216
|
+
positions: bucket,
|
|
76
217
|
};
|
|
77
218
|
}
|
|
78
|
-
|
|
79
|
-
|
|
219
|
+
export async function fetchPositionDetail(postId) {
|
|
220
|
+
const id = (postId ?? "").trim();
|
|
221
|
+
if (!id)
|
|
222
|
+
return { ok: false, source: SOURCE, message: "post_id is required", post_id: id };
|
|
223
|
+
const r = await post("/api/outer/ats-apply/website/job", {
|
|
224
|
+
orgId: ORG_ID,
|
|
225
|
+
siteId: CAMPUS_SITE_ID,
|
|
226
|
+
jobId: id,
|
|
227
|
+
});
|
|
228
|
+
if (!r.ok || !r.data) {
|
|
229
|
+
return { ok: false, source: SOURCE, message: r.message || "no detail returned", post_id: id };
|
|
230
|
+
}
|
|
231
|
+
const raw = r.data;
|
|
232
|
+
const cities = (raw.locations ?? [])
|
|
233
|
+
.map((l) => [l.provinceName, l.cityName].filter(Boolean).join("·"))
|
|
234
|
+
.join(", ");
|
|
80
235
|
return {
|
|
81
|
-
ok:
|
|
236
|
+
ok: true,
|
|
82
237
|
source: SOURCE,
|
|
83
|
-
|
|
84
|
-
|
|
238
|
+
post_id: String(raw.id ?? id),
|
|
239
|
+
title: raw.title ?? "",
|
|
240
|
+
project: raw.projectFolder?.name ?? "",
|
|
241
|
+
department: raw.department?.name ?? "",
|
|
242
|
+
description: (raw.description ?? raw.responsibility ?? "").trim(),
|
|
243
|
+
requirements: (raw.requirement ?? "").trim(),
|
|
244
|
+
work_cities: cities,
|
|
245
|
+
commitment: raw.commitment ?? "",
|
|
246
|
+
published_at: raw.publishedAt ?? raw.openedAt ?? "",
|
|
247
|
+
apply_url: `https://app.mokahr.com/campus-recruitment/sina/${CAMPUS_SITE_ID}/job/${encodeURIComponent(String(raw.id ?? id))}`,
|
|
85
248
|
};
|
|
86
249
|
}
|
|
87
250
|
// ---------- fetchDictionaries ----------
|
|
88
251
|
export async function fetchDictionaries() {
|
|
252
|
+
const r = await post("/api/outer/ats-apply/website/jobs/v2/filterFieldsAggregations", { orgId: ORG_ID, siteId: CAMPUS_SITE_ID });
|
|
89
253
|
return {
|
|
90
|
-
ok:
|
|
254
|
+
ok: r.ok,
|
|
91
255
|
source: SOURCE,
|
|
92
|
-
|
|
93
|
-
|
|
256
|
+
api_host: API_ROOT,
|
|
257
|
+
verified_at: new Date().toISOString(),
|
|
258
|
+
filter_fields: r.data ?? null,
|
|
259
|
+
channels: { campus: CAMPUS_SITE_ID, social: SOCIAL_SITE_ID },
|
|
94
260
|
};
|
|
95
261
|
}
|
|
96
|
-
// ---------- notices (no public endpoint) ----------
|
|
262
|
+
// ---------- notices (no public endpoint on Moka tenant) ----------
|
|
263
|
+
const NO_NOTICES = "Weibo/Sina Moka tenant does not expose a public notices/announcements endpoint.";
|
|
97
264
|
export async function listNotices() {
|
|
98
|
-
return {
|
|
99
|
-
ok: false,
|
|
100
|
-
source: SOURCE,
|
|
101
|
-
message: "Weibo: no public notices endpoint",
|
|
102
|
-
};
|
|
265
|
+
return { ok: false, source: SOURCE, message: NO_NOTICES, notices: [] };
|
|
103
266
|
}
|
|
104
|
-
export async function getNotice(
|
|
105
|
-
return {
|
|
106
|
-
ok: false,
|
|
107
|
-
source: SOURCE,
|
|
108
|
-
message: "Weibo: no public notices endpoint",
|
|
109
|
-
};
|
|
267
|
+
export async function getNotice(noticeId) {
|
|
268
|
+
return { ok: false, source: SOURCE, message: NO_NOTICES, notice_id: noticeId };
|
|
110
269
|
}
|
|
111
|
-
export async function findNoticesByQuestion(
|
|
112
|
-
return {
|
|
113
|
-
ok: false,
|
|
114
|
-
source: SOURCE,
|
|
115
|
-
message: "Weibo: no public notices endpoint",
|
|
116
|
-
};
|
|
270
|
+
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
271
|
+
return { ok: false, source: SOURCE, question, message: NO_NOTICES, matches: [] };
|
|
117
272
|
}
|
|
118
273
|
// ---------- matchResume ----------
|
|
119
|
-
// Resume matching cannot fetch live position data without auth.
|
|
120
|
-
// We surface the signals extracted from the resume and direct the user to
|
|
121
|
-
// the Weibo campus page to search manually.
|
|
122
274
|
export async function matchResume(text, opts = {}) {
|
|
123
|
-
void opts;
|
|
124
275
|
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
276
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
277
|
+
const candidates = Math.max(topN, opts.candidates ?? 200);
|
|
278
|
+
const all = await fetchAllPositions({ pageSize: 50, maxPages: Math.ceil(candidates / 50) });
|
|
279
|
+
if (!all.ok) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
source: SOURCE,
|
|
283
|
+
message: all.message,
|
|
284
|
+
extracted_terms: terms,
|
|
285
|
+
city_preferences: cities,
|
|
286
|
+
matches: [],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const scored = [];
|
|
290
|
+
for (const p of all.positions) {
|
|
291
|
+
const haystack = `${p.title} ${p.project} ${p.bgs} ${p.work_cities}`;
|
|
292
|
+
const score = scoreOverlap(haystack, terms, cities).score;
|
|
293
|
+
if (score > 0)
|
|
294
|
+
scored.push({ score, position: p });
|
|
295
|
+
}
|
|
296
|
+
scored.sort((a, b) => b.score - a.score);
|
|
125
297
|
return {
|
|
126
|
-
ok:
|
|
298
|
+
ok: true,
|
|
127
299
|
source: SOURCE,
|
|
128
|
-
message: STUB_MSG,
|
|
129
|
-
apply_url: CAMPUS_PAGE,
|
|
130
300
|
extracted_terms: terms,
|
|
131
301
|
city_preferences: cities,
|
|
302
|
+
candidate_pool: all.positions.length,
|
|
303
|
+
matches: scored.slice(0, topN).map((s) => s.position),
|
|
132
304
|
};
|
|
133
305
|
}
|
|
134
|
-
// Export scoreOverlap so callers that import helpers from this module can use them.
|
|
135
306
|
export { extractResumeSignals, scoreOverlap };
|
package/dist/zerooneai.js
CHANGED
|
@@ -1,38 +1,41 @@
|
|
|
1
|
-
// 01.AI / 零一万物
|
|
1
|
+
// Thin client for 01.AI / 零一万物 recruiting portal.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
3
|
+
// Portal: https://01ai.jobs.feishu.cn/
|
|
4
|
+
// Platform: Feishu Recruiting (ATSX) SaaS — same API surface as nio.ts / moonshot.ts.
|
|
5
|
+
//
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Discovery (2026-05):
|
|
8
|
+
//
|
|
9
|
+
// www.01.ai/ → Strikingly site, links to portal
|
|
10
|
+
// 01ai.jobs.feishu.cn/index/ → Feishu ATSX, channel "index"
|
|
11
|
+
// tenant "零一万物" / "社招官网"
|
|
12
|
+
//
|
|
13
|
+
// The portal channel slug is "index" (not "social" / "campus") — the
|
|
14
|
+
// tenant only configured one channel and it's named "index".
|
|
15
|
+
//
|
|
16
|
+
// ============================================================
|
|
17
|
+
// PositionSummary field mapping (Feishu → canonical):
|
|
18
|
+
// post_id ← String(item.id)
|
|
19
|
+
// title ← item.title
|
|
20
|
+
// project ← item.job_category?.name ?? item.job_function?.name
|
|
21
|
+
// recruit_label ← item.recruit_type?.name
|
|
22
|
+
// bgs ← "" (not exposed in public search)
|
|
23
|
+
// work_cities ← city_list joined " / " (city_info used as fallback)
|
|
24
|
+
// apply_url ← https://01ai.jobs.feishu.cn/index/position/${id}/detail
|
|
25
|
+
import { createAdapter } from "./feishu.js";
|
|
26
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
27
|
+
export { extractResumeSignals, scoreOverlap, checkResume };
|
|
28
|
+
const _adapter = createAdapter({
|
|
29
|
+
host: "01ai.jobs.feishu.cn",
|
|
30
|
+
channel: "index",
|
|
31
|
+
label: "01.AI (零一万物)",
|
|
32
|
+
applyUrlPrefix: "https://01ai.jobs.feishu.cn/index/position",
|
|
33
|
+
});
|
|
34
|
+
export const searchPositions = _adapter.searchPositions;
|
|
35
|
+
export const fetchAllPositions = _adapter.fetchAllPositions;
|
|
36
|
+
export const fetchPositionDetail = _adapter.fetchPositionDetail;
|
|
37
|
+
export const fetchDictionaries = _adapter.fetchDictionaries;
|
|
38
|
+
export const listNotices = _adapter.listNotices;
|
|
39
|
+
export const getNotice = _adapter.getNotice;
|
|
40
|
+
export const findNoticesByQuestion = _adapter.findNoticesByQuestion;
|
|
41
|
+
export const matchResume = _adapter.matchResume;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-pro",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Query Chinese big-tech campus recruiting from your terminal. 50 companies
|
|
3
|
+
"version": "0.7.2",
|
|
4
|
+
"description": "Query Chinese big-tech campus recruiting from your terminal. 50 companies, 43 live: Tencent, ByteDance, Alibaba, Meituan, Xiaohongshu, JD, Kuaishou, Xiaomi, Baidu, NetEase, Didi, Bilibili, PDD, NIO, MiniMax, Huawei, Weibo, miHoYo, Ping An, SenseTime, Trip.com, Unitree, BYD, Li Auto, Moonshot, Zhipu, iQIYI, Megvii, Agibot, DeepSeek, 01.AI, Galaxy Universal, StepFun, Baichuan, XPeng, WeRide, HoYoverse, iFlytek, OPPO, vivo, SF Express, Horizon Robotics, Cambricon. No signup, no token, no server.",
|
|
5
5
|
"homepage": "https://job.ha7ch.com",
|
|
6
6
|
"repository": "https://github.com/HA7CH/job-pro",
|
|
7
7
|
"license": "MIT",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^20",
|
|
35
|
+
"puppeteer-core": "^25.0.2",
|
|
35
36
|
"tsx": "^4",
|
|
36
37
|
"typescript": "^5"
|
|
37
38
|
},
|