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/weibo.js CHANGED
@@ -1,135 +1,306 @@
1
- // Weibo / Sina campus-recruiting adapter.
1
+ // Weibo / Sina campus + social recruiting adapter.
2
2
  //
3
3
  // ============================================================
4
- // API DISCOVERY (probed 2026-05-14)
4
+ // API DISCOVERY (probed 2026-05-15)
5
5
  //
6
- // Three potential portals were investigated:
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
- // 1. job.weibo.com / hr.weibo.com / campus.weibo.com
9
- // DNS resolves to 198.18.x.x (IANA RFC 2544 benchmarking range — unreachable
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
- // 2. career.sina.com.cn (Sina's self-hosted Node/Express ATS)
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
- // 3. weibo.wd1.myworkdayjobs.com (Workday tenant — also exists for sinagroup)
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
- // 4. weibo.mokahr.com — Moka slug: SSL handshake failure (no valid portal).
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
- // 5. Greenhouse boards.greenhouse.io/weibo — HTTP 301, not a Weibo org.
22
+ // {
23
+ // "data": <base64 ciphertext>,
24
+ // "necromancer": <hex string AES key>
25
+ // }
32
26
  //
33
- // VERDICT: No public unauthenticated API exists for Weibo/Sina campus recruiting.
34
- // The canonical path is career.sina.com.cn which requires an active login session.
35
- // This adapter is an honest stub that returns ok:false with a clear message.
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
- // PositionSummary field mapping (canonical keys, matches all other adapters)
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 = "career.sina.com.cn";
50
- const CAMPUS_PAGE = "https://career.sina.com.cn/campus-recruitment/sina/43536";
51
- // ---------- stub message ----------
52
- const STUB_MSG = "Weibo campus recruiting (career.sina.com.cn) is fully auth-gated: " +
53
- "every endpoint requires a valid login session. " +
54
- "job.weibo.com / hr.weibo.com resolve to IANA-reserved IPs (unreachable outside Sina intranet). " +
55
- "No public unauthenticated JSON API was found. " +
56
- `Apply directly at ${CAMPUS_PAGE}`;
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(_opts = {}) {
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: false,
171
+ ok: true,
61
172
  source: SOURCE,
62
- message: STUB_MSG,
63
- apply_url: CAMPUS_PAGE,
64
- positions: [],
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(_opts = {}) {
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: false,
212
+ ok: true,
71
213
  source: SOURCE,
72
- message: STUB_MSG,
73
- apply_url: CAMPUS_PAGE,
74
- fetched: 0,
75
- positions: [],
214
+ total: total ?? bucket.length,
215
+ fetched: bucket.length,
216
+ positions: bucket,
76
217
  };
77
218
  }
78
- // ---------- fetchPositionDetail ----------
79
- export async function fetchPositionDetail(_postId) {
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: false,
236
+ ok: true,
82
237
  source: SOURCE,
83
- message: STUB_MSG,
84
- apply_url: CAMPUS_PAGE,
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: false,
254
+ ok: r.ok,
91
255
  source: SOURCE,
92
- message: STUB_MSG,
93
- apply_url: CAMPUS_PAGE,
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(_id) {
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(_question, _opts = {}) {
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: false,
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 / 零一万物 stub adapter for `job-pro`.
1
+ // Thin client for 01.AI / 零一万物 recruiting portal.
2
2
  //
3
- // STATUS: stub-only. 01.AI (Kai-Fu Lee's AI lab) lists careers via a SPA
4
- // without a public anonymous JSON endpoint discoverable from outside CN.
5
- // Probe results:
6
- // www.01.ai/careers / 01.ai/careers → 200 HTML SPA, no inline data
7
- // 01ai.jobs.feishu.cn, lingyiwanwu.jobs.feishu.cn → no real Feishu tenant
8
- // When 01.AI opens a public API we rewrite this in one pass.
9
- import { extractResumeSignals, checkResume } from "./tencent.js";
10
- export { checkResume };
11
- const SOURCE = "www.01.ai";
12
- const STUB_MESSAGE = "01.AI / 零一万物: no public job API discovered. Corporate careers page is SPA " +
13
- "with no embedded job data; no Feishu/Moka tenant resolves anonymously.";
14
- export async function searchPositions(_opts = {}) {
15
- return { ok: false, source: SOURCE, message: STUB_MESSAGE, query: {}, positions: [] };
16
- }
17
- export async function fetchAllPositions(_opts = {}) {
18
- return { ok: false, source: SOURCE, message: STUB_MESSAGE, total: 0, fetched: 0, positions: [] };
19
- }
20
- export async function fetchPositionDetail(postId) {
21
- return { ok: false, source: SOURCE, message: STUB_MESSAGE, post_id: postId };
22
- }
23
- export async function fetchDictionaries() {
24
- return { ok: false, source: SOURCE, message: STUB_MESSAGE };
25
- }
26
- export async function listNotices() {
27
- return { ok: false, source: SOURCE, message: STUB_MESSAGE, notices: [] };
28
- }
29
- export async function getNotice(noticeId) {
30
- return { ok: false, source: SOURCE, message: STUB_MESSAGE, notice_id: noticeId };
31
- }
32
- export async function findNoticesByQuestion(question, _opts = {}) {
33
- return { ok: false, source: SOURCE, question, message: STUB_MESSAGE, matches: [] };
34
- }
35
- export async function matchResume(text, _opts = {}) {
36
- const { terms, cities } = extractResumeSignals(text ?? "");
37
- return { ok: false, source: SOURCE, extracted_terms: terms, city_preferences: cities, matches: [], message: STUB_MESSAGE };
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.0",
4
- "description": "Query Chinese big-tech campus recruiting from your terminal. 50 companies (26 live, 24 auth-gated stub). Live: Tencent, ByteDance, Alibaba, Meituan, Xiaohongshu, JD, Kuaishou, Xiaomi, Baidu, NetEase, Didi, Bilibili, NIO, MiniMax, Huawei, Ping An, Trip.com, Unitree, Li Auto, Moonshot, Zhipu, iQIYI, Agibot, XPeng, WeRide, HoYoverse. No signup, no token, no server.",
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
  },