@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/zhipu.js
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
// Thin client for 智谱AI (ZhipuAI / GLM) careers, hosted on Feishu Recruiting (ATSX).
|
|
2
|
+
//
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Discovery notes (probed 2026-05):
|
|
5
|
+
//
|
|
6
|
+
// Attempted endpoints:
|
|
7
|
+
// https://zhipuai.cn/careers — SSL/network error (not reachable from CLI)
|
|
8
|
+
// https://careers.zhipuai.cn/ — SSL/network error
|
|
9
|
+
// https://hr.zhipuai.cn/ — SSL/network error
|
|
10
|
+
// https://zhipu.jobs.feishu.cn/ — HTTP 404 (tenant not configured)
|
|
11
|
+
// https://zhipuai.jobs.feishu.cn/ — HTTP 404 (tenant not configured)
|
|
12
|
+
//
|
|
13
|
+
// Live endpoint: https://zhipu-ai.jobs.feishu.cn/
|
|
14
|
+
// Host: zhipu-ai.jobs.feishu.cn
|
|
15
|
+
// Channel: "index" (from window.js-websiteInfo → website_info.path)
|
|
16
|
+
// Tenant: 北京智谱华章科技股份有限公司 (tenant_id_md5: 71bfc100479a8c605e8529cddf3ccf2b)
|
|
17
|
+
// Type: 社招官网 (social / experienced hire only — no campus portal found)
|
|
18
|
+
// Total: ~222 active positions (probed 2026-05)
|
|
19
|
+
//
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Endpoint inventory (verified 2026-05):
|
|
22
|
+
//
|
|
23
|
+
// POST https://zhipu-ai.jobs.feishu.cn/api/v1/search/job/posts
|
|
24
|
+
// Payload: { keyword, limit, offset, portal_type:3, portal_entrance:1, language:"zh",
|
|
25
|
+
// recruitment_id_list?, job_category_id_list?, location_code_list? }
|
|
26
|
+
// Headers: portal-channel: index, portal-platform: pc, website-path: index
|
|
27
|
+
// Response: { code:0, data:{ job_post_list:[...], count:<int> }, message:"ok" }
|
|
28
|
+
//
|
|
29
|
+
// GET https://zhipu-ai.jobs.feishu.cn/api/v1/config/job/filters/index
|
|
30
|
+
// Returns filter taxonomy: job_type_list (5 categories), city_list (6 cities),
|
|
31
|
+
// recruitment_type_list (only "1" = 社招)
|
|
32
|
+
//
|
|
33
|
+
// ============================================================
|
|
34
|
+
// Filter taxonomy (probed 2026-05):
|
|
35
|
+
//
|
|
36
|
+
// DIMENSION 1 — job_type_list / job_category_id_list (职位类别)
|
|
37
|
+
// 研发 id: 6791702736615426317
|
|
38
|
+
// 产品 / 策划 / 项目 id: 6791702736615409933
|
|
39
|
+
// 销售 id: 6791702736615360781
|
|
40
|
+
// 设计 id: 6791702736615344397
|
|
41
|
+
// 市场 id: 6791702736615377165
|
|
42
|
+
//
|
|
43
|
+
// DIMENSION 2 — city_list / location_code_list (城市)
|
|
44
|
+
// 北京 CT_11 上海 CT_125 深圳 CT_128
|
|
45
|
+
// 杭州 CT_52 成都 CT_22
|
|
46
|
+
// (+ additional cities may appear in posts not listed in filters)
|
|
47
|
+
//
|
|
48
|
+
// DIMENSION 3 — recruitment_type_list
|
|
49
|
+
// "1" = 社招 (experienced hire) — the only portal available
|
|
50
|
+
// child "101" = 全职 (full-time), "102" = 实习 (intern if exists)
|
|
51
|
+
//
|
|
52
|
+
// ============================================================
|
|
53
|
+
// ---- PositionSummary field mapping (Zhipu → canonical) ----
|
|
54
|
+
// post_id ← String(item.id)
|
|
55
|
+
// title ← item.title
|
|
56
|
+
// project ← item.job_category.name (job_function.name as fallback)
|
|
57
|
+
// recruit_label ← item.recruit_type.name (e.g. "全职")
|
|
58
|
+
// bgs ← "" (not exposed in public search)
|
|
59
|
+
// work_cities ← city_list joined " / " (city_info used as fallback)
|
|
60
|
+
// apply_url ← https://zhipu-ai.jobs.feishu.cn/index/position/${id}/detail
|
|
61
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
62
|
+
export { checkResume };
|
|
63
|
+
const HOST = "zhipu-ai.jobs.feishu.cn";
|
|
64
|
+
const CHANNEL = "index";
|
|
65
|
+
const API_ROOT = `https://${HOST}/api/v1`;
|
|
66
|
+
const POSITION_PAGE = `https://${HOST}/${CHANNEL}/position`;
|
|
67
|
+
const DETAIL_PAGE = (id) => `https://${HOST}/${CHANNEL}/position/${encodeURIComponent(id)}/detail`;
|
|
68
|
+
const SOURCE = HOST;
|
|
69
|
+
const DEFAULT_HEADERS = {
|
|
70
|
+
"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",
|
|
71
|
+
Accept: "application/json, text/plain, */*",
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"portal-channel": CHANNEL,
|
|
74
|
+
"portal-platform": "pc",
|
|
75
|
+
"website-path": CHANNEL,
|
|
76
|
+
Referer: POSITION_PAGE,
|
|
77
|
+
};
|
|
78
|
+
async function call(path, body) {
|
|
79
|
+
const url = `${API_ROOT}${path}`;
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: DEFAULT_HEADERS,
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
96
|
+
}
|
|
97
|
+
let payload;
|
|
98
|
+
try {
|
|
99
|
+
payload = (await response.json());
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ok: payload.code === 0,
|
|
106
|
+
data: payload.data,
|
|
107
|
+
message: payload.message || (payload.code === 0 ? "ok" : "upstream error"),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function summarizePosition(item) {
|
|
111
|
+
const id = String(item.id ?? "");
|
|
112
|
+
const cityList = item.city_list ?? [];
|
|
113
|
+
let work_cities;
|
|
114
|
+
if (cityList.length > 1) {
|
|
115
|
+
work_cities = cityList.map((c) => c.name ?? "").filter(Boolean).join(" / ");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
work_cities = cityList[0]?.name ?? item.city_info?.name ?? "";
|
|
119
|
+
}
|
|
120
|
+
// ZhipuAI: job_category is usually populated; job_function is null in public search
|
|
121
|
+
const project = item.job_category?.name ?? item.job_function?.name ?? "";
|
|
122
|
+
return {
|
|
123
|
+
post_id: id,
|
|
124
|
+
title: item.title ?? "",
|
|
125
|
+
project,
|
|
126
|
+
recruit_label: item.recruit_type?.name ?? "",
|
|
127
|
+
bgs: "",
|
|
128
|
+
work_cities,
|
|
129
|
+
apply_url: id ? DETAIL_PAGE(id) : POSITION_PAGE,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// ---------- searchPositions ----------
|
|
133
|
+
export async function searchPositions(opts = {}) {
|
|
134
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
135
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
136
|
+
const offset = (page - 1) * pageSize;
|
|
137
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
138
|
+
const asStringList = (v) => {
|
|
139
|
+
if (v === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
const arr = Array.isArray(v) ? v : [v];
|
|
142
|
+
return arr.map(String);
|
|
143
|
+
};
|
|
144
|
+
const payload = {
|
|
145
|
+
keyword,
|
|
146
|
+
limit: pageSize,
|
|
147
|
+
offset,
|
|
148
|
+
portal_type: 3,
|
|
149
|
+
portal_entrance: 1,
|
|
150
|
+
language: "zh",
|
|
151
|
+
};
|
|
152
|
+
const recruitmentIdList = asStringList(opts.recruitmentIdList);
|
|
153
|
+
if (recruitmentIdList?.length) {
|
|
154
|
+
payload.recruitment_id_list = recruitmentIdList;
|
|
155
|
+
}
|
|
156
|
+
const jobCategoryIdList = asStringList(opts.jobCategoryIdList);
|
|
157
|
+
if (jobCategoryIdList?.length) {
|
|
158
|
+
payload.job_category_id_list = jobCategoryIdList;
|
|
159
|
+
}
|
|
160
|
+
const cityIdList = asStringList(opts.cityIdList);
|
|
161
|
+
if (cityIdList?.length) {
|
|
162
|
+
payload.location_code_list = cityIdList;
|
|
163
|
+
}
|
|
164
|
+
const response = await call("/search/job/posts", payload);
|
|
165
|
+
if (!response.ok || !response.data) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
message: response.message,
|
|
169
|
+
source: SOURCE,
|
|
170
|
+
query: payload,
|
|
171
|
+
positions: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const rows = response.data.job_post_list ?? [];
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
source: SOURCE,
|
|
178
|
+
query: payload,
|
|
179
|
+
page,
|
|
180
|
+
page_size: pageSize,
|
|
181
|
+
total: response.data.count ?? rows.length,
|
|
182
|
+
positions: rows.map(summarizePosition),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// ---------- fetchAllPositions ----------
|
|
186
|
+
export async function fetchAllPositions(opts = {}) {
|
|
187
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
|
|
188
|
+
const maxPages = Math.max(1, opts.maxPages ?? 5);
|
|
189
|
+
const bucket = [];
|
|
190
|
+
let total;
|
|
191
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
192
|
+
const result = await searchPositions({ ...opts, page, pageSize });
|
|
193
|
+
if (!result.ok) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
message: result.message,
|
|
197
|
+
source: SOURCE,
|
|
198
|
+
fetched: bucket.length,
|
|
199
|
+
positions: bucket,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (total === undefined)
|
|
203
|
+
total = result.total;
|
|
204
|
+
if (!result.positions.length)
|
|
205
|
+
break;
|
|
206
|
+
bucket.push(...result.positions);
|
|
207
|
+
if (total !== undefined && bucket.length >= total)
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
ok: true,
|
|
212
|
+
source: SOURCE,
|
|
213
|
+
total: total ?? bucket.length,
|
|
214
|
+
fetched: bucket.length,
|
|
215
|
+
positions: bucket,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ---------- fetchPositionDetail ----------
|
|
219
|
+
// Zhipu/Feishu has no public per-post detail REST endpoint.
|
|
220
|
+
// Paginate the search and filter by id.
|
|
221
|
+
export async function fetchPositionDetail(postId) {
|
|
222
|
+
const id = (postId ?? "").trim();
|
|
223
|
+
if (!id)
|
|
224
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
225
|
+
const pageSize = 100;
|
|
226
|
+
const maxPages = 5;
|
|
227
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
228
|
+
const offset = (page - 1) * pageSize;
|
|
229
|
+
const payload = {
|
|
230
|
+
keyword: "",
|
|
231
|
+
limit: pageSize,
|
|
232
|
+
offset,
|
|
233
|
+
portal_type: 3,
|
|
234
|
+
portal_entrance: 1,
|
|
235
|
+
language: "zh",
|
|
236
|
+
};
|
|
237
|
+
const response = await call("/search/job/posts", payload);
|
|
238
|
+
if (!response.ok || !response.data)
|
|
239
|
+
break;
|
|
240
|
+
const posts = response.data.job_post_list ?? [];
|
|
241
|
+
const found = posts.find((p) => String(p.id) === id);
|
|
242
|
+
if (found) {
|
|
243
|
+
const summary = summarizePosition(found);
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
source: SOURCE,
|
|
247
|
+
post_id: id,
|
|
248
|
+
title: found.title ?? "",
|
|
249
|
+
direction: found.sub_title ?? "",
|
|
250
|
+
description: found.description ?? "",
|
|
251
|
+
requirements: found.requirement ?? "",
|
|
252
|
+
work_cities: found.city_list ?? (found.city_info ? [found.city_info] : []),
|
|
253
|
+
apply_url: summary.apply_url,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (posts.length < pageSize)
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
source: SOURCE,
|
|
262
|
+
post_id: id,
|
|
263
|
+
message: `post ${id} not found in public search results (searched up to ${maxPages * pageSize} posts)`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
let _filterCache = null;
|
|
267
|
+
export async function fetchDictionaries() {
|
|
268
|
+
if (_filterCache !== null)
|
|
269
|
+
return _filterCache;
|
|
270
|
+
const url = `${API_ROOT}/config/job/filters/${CHANNEL}`;
|
|
271
|
+
let response;
|
|
272
|
+
try {
|
|
273
|
+
response = await fetch(url, { headers: DEFAULT_HEADERS });
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
const r = {
|
|
277
|
+
ok: false,
|
|
278
|
+
source: SOURCE,
|
|
279
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
280
|
+
};
|
|
281
|
+
_filterCache = r;
|
|
282
|
+
return r;
|
|
283
|
+
}
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
const r = {
|
|
286
|
+
ok: false,
|
|
287
|
+
source: SOURCE,
|
|
288
|
+
message: `HTTP ${response.status}`,
|
|
289
|
+
};
|
|
290
|
+
_filterCache = r;
|
|
291
|
+
return r;
|
|
292
|
+
}
|
|
293
|
+
let payload;
|
|
294
|
+
try {
|
|
295
|
+
payload = await response.json();
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
const r = {
|
|
299
|
+
ok: false,
|
|
300
|
+
source: SOURCE,
|
|
301
|
+
message: `bad JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
302
|
+
};
|
|
303
|
+
_filterCache = r;
|
|
304
|
+
return r;
|
|
305
|
+
}
|
|
306
|
+
if (payload.code !== 0 || !payload.data) {
|
|
307
|
+
const r = {
|
|
308
|
+
ok: false,
|
|
309
|
+
source: SOURCE,
|
|
310
|
+
message: payload.message ?? "upstream error",
|
|
311
|
+
};
|
|
312
|
+
_filterCache = r;
|
|
313
|
+
return r;
|
|
314
|
+
}
|
|
315
|
+
const d = payload.data;
|
|
316
|
+
const jobCategories = (d.job_type_list ?? []).map((cat) => ({
|
|
317
|
+
id: cat.id ?? "",
|
|
318
|
+
name: cat.name ?? "",
|
|
319
|
+
en_name: cat.en_name ?? "",
|
|
320
|
+
depth: cat.depth ?? 1,
|
|
321
|
+
parent_id: cat.parent?.id ?? null,
|
|
322
|
+
}));
|
|
323
|
+
const cities = (d.city_list ?? []).map((c) => ({
|
|
324
|
+
code: c.code ?? "",
|
|
325
|
+
name: c.name ?? "",
|
|
326
|
+
en_name: c.en_name ?? "",
|
|
327
|
+
}));
|
|
328
|
+
// Flatten recruitment_type_list (parent + children)
|
|
329
|
+
const recruitmentTypes = [];
|
|
330
|
+
const seen = new Set();
|
|
331
|
+
const walkRT = (items) => {
|
|
332
|
+
for (const rt of items ?? []) {
|
|
333
|
+
if (rt.id && !seen.has(rt.id)) {
|
|
334
|
+
seen.add(rt.id);
|
|
335
|
+
recruitmentTypes.push({
|
|
336
|
+
id: rt.id,
|
|
337
|
+
name: rt.name ?? "",
|
|
338
|
+
en_name: rt.en_name ?? "",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (rt.children?.length)
|
|
342
|
+
walkRT(rt.children);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
walkRT(d.recruitment_type_list);
|
|
346
|
+
// Fallback: known static values when API omits them
|
|
347
|
+
if (!recruitmentTypes.length) {
|
|
348
|
+
recruitmentTypes.push({ id: "1", name: "社招", en_name: "Experienced" }, { id: "101", name: "全职", en_name: "Full-time" });
|
|
349
|
+
}
|
|
350
|
+
const result = {
|
|
351
|
+
ok: true,
|
|
352
|
+
source: SOURCE,
|
|
353
|
+
jobCategories,
|
|
354
|
+
cities,
|
|
355
|
+
recruitmentTypes,
|
|
356
|
+
};
|
|
357
|
+
_filterCache = result;
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
// ---------- stub notices ----------
|
|
361
|
+
const STUB_NOTICES = {
|
|
362
|
+
ok: false,
|
|
363
|
+
source: SOURCE,
|
|
364
|
+
message: "ZhipuAI: no public notices endpoint",
|
|
365
|
+
};
|
|
366
|
+
export async function listNotices() {
|
|
367
|
+
return STUB_NOTICES;
|
|
368
|
+
}
|
|
369
|
+
export async function getNotice(_id) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
source: SOURCE,
|
|
373
|
+
message: "ZhipuAI: no public notices endpoint",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
source: SOURCE,
|
|
380
|
+
message: "ZhipuAI: no public notices endpoint",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// ---------- matchResume ----------
|
|
384
|
+
export async function matchResume(text, opts = {}) {
|
|
385
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
386
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
387
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
388
|
+
if (!terms.length) {
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
source: SOURCE,
|
|
392
|
+
message: "could not extract any technical signals from the text",
|
|
393
|
+
preview: (text ?? "").slice(0, 120),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
397
|
+
const list = await searchPositions({ keyword, page: 1, pageSize: 100 });
|
|
398
|
+
if (!list.ok) {
|
|
399
|
+
return { ok: false, source: SOURCE, message: list.message, positions: [] };
|
|
400
|
+
}
|
|
401
|
+
// Re-fetch raw posts to access description + requirement fields for scoring
|
|
402
|
+
const payload = {
|
|
403
|
+
keyword,
|
|
404
|
+
limit: 100,
|
|
405
|
+
offset: 0,
|
|
406
|
+
portal_type: 3,
|
|
407
|
+
portal_entrance: 1,
|
|
408
|
+
language: "zh",
|
|
409
|
+
};
|
|
410
|
+
const raw = await call("/search/job/posts", payload);
|
|
411
|
+
const rawPosts = raw.ok ? (raw.data?.job_post_list ?? []) : [];
|
|
412
|
+
const rawById = new Map();
|
|
413
|
+
for (const p of rawPosts) {
|
|
414
|
+
rawById.set(String(p.id ?? ""), p);
|
|
415
|
+
}
|
|
416
|
+
const scored = [];
|
|
417
|
+
for (const p of list.positions) {
|
|
418
|
+
const rp = rawById.get(p.post_id);
|
|
419
|
+
const blob = [
|
|
420
|
+
p.title,
|
|
421
|
+
p.project,
|
|
422
|
+
p.recruit_label,
|
|
423
|
+
p.work_cities,
|
|
424
|
+
rp?.description ?? "",
|
|
425
|
+
rp?.requirement ?? "",
|
|
426
|
+
].join(" ");
|
|
427
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
428
|
+
if (score > 0) {
|
|
429
|
+
scored.push({
|
|
430
|
+
score,
|
|
431
|
+
position: p,
|
|
432
|
+
reasons,
|
|
433
|
+
description: rp?.description,
|
|
434
|
+
requirements: rp?.requirement,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
scored.sort((a, b) => b.score - a.score);
|
|
439
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
440
|
+
if (!shortlist.length) {
|
|
441
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({
|
|
442
|
+
score: 0,
|
|
443
|
+
position,
|
|
444
|
+
reasons: [],
|
|
445
|
+
description: rawById.get(position.post_id)?.description,
|
|
446
|
+
requirements: rawById.get(position.post_id)?.requirement,
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
450
|
+
const mr = s.reasons.length > 0
|
|
451
|
+
? s.reasons.slice(0, 5)
|
|
452
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
453
|
+
return {
|
|
454
|
+
...s.position,
|
|
455
|
+
description: s.description,
|
|
456
|
+
requirements: s.requirements,
|
|
457
|
+
match_reasons: mr,
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
source: SOURCE,
|
|
463
|
+
extracted_terms: terms,
|
|
464
|
+
city_preferences: cities,
|
|
465
|
+
matches,
|
|
466
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
467
|
+
"The only authority on selection is HR.",
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
// ---------- Phase 2: fetchApplicationSchema ----------
|
|
471
|
+
import { makeFeishuApplyFn } from "./feishu.js";
|
|
472
|
+
export const fetchApplicationSchema = makeFeishuApplyFn({
|
|
473
|
+
host: "zhipu-ai.jobs.feishu.cn",
|
|
474
|
+
source: "zhipu-ai.jobs.feishu.cn",
|
|
475
|
+
channel: "index",
|
|
476
|
+
applyUrlPrefix: "https://zhipu-ai.jobs.feishu.cn/index/position",
|
|
477
|
+
fetchTitle: (id) => fetchPositionDetail(id),
|
|
478
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# job-pro session bridge — Chrome extension
|
|
2
|
+
|
|
3
|
+
Manifest v3 extension that captures careers-site session cookies + CSRF/XSRF
|
|
4
|
+
headers for use by the CLI's auto-apply (Phase 2.1). Unlike the simple
|
|
5
|
+
Greenhouse / Lever boards (where the apply form is open-access and the CLI
|
|
6
|
+
can submit anonymously via `--debug-submit-to`), most Chinese ATS tenants
|
|
7
|
+
gate `apply` behind a logged-in candidate session. This extension lets
|
|
8
|
+
users log in once in their normal browser, then export the captured
|
|
9
|
+
session into `~/.jobpro/<adapter>.session.json` for the CLI to re-use.
|
|
10
|
+
|
|
11
|
+
## Install (developer mode, local)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Repo root
|
|
15
|
+
cd extension/
|
|
16
|
+
# Optional: generate placeholder icons (just colored squares).
|
|
17
|
+
# The extension still loads without them; popup just won't have an icon.
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
1. Open Chrome → `chrome://extensions/` → enable **Developer mode**.
|
|
21
|
+
2. Click **Load unpacked** → select the `extension/` directory.
|
|
22
|
+
3. Pin the puzzle-piece icon to the toolbar for quick access.
|
|
23
|
+
|
|
24
|
+
## Capture a session
|
|
25
|
+
|
|
26
|
+
1. Log into any supported careers site (e.g. `talent.antgroup.com`,
|
|
27
|
+
`iflytek.zhiye.com`, `app.mokahr.com/.../<org>/<siteId>`).
|
|
28
|
+
2. Browse around — view a job, open the apply modal — so the SPA fires
|
|
29
|
+
its auth-bearing XHRs. The extension listens for `Cookie`,
|
|
30
|
+
`X-Xsrf-Token`, `Authorization`, and Feishu/Beisen-style headers
|
|
31
|
+
(`X-Fscp-Std-Info`, `langtype`, etc.) and caches them by adapter key.
|
|
32
|
+
3. Click the toolbar icon → **Export** on the captured row. The
|
|
33
|
+
extension downloads `jobpro/<adapter>.session.json` via Chrome's
|
|
34
|
+
download manager.
|
|
35
|
+
4. Move the file:
|
|
36
|
+
```bash
|
|
37
|
+
mkdir -p ~/.jobpro
|
|
38
|
+
mv ~/Downloads/jobpro/<adapter>.session.json ~/.jobpro/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What's in the JSON
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"adapter": "antgroup",
|
|
46
|
+
"host": "talent.antgroup.com",
|
|
47
|
+
"exported_at": "2026-05-16T08:00:00.000Z",
|
|
48
|
+
"headers": {
|
|
49
|
+
"x-xsrf-token": "VSQK2wSZQC-DRAZxaQevxQ",
|
|
50
|
+
"x-fscp-std-info": "{\"client_id\": \"40108\"}",
|
|
51
|
+
"cookie": "<full cookie header>",
|
|
52
|
+
"...": "..."
|
|
53
|
+
},
|
|
54
|
+
"cookies": [
|
|
55
|
+
{ "name": "XSRF-TOKEN", "value": "…", "domain": ".liepin.com", "path": "/", … }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The CLI doesn't read this file yet — Phase 2.1 wires that in.
|
|
61
|
+
Today the file is the deliverable; future iterations land the
|
|
62
|
+
`<adapter>.applyWithSession(sessionPath, postId)` flow.
|
|
63
|
+
|
|
64
|
+
## Why MV3, not a content script injection
|
|
65
|
+
|
|
66
|
+
We need `chrome.cookies` to dump HttpOnly cookies (used by every Chinese
|
|
67
|
+
ATS we've probed). Only background service workers can call
|
|
68
|
+
`chrome.cookies.getAll()`. The popup just talks to the worker via
|
|
69
|
+
`chrome.runtime.sendMessage`.
|
|
70
|
+
|
|
71
|
+
## Scope (privacy)
|
|
72
|
+
|
|
73
|
+
* Only captures headers from hosts explicitly listed in
|
|
74
|
+
`manifest.json#host_permissions`. Browsing anywhere else is invisible
|
|
75
|
+
to the extension.
|
|
76
|
+
* Storage is `chrome.storage.local` — never synced, never sent to any
|
|
77
|
+
remote.
|
|
78
|
+
* Exports are user-triggered downloads to `~/Downloads/jobpro/`. No
|
|
79
|
+
network egress.
|