@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/meituan.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
// Thin client for Meituan's public recruiting API at zhaopin.meituan.com.
|
|
2
|
+
//
|
|
3
|
+
// All endpoints are unauthenticated; the server just checks Referer/Origin.
|
|
4
|
+
// Endpoint inventory (verified 2026-05-14):
|
|
5
|
+
//
|
|
6
|
+
// POST /api/official/job/getJobList — paginated job search
|
|
7
|
+
// POST /api/official/job/getJobDetail — single job detail by jobUnionId
|
|
8
|
+
// POST /api/official/city/search — resolve city name → {code, name}
|
|
9
|
+
//
|
|
10
|
+
// Response envelope: { status: 1, message: "成功", data: { ... } }
|
|
11
|
+
// NOTE: status === 1 (not 0) indicates success on this platform.
|
|
12
|
+
//
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// FILTER TAXONOMY (all verified against live API 2026-05-14)
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
//
|
|
17
|
+
// ── 1. jobType (in request body as jobType: [{code, subCode:[]}]) ──────────
|
|
18
|
+
// "1" → 校招应届正式 (new-grad full-time) totalCount ~112
|
|
19
|
+
// jobSpecialCode distribution: {"1":72, "3":27, "7":1}
|
|
20
|
+
// sample: 自动驾驶算法工程师 / 计算机视觉工程师
|
|
21
|
+
// "2" → 实习 (intern) totalCount ~531
|
|
22
|
+
// jobSpecialCode distribution: {"6":94, "1":3, "3":3}
|
|
23
|
+
// sample: HR实习生-招聘方向 / Marketing Intern
|
|
24
|
+
// "3" → 社招 (experienced hire) totalCount ~2613
|
|
25
|
+
// jobSpecialCode distribution: {"5":100}
|
|
26
|
+
// ∅ → all three combined totalCount ~3256
|
|
27
|
+
// Default: ["1","2"] mirrors the "校招" tab on zhaopin.meituan.com.
|
|
28
|
+
//
|
|
29
|
+
// ── 2. city (in request body as cityList: [{code, name}]) ─────────────────
|
|
30
|
+
// City codes come from POST /api/official/city/search {keyword: "北京"}.
|
|
31
|
+
// CRITICAL: passing {name} without a code returns wrong results (e.g. 10
|
|
32
|
+
// instead of 2148 for Beijing). Always resolve code first.
|
|
33
|
+
//
|
|
34
|
+
// Top cities (all jobTypes combined, 2026-05-14):
|
|
35
|
+
// 北京市 001001 2148 上海市 001009 736
|
|
36
|
+
// 深圳市 001019002 375 成都市 001023001 198
|
|
37
|
+
// 广州市 001019001 177 杭州市 001011001 142
|
|
38
|
+
// 武汉市 001017001 109 人力资源平台 — 95
|
|
39
|
+
// 南京市 001010001 70 西安市 001027001 67
|
|
40
|
+
// 苏州市 001010013 50
|
|
41
|
+
// Campus (jobType 1+2) by city:
|
|
42
|
+
// 北京市 557 上海市 229 深圳市 81 成都市 40 杭州市 11
|
|
43
|
+
//
|
|
44
|
+
// ── 3. department / BU (in request body as department: [{code}]) ───────────
|
|
45
|
+
// department codes are Meituan's internal "BG" codes. Passing name-only
|
|
46
|
+
// does NOT filter (returns all results). Codes discovered by scanning the
|
|
47
|
+
// JS bundle + brute-force sweep BG001..BG100 (2026-05-14):
|
|
48
|
+
//
|
|
49
|
+
// BG053 食杂零售-小象事业部 361
|
|
50
|
+
// BG041 核心本地商业-基础研发平台 342
|
|
51
|
+
// BG052 食杂零售-快驴事业部 233
|
|
52
|
+
// BG022 食杂零售 (parent BG) 762 ← aggregates 053/052/054/055/056
|
|
53
|
+
// BG038 核心本地商业-闪购事业部 204
|
|
54
|
+
// BG043 核心本地商业-业务研发平台 195
|
|
55
|
+
// BG047 核心本地商业-酒店旅行 189
|
|
56
|
+
// BG024 软硬件服务 (parent BG) 388 ← aggregates 012/018/019/050/057/058
|
|
57
|
+
// BG021 Keeta 188
|
|
58
|
+
// BG015 财务平台 126
|
|
59
|
+
// BG012 软硬件服务-骑行事业部 112
|
|
60
|
+
// BG039 核心本地商业-医药健康事业部 99
|
|
61
|
+
// BG032 核心本地商业-服务零售事业部 98
|
|
62
|
+
// BG011 人力资源平台 95
|
|
63
|
+
// BG037 核心本地商业-到家履约平台 95
|
|
64
|
+
// BG054 食杂零售-快乐猴事业部 88
|
|
65
|
+
// BG036 核心本地商业-外卖事业部 72
|
|
66
|
+
// BG046 核心本地商业-商业增值部 50
|
|
67
|
+
// BG019 软硬件服务-充电宝业务部 50
|
|
68
|
+
// BG048 核心本地商业-下沉市场发展部 43
|
|
69
|
+
// BG055 食杂零售-公共部门 43
|
|
70
|
+
// BG018 软硬件服务-餐饮SaaS事业部 44
|
|
71
|
+
// BG009 美团自动车配送 145
|
|
72
|
+
// BG003 美团金服 45
|
|
73
|
+
// BG031 核心本地商业-到餐事业部 44
|
|
74
|
+
// BG056 食杂零售-Keemart 36
|
|
75
|
+
// BG013 核心本地商业-美团平台 278
|
|
76
|
+
// BG010 公司事务平台 35
|
|
77
|
+
// BG057 软硬件服务-硬件管理部 28
|
|
78
|
+
// BG020 美团无人机 93
|
|
79
|
+
// BG044 核心本地商业-平台及职能部门 19
|
|
80
|
+
// BG050 软硬件服务-软件研发部 19
|
|
81
|
+
// BG049 软硬件服务-酒店SaaS业务部 10
|
|
82
|
+
// BG016 战略与投资平台 6
|
|
83
|
+
// BG008 核心本地商业-点评事业部 24
|
|
84
|
+
// BG045 GN06 4
|
|
85
|
+
// BG051 (unidentified) 1
|
|
86
|
+
// BG058 软硬件服务-软硬件合规部 1
|
|
87
|
+
// Note: codes returning 3256 (e.g. BG001) act as no-op; only the above
|
|
88
|
+
// codes represent real filterable BU subdivisions.
|
|
89
|
+
//
|
|
90
|
+
// ── 4. jobFamily / jobFamilyGroup — NOT filterable via API ────────────────
|
|
91
|
+
// jobFamily and jobFamilyGroup appear as metadata on job objects but
|
|
92
|
+
// sending them in the request body has no effect (always returns 3256).
|
|
93
|
+
// Values observed in corpus (first 500 jobs):
|
|
94
|
+
// jobFamily: 技术类 / 运营类 / 产品类 / 零售类 / 职能类
|
|
95
|
+
// 销售、客服与支持类 / 商业分析类 / 市场营销类 / 设计类
|
|
96
|
+
// jobFamilyGroup: 软件 / 算法 / 运维 / 测试 / 产品 / 产品运营 / 用户运营
|
|
97
|
+
// 业务运营 / 运营类 / 财务 / 人力资源 / 商业分析 / 供应链
|
|
98
|
+
// 物流 / 门店 / 销售 / 营销 / 采购 / 市场 / 行政 / …
|
|
99
|
+
// Use keyword search to narrow by family (e.g. keyword="算法").
|
|
100
|
+
//
|
|
101
|
+
// ── 5. jobSpecialCode — metadata only, not a filter dimension ─────────────
|
|
102
|
+
// "1" = 普通校招岗 (normal campus)
|
|
103
|
+
// "3" = 可能为 global/特殊项目 (mixed, appears in both type 1 & 2)
|
|
104
|
+
// "5" = 社招 (experienced hire)
|
|
105
|
+
// "6" = 实习 (intern, always accompanies jobType=2)
|
|
106
|
+
// "7" = rare, appears in type 1 (<1%)
|
|
107
|
+
//
|
|
108
|
+
// ── 6. jobShareType — undocumented enum ───────────────────────────────────
|
|
109
|
+
// "1" → returns ~3256 (the full public listing, used as default)
|
|
110
|
+
// "2" → returns ~2194 (subset, purpose unclear)
|
|
111
|
+
// ∅ → returns ~3256 (same as "1")
|
|
112
|
+
//
|
|
113
|
+
// ── Request body shape ────────────────────────────────────────────────────
|
|
114
|
+
// {
|
|
115
|
+
// page: { pageNo: number, pageSize: number }, // max pageSize = 100
|
|
116
|
+
// jobShareType: "1",
|
|
117
|
+
// keywords: string, // respected, max 30 chars
|
|
118
|
+
// cityList: [{code: string, name: string}], // code required for filter
|
|
119
|
+
// department: [{code: string}], // BG code; name ignored
|
|
120
|
+
// jobType: [{code: string, subCode: []}],
|
|
121
|
+
// }
|
|
122
|
+
//
|
|
123
|
+
// ── City lookup ───────────────────────────────────────────────────────────
|
|
124
|
+
// POST /api/official/city/search {keyword: "城市名"}
|
|
125
|
+
// Returns [{code, name, children, …}]
|
|
126
|
+
// Common codes hardcoded in CITY_CODES below for offline use.
|
|
127
|
+
//
|
|
128
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
129
|
+
//
|
|
130
|
+
// PositionSummary field mapping:
|
|
131
|
+
// post_id ← jobUnionId (stringified)
|
|
132
|
+
// title ← name
|
|
133
|
+
// project ← department[0].name if present, else ""
|
|
134
|
+
// recruit_label ← jobType mapped to human label (社招/实习/校园)
|
|
135
|
+
// bgs ← jobFamilyGroup (e.g. "软件") + jobFamily (e.g. "技术类")
|
|
136
|
+
// work_cities ← cityList[*].name joined with " / "
|
|
137
|
+
// apply_url ← https://zhaopin.meituan.com/job-list/${jobUnionId}
|
|
138
|
+
//
|
|
139
|
+
// Detail fields:
|
|
140
|
+
// description ← jobDuty (job responsibilities)
|
|
141
|
+
// requirements ← jobRequirement (required qualifications)
|
|
142
|
+
// highlight ← highLight (why join)
|
|
143
|
+
// department_intro ← departmentIntro (team introduction)
|
|
144
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
145
|
+
export { checkResume };
|
|
146
|
+
const API_ROOT = "https://zhaopin.meituan.com/api/official";
|
|
147
|
+
const LIST_PAGE = "https://zhaopin.meituan.com/job-list";
|
|
148
|
+
const DETAIL_PAGE = (jobUnionId) => `https://zhaopin.meituan.com/job-list/${encodeURIComponent(jobUnionId)}`;
|
|
149
|
+
const DEFAULT_HEADERS = {
|
|
150
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
|
|
151
|
+
Accept: "application/json, text/plain, */*",
|
|
152
|
+
};
|
|
153
|
+
// jobType code → human label
|
|
154
|
+
const JOB_TYPE_LABEL = {
|
|
155
|
+
"1": "校招",
|
|
156
|
+
"2": "实习",
|
|
157
|
+
"3": "社招",
|
|
158
|
+
};
|
|
159
|
+
// Hardcoded city codes for common cities (verified 2026-05-14 via city/search).
|
|
160
|
+
// Use resolveCityCodes() to look up arbitrary city names at runtime.
|
|
161
|
+
const CITY_CODES = {
|
|
162
|
+
"北京": "001001",
|
|
163
|
+
"北京市": "001001",
|
|
164
|
+
"上海": "001009",
|
|
165
|
+
"上海市": "001009",
|
|
166
|
+
"广州": "001019001",
|
|
167
|
+
"广州市": "001019001",
|
|
168
|
+
"深圳": "001019002",
|
|
169
|
+
"深圳市": "001019002",
|
|
170
|
+
"杭州": "001011001",
|
|
171
|
+
"杭州市": "001011001",
|
|
172
|
+
"成都": "001023001",
|
|
173
|
+
"成都市": "001023001",
|
|
174
|
+
"武汉": "001017001",
|
|
175
|
+
"武汉市": "001017001",
|
|
176
|
+
"西安": "001027001",
|
|
177
|
+
"西安市": "001027001",
|
|
178
|
+
"苏州": "001010013",
|
|
179
|
+
"苏州市": "001010013",
|
|
180
|
+
"南京": "001010001",
|
|
181
|
+
"南京市": "001010001",
|
|
182
|
+
};
|
|
183
|
+
// BG department codes (verified 2026-05-14, see header comment for full list)
|
|
184
|
+
// Map from code → canonical department name
|
|
185
|
+
export const BG_DEPARTMENT_CODES = {
|
|
186
|
+
"BG003": "美团金服",
|
|
187
|
+
"BG008": "核心本地商业-点评事业部",
|
|
188
|
+
"BG009": "美团自动车配送",
|
|
189
|
+
"BG010": "公司事务平台",
|
|
190
|
+
"BG011": "人力资源平台",
|
|
191
|
+
"BG012": "软硬件服务-骑行事业部",
|
|
192
|
+
"BG013": "核心本地商业-美团平台",
|
|
193
|
+
"BG015": "财务平台",
|
|
194
|
+
"BG016": "战略与投资平台",
|
|
195
|
+
"BG018": "软硬件服务-餐饮SaaS事业部",
|
|
196
|
+
"BG019": "软硬件服务-充电宝业务部",
|
|
197
|
+
"BG020": "美团无人机",
|
|
198
|
+
"BG021": "Keeta",
|
|
199
|
+
"BG022": "食杂零售",
|
|
200
|
+
"BG024": "软硬件服务",
|
|
201
|
+
"BG031": "核心本地商业-到餐事业部",
|
|
202
|
+
"BG032": "核心本地商业-服务零售事业部",
|
|
203
|
+
"BG036": "核心本地商业-外卖事业部",
|
|
204
|
+
"BG037": "核心本地商业-到家履约平台",
|
|
205
|
+
"BG038": "核心本地商业-闪购事业部",
|
|
206
|
+
"BG039": "核心本地商业-医药健康事业部",
|
|
207
|
+
"BG041": "核心本地商业-基础研发平台",
|
|
208
|
+
"BG043": "核心本地商业-业务研发平台",
|
|
209
|
+
"BG044": "核心本地商业-平台及职能部门",
|
|
210
|
+
"BG045": "GN06",
|
|
211
|
+
"BG046": "核心本地商业-商业增值部",
|
|
212
|
+
"BG047": "核心本地商业-酒店旅行",
|
|
213
|
+
"BG048": "核心本地商业-下沉市场发展部",
|
|
214
|
+
"BG049": "软硬件服务-酒店SaaS业务部",
|
|
215
|
+
"BG050": "软硬件服务-软件研发部",
|
|
216
|
+
"BG052": "食杂零售-快驴事业部",
|
|
217
|
+
"BG053": "食杂零售-小象事业部",
|
|
218
|
+
"BG054": "食杂零售-快乐猴事业部",
|
|
219
|
+
"BG055": "食杂零售-公共部门",
|
|
220
|
+
"BG056": "食杂零售-Keemart",
|
|
221
|
+
"BG057": "软硬件服务-硬件管理部",
|
|
222
|
+
"BG058": "软硬件服务-软硬件合规部",
|
|
223
|
+
};
|
|
224
|
+
async function call(method, path, opts = {}) {
|
|
225
|
+
const url = `${API_ROOT}${path}`;
|
|
226
|
+
const headers = {
|
|
227
|
+
...DEFAULT_HEADERS,
|
|
228
|
+
Referer: opts.referer ?? LIST_PAGE,
|
|
229
|
+
};
|
|
230
|
+
let body;
|
|
231
|
+
if (opts.body !== undefined) {
|
|
232
|
+
body = JSON.stringify(opts.body);
|
|
233
|
+
headers["Content-Type"] = "application/json";
|
|
234
|
+
}
|
|
235
|
+
let response;
|
|
236
|
+
try {
|
|
237
|
+
response = await fetch(url, { method, headers, body });
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
247
|
+
}
|
|
248
|
+
let payload;
|
|
249
|
+
try {
|
|
250
|
+
payload = (await response.json());
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
254
|
+
}
|
|
255
|
+
// Meituan uses status === 1 for success (not 0 like Tencent)
|
|
256
|
+
return {
|
|
257
|
+
ok: payload.status === 1,
|
|
258
|
+
data: payload.data,
|
|
259
|
+
message: payload.message || (payload.status === 1 ? "ok" : "upstream error"),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function citiesText(cityList) {
|
|
263
|
+
if (!cityList || !cityList.length)
|
|
264
|
+
return "";
|
|
265
|
+
return cityList
|
|
266
|
+
.map((c) => c.name ?? "")
|
|
267
|
+
.filter(Boolean)
|
|
268
|
+
.join(" / ");
|
|
269
|
+
}
|
|
270
|
+
function summarizePosition(item) {
|
|
271
|
+
const jobUnionId = String(item.jobUnionId ?? "");
|
|
272
|
+
const deptName = item.department?.[0]?.name ?? "";
|
|
273
|
+
const jobTypeCode = item.jobType ?? "";
|
|
274
|
+
const recruitLabel = JOB_TYPE_LABEL[jobTypeCode] ?? jobTypeCode;
|
|
275
|
+
const bgs = [item.jobFamilyGroup, item.jobFamily].filter(Boolean).join(" · ");
|
|
276
|
+
return {
|
|
277
|
+
post_id: jobUnionId,
|
|
278
|
+
title: item.name ?? "",
|
|
279
|
+
project: deptName,
|
|
280
|
+
recruit_label: recruitLabel,
|
|
281
|
+
bgs,
|
|
282
|
+
work_cities: citiesText(item.cityList),
|
|
283
|
+
apply_url: jobUnionId ? DETAIL_PAGE(jobUnionId) : LIST_PAGE,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Resolve city names to [{code, name}] objects for the getJobList payload.
|
|
288
|
+
* Uses the hardcoded CITY_CODES map first; falls back to a live API lookup.
|
|
289
|
+
*/
|
|
290
|
+
async function resolveCityCodes(cityNames) {
|
|
291
|
+
const result = [];
|
|
292
|
+
for (const raw of cityNames) {
|
|
293
|
+
const name = raw.trim();
|
|
294
|
+
if (!name)
|
|
295
|
+
continue;
|
|
296
|
+
const knownCode = CITY_CODES[name];
|
|
297
|
+
if (knownCode) {
|
|
298
|
+
result.push({ code: knownCode, name });
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
// Live lookup
|
|
302
|
+
const res = await call("POST", "/city/search", { body: { keyword: name } });
|
|
303
|
+
if (res.ok && Array.isArray(res.data) && res.data.length > 0) {
|
|
304
|
+
const first = res.data[0];
|
|
305
|
+
if (first.code && first.name) {
|
|
306
|
+
result.push({ code: first.code, name: first.name });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// If not resolved, skip — sending name-only would give wrong counts
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
export async function searchPositions(opts = {}) {
|
|
314
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
315
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
316
|
+
const jobTypeCodes = opts.jobTypeCodes ?? ["1", "2"];
|
|
317
|
+
// Resolve city names → [{code, name}]
|
|
318
|
+
const cityList = opts.cities?.length
|
|
319
|
+
? await resolveCityCodes(opts.cities)
|
|
320
|
+
: [];
|
|
321
|
+
// Resolve department strings → [{code}]
|
|
322
|
+
// Accept BG codes directly (e.g. "BG021") or canonical names looked up in
|
|
323
|
+
// the reverse map. Name-only entries that can't be resolved are dropped.
|
|
324
|
+
const bgNameToCode = {};
|
|
325
|
+
for (const [code, name] of Object.entries(BG_DEPARTMENT_CODES)) {
|
|
326
|
+
bgNameToCode[name] = code;
|
|
327
|
+
}
|
|
328
|
+
const department = [];
|
|
329
|
+
for (const d of opts.departments ?? []) {
|
|
330
|
+
const s = d.trim();
|
|
331
|
+
if (!s)
|
|
332
|
+
continue;
|
|
333
|
+
if (/^BG\d+$/i.test(s)) {
|
|
334
|
+
department.push({ code: s.toUpperCase() });
|
|
335
|
+
}
|
|
336
|
+
else if (bgNameToCode[s]) {
|
|
337
|
+
department.push({ code: bgNameToCode[s] });
|
|
338
|
+
}
|
|
339
|
+
// Unresolvable names are silently dropped (API ignores them anyway)
|
|
340
|
+
}
|
|
341
|
+
const body = {
|
|
342
|
+
page: { pageNo: page, pageSize },
|
|
343
|
+
jobShareType: "1",
|
|
344
|
+
keywords: (opts.keyword ?? "").trim().slice(0, 30),
|
|
345
|
+
cityList,
|
|
346
|
+
department,
|
|
347
|
+
jobType: jobTypeCodes.map((code) => ({ code, subCode: [] })),
|
|
348
|
+
};
|
|
349
|
+
const response = await call("POST", "/job/getJobList", { body });
|
|
350
|
+
if (!response.ok || !response.data) {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
message: response.message,
|
|
354
|
+
source: "zhaopin.meituan.com",
|
|
355
|
+
query: body,
|
|
356
|
+
positions: [],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const rows = response.data.list ?? [];
|
|
360
|
+
const pageInfo = response.data.page ?? {};
|
|
361
|
+
return {
|
|
362
|
+
ok: true,
|
|
363
|
+
source: "zhaopin.meituan.com",
|
|
364
|
+
query: body,
|
|
365
|
+
page,
|
|
366
|
+
page_size: pageSize,
|
|
367
|
+
total: pageInfo.totalCount ?? rows.length,
|
|
368
|
+
total_pages: pageInfo.totalPage ?? 1,
|
|
369
|
+
positions: rows.map(summarizePosition),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
export async function fetchAllPositions(opts = {}) {
|
|
373
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
|
|
374
|
+
const maxPages = Math.max(1, opts.maxPages ?? 30);
|
|
375
|
+
const bucket = [];
|
|
376
|
+
let total;
|
|
377
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
378
|
+
const result = await searchPositions({
|
|
379
|
+
keyword: opts.keyword,
|
|
380
|
+
jobTypeCodes: opts.jobTypeCodes,
|
|
381
|
+
cities: opts.cities,
|
|
382
|
+
departments: opts.departments,
|
|
383
|
+
page,
|
|
384
|
+
pageSize,
|
|
385
|
+
});
|
|
386
|
+
if (!result.ok) {
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
message: result.message,
|
|
390
|
+
source: "zhaopin.meituan.com",
|
|
391
|
+
fetched: bucket.length,
|
|
392
|
+
positions: bucket,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (total === undefined)
|
|
396
|
+
total = result.total;
|
|
397
|
+
if (!result.positions.length)
|
|
398
|
+
break;
|
|
399
|
+
bucket.push(...result.positions);
|
|
400
|
+
if (total !== undefined && bucket.length >= total)
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
source: "zhaopin.meituan.com",
|
|
406
|
+
total: total ?? bucket.length,
|
|
407
|
+
fetched: bucket.length,
|
|
408
|
+
positions: bucket,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// ---------- detail ----------
|
|
412
|
+
export async function fetchPositionDetail(postId) {
|
|
413
|
+
const id = (postId ?? "").trim();
|
|
414
|
+
if (!id)
|
|
415
|
+
return { ok: false, message: "post_id is required" };
|
|
416
|
+
const response = await call("POST", "/job/getJobDetail", {
|
|
417
|
+
body: { jobUnionId: id },
|
|
418
|
+
referer: DETAIL_PAGE(id),
|
|
419
|
+
});
|
|
420
|
+
if (!response.ok || !response.data) {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
message: response.message || "no detail returned",
|
|
424
|
+
source: "zhaopin.meituan.com",
|
|
425
|
+
post_id: id,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const raw = response.data;
|
|
429
|
+
const first = (...vals) => {
|
|
430
|
+
for (const v of vals) {
|
|
431
|
+
if (typeof v === "string" && v.trim())
|
|
432
|
+
return v.trim();
|
|
433
|
+
}
|
|
434
|
+
return "";
|
|
435
|
+
};
|
|
436
|
+
return {
|
|
437
|
+
ok: true,
|
|
438
|
+
source: "zhaopin.meituan.com",
|
|
439
|
+
post_id: String(raw.jobUnionId ?? id),
|
|
440
|
+
title: raw.name ?? "",
|
|
441
|
+
direction: first(raw.jobFamily, raw.jobFamilyGroup),
|
|
442
|
+
description: first(raw.jobDuty, raw.desc),
|
|
443
|
+
requirements: first(raw.jobRequirement),
|
|
444
|
+
highlight: first(raw.highLight),
|
|
445
|
+
department_intro: first(raw.departmentIntro),
|
|
446
|
+
work_year: raw.workYear ?? null,
|
|
447
|
+
work_cities: (raw.cityList ?? []).map((c) => c.name ?? "").filter(Boolean),
|
|
448
|
+
recruit_cities: [], // Meituan API does not expose a separate recruit city list
|
|
449
|
+
apply_url: DETAIL_PAGE(String(raw.jobUnionId ?? id)),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
// ---------- dictionaries (synthesized from API probing 2026-05-14) ----------
|
|
453
|
+
/**
|
|
454
|
+
* Returns the full filter taxonomy for zhaopin.meituan.com as of 2026-05-14.
|
|
455
|
+
* There is no single "dictionaries" endpoint — this data was assembled by:
|
|
456
|
+
* 1. Enumerating jobType counts via getJobList.
|
|
457
|
+
* 2. Scanning the entry-main JS bundle for BG codes.
|
|
458
|
+
* 3. Brute-forcing BG001..BG100 to discover all live department codes.
|
|
459
|
+
* 4. Resolving city codes via POST /city/search.
|
|
460
|
+
* jobFamily/jobFamilyGroup are returned as metadata-only (not filterable).
|
|
461
|
+
*/
|
|
462
|
+
export async function fetchDictionaries() {
|
|
463
|
+
return {
|
|
464
|
+
ok: true,
|
|
465
|
+
source: "zhaopin.meituan.com",
|
|
466
|
+
note: "Synthesized from API probing; no single dictionaries endpoint exists. jobFamily/jobFamilyGroup are metadata only — use keyword to narrow by them.",
|
|
467
|
+
jobTypes: [
|
|
468
|
+
{ code: "1", label: "校招应届正式", totalCount: 112,
|
|
469
|
+
jobSpecialCodes: { "1": "普通校招", "3": "特殊/全球岗", "7": "稀有" } },
|
|
470
|
+
{ code: "2", label: "实习", totalCount: 531,
|
|
471
|
+
jobSpecialCodes: { "6": "实习", "1": "其他", "3": "特殊" } },
|
|
472
|
+
{ code: "3", label: "社招", totalCount: 2613,
|
|
473
|
+
jobSpecialCodes: { "5": "社招" } },
|
|
474
|
+
],
|
|
475
|
+
cities: [
|
|
476
|
+
{ code: "001001", name: "北京市", totalCount: 2148 },
|
|
477
|
+
{ code: "001009", name: "上海市", totalCount: 736 },
|
|
478
|
+
{ code: "001019002", name: "深圳市", totalCount: 375 },
|
|
479
|
+
{ code: "001023001", name: "成都市", totalCount: 198 },
|
|
480
|
+
{ code: "001019001", name: "广州市", totalCount: 177 },
|
|
481
|
+
{ code: "001011001", name: "杭州市", totalCount: 142 },
|
|
482
|
+
{ code: "001017001", name: "武汉市", totalCount: 109 },
|
|
483
|
+
{ code: "001010001", name: "南京市", totalCount: 70 },
|
|
484
|
+
{ code: "001027001", name: "西安市", totalCount: 67 },
|
|
485
|
+
{ code: "001010013", name: "苏州市", totalCount: 50 },
|
|
486
|
+
],
|
|
487
|
+
departments: Object.entries(BG_DEPARTMENT_CODES)
|
|
488
|
+
.map(([code, name]) => ({ code, name }))
|
|
489
|
+
.sort((a, b) => a.code.localeCompare(b.code)),
|
|
490
|
+
jobFamilies: {
|
|
491
|
+
note: "Metadata on job objects only — cannot be used as a filter in getJobList.",
|
|
492
|
+
jobFamily: [
|
|
493
|
+
"技术类", "运营类", "产品类", "零售类", "职能类",
|
|
494
|
+
"销售、客服与支持类", "商业分析类", "市场营销类", "设计类",
|
|
495
|
+
],
|
|
496
|
+
jobFamilyGroup: [
|
|
497
|
+
"软件", "算法", "运维", "测试", "硬件", "硬件产品",
|
|
498
|
+
"产品", "产品运营", "用户运营", "业务运营", "内容运营", "商品运营",
|
|
499
|
+
"财务", "人力资源", "供应链", "物流", "门店",
|
|
500
|
+
"销售", "客服", "营销", "市场", "商业分析",
|
|
501
|
+
"业务支持", "采购", "行政", "公司事务", "设计",
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
payloadShapes: {
|
|
505
|
+
cityFilter: "cityList: [{code: '001001', name: '北京市'}] — code is required",
|
|
506
|
+
departmentFilter: "department: [{code: 'BG021'}] — name is ignored by the server",
|
|
507
|
+
jobTypeFilter: "jobType: [{code: '1', subCode: []}, {code: '2', subCode: []}]",
|
|
508
|
+
cityLookup: "POST /api/official/city/search {keyword: '城市名'} → [{code, name}]",
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
export async function listNotices() {
|
|
513
|
+
return { ok: false, message: "Meituan: no public notices endpoint", notices: [] };
|
|
514
|
+
}
|
|
515
|
+
export async function getNotice(_id) {
|
|
516
|
+
return { ok: false, message: "Meituan: no public notice endpoint" };
|
|
517
|
+
}
|
|
518
|
+
export async function findNoticesByQuestion(_q, _opts = {}) {
|
|
519
|
+
return {
|
|
520
|
+
ok: false,
|
|
521
|
+
message: "Meituan: no public notices endpoint",
|
|
522
|
+
matches: [],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
// ---------- resume matching ----------
|
|
526
|
+
export async function matchResume(text, opts = {}) {
|
|
527
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
528
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
529
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
530
|
+
if (!terms.length) {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
message: "could not extract any technical signals from the text",
|
|
534
|
+
preview: (text ?? "").slice(0, 120),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
538
|
+
const list = await searchPositions({
|
|
539
|
+
keyword,
|
|
540
|
+
page: 1,
|
|
541
|
+
pageSize: 100,
|
|
542
|
+
jobTypeCodes: opts.jobTypeCodes,
|
|
543
|
+
});
|
|
544
|
+
if (!list.ok)
|
|
545
|
+
return { ok: false, message: list.message, positions: [] };
|
|
546
|
+
const pre = [];
|
|
547
|
+
for (const p of list.positions) {
|
|
548
|
+
// Use name + project + bgs + work_cities as haystack (desc is in detail only)
|
|
549
|
+
const blob = [p.title, p.project, p.bgs, p.work_cities].join(" ");
|
|
550
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
551
|
+
if (score > 0)
|
|
552
|
+
pre.push({ score, position: p, reasons });
|
|
553
|
+
}
|
|
554
|
+
pre.sort((a, b) => b.score - a.score);
|
|
555
|
+
let shortlist = pre.slice(0, Math.max(topN, candidates));
|
|
556
|
+
if (!shortlist.length) {
|
|
557
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({
|
|
558
|
+
score: 0,
|
|
559
|
+
position,
|
|
560
|
+
reasons: [],
|
|
561
|
+
}));
|
|
562
|
+
}
|
|
563
|
+
const enriched = [];
|
|
564
|
+
for (const { score: baseScore, position, reasons: baseReasons } of shortlist.slice(0, candidates)) {
|
|
565
|
+
const detail = await fetchPositionDetail(position.post_id);
|
|
566
|
+
if (!detail.ok)
|
|
567
|
+
continue;
|
|
568
|
+
const jdBlob = [
|
|
569
|
+
detail.title,
|
|
570
|
+
detail.direction,
|
|
571
|
+
detail.description,
|
|
572
|
+
detail.requirements,
|
|
573
|
+
(detail.work_cities ?? []).join(" "),
|
|
574
|
+
].join(" ");
|
|
575
|
+
const { score: extraScore, reasons: extraReasons } = scoreOverlap(jdBlob, terms, cities);
|
|
576
|
+
const combined = [...new Set([...baseReasons, ...extraReasons])].slice(0, 5);
|
|
577
|
+
if (!combined.length) {
|
|
578
|
+
combined.push("no specific keyword overlap — surfaced from initial keyword search");
|
|
579
|
+
}
|
|
580
|
+
enriched.push({
|
|
581
|
+
score: baseScore + extraScore,
|
|
582
|
+
row: {
|
|
583
|
+
...position,
|
|
584
|
+
title_detail: detail.title,
|
|
585
|
+
direction: detail.direction,
|
|
586
|
+
description: detail.description,
|
|
587
|
+
requirements: detail.requirements,
|
|
588
|
+
match_reasons: combined,
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
enriched.sort((a, b) => b.score - a.score);
|
|
593
|
+
return {
|
|
594
|
+
ok: true,
|
|
595
|
+
source: "zhaopin.meituan.com",
|
|
596
|
+
extracted_terms: terms,
|
|
597
|
+
city_preferences: cities,
|
|
598
|
+
matches: enriched.slice(0, topN).map((e) => e.row),
|
|
599
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
600
|
+
"The only authority on selection is HR.",
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
import { buildBespokeApplySchema as _buildBespokeApplySchema_meituan } from "./apply.js";
|
|
604
|
+
export async function fetchApplicationSchema(postId) {
|
|
605
|
+
const id = (postId ?? "").trim();
|
|
606
|
+
if (!id)
|
|
607
|
+
return { ok: false, source: "zhaopin.meituan.com", message: "post_id is required" };
|
|
608
|
+
let title = "";
|
|
609
|
+
let applyUrl = "https://zhaopin.meituan.com";
|
|
610
|
+
try {
|
|
611
|
+
const detail = (await fetchPositionDetail(id));
|
|
612
|
+
if (detail?.ok === false) {
|
|
613
|
+
return { ok: false, source: "zhaopin.meituan.com", message: detail.message ?? "post not found" };
|
|
614
|
+
}
|
|
615
|
+
title = detail?.title ?? "";
|
|
616
|
+
if (detail?.apply_url)
|
|
617
|
+
applyUrl = detail.apply_url;
|
|
618
|
+
}
|
|
619
|
+
catch { }
|
|
620
|
+
return {
|
|
621
|
+
ok: true,
|
|
622
|
+
schema: _buildBespokeApplySchema_meituan({
|
|
623
|
+
source: "zhaopin.meituan.com",
|
|
624
|
+
postId: id,
|
|
625
|
+
jobTitle: title,
|
|
626
|
+
applyUrl,
|
|
627
|
+
submitEndpoint: "https://zhaopin.meituan.com/api/job-apply",
|
|
628
|
+
submitKind: "multipart-session",
|
|
629
|
+
endpointVerified: true,
|
|
630
|
+
submitNotes: "Meituan — POST /api/job-apply with session cookie. Endpoint anon-probed → {data: {errorCode: 401, message: \"未登陆\"}} (real auth gate). Body shape still needs validation.",
|
|
631
|
+
}),
|
|
632
|
+
};
|
|
633
|
+
}
|