@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/didi.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// Thin client for Didi's public job portal API at talent.didiglobal.com.
|
|
2
|
+
//
|
|
3
|
+
// ============================================================
|
|
4
|
+
// API DISCOVERY NOTES (probed 2026-05):
|
|
5
|
+
//
|
|
6
|
+
// campus.didiglobal.com — Moka white-label campus site (redirects to /campus_apply/didiglobal/96064).
|
|
7
|
+
// All data endpoints return AES-encrypted blobs {"data":"...","necromancer":"..."}.
|
|
8
|
+
// Cannot be decoded without the JS runtime cipher. BLOCKED.
|
|
9
|
+
//
|
|
10
|
+
// talent.didiglobal.com — Didi's self-hosted recruiting portal. Serves all open positions
|
|
11
|
+
// (campus + social hire combined, 1200+ active listings).
|
|
12
|
+
// Public, unauthenticated, no CORS restrictions.
|
|
13
|
+
//
|
|
14
|
+
// talent.didiglobal.com/recruit-portal-service/api/job/front/list — live ✓
|
|
15
|
+
// talent.didiglobal.com/recruit-portal-service/api/job/front/view/{jdId} — live ✓
|
|
16
|
+
// talent.didiglobal.com/recruit-portal-service/api/job/job_locations — live ✓
|
|
17
|
+
// talent.didiglobal.com/recruit-portal-service/api/job/jdpublish/confirm/listJdTypes — live ✓
|
|
18
|
+
//
|
|
19
|
+
// ============================================================
|
|
20
|
+
// Endpoint: GET /recruit-portal-service/api/job/front/list
|
|
21
|
+
// Query params:
|
|
22
|
+
// jobName — keyword filter (URL-encoded, e.g. "算法")
|
|
23
|
+
// workArea — city name filter, e.g. "北京市" (from /api/job/job_locations list)
|
|
24
|
+
// jobType — job category code (integer, see taxonomy below)
|
|
25
|
+
// recruitType — declared but NOT enforced server-side; returns same 1213 regardless of value
|
|
26
|
+
// page — 1-indexed page number
|
|
27
|
+
// size — page size; server ignores values != 16 and always returns 16 items/page
|
|
28
|
+
// Response: { meta:{api,method,code:0,message}, data:{total,items:[...],page,size} }
|
|
29
|
+
//
|
|
30
|
+
// ============================================================
|
|
31
|
+
// Filter taxonomy (verified 2026-05):
|
|
32
|
+
//
|
|
33
|
+
// jobType codes (from GET /api/job/jdpublish/confirm/listJdTypes):
|
|
34
|
+
// 1=技术 (~416) 2=设计 (~20) 3=产品 (~101) 4=数据 (~68)
|
|
35
|
+
// 5=运营 (~382) 6=销售 (~54) 7=客服 9=市场 (~18)
|
|
36
|
+
// 10=人力 (~18) 11=行政 12=财务 13=法务
|
|
37
|
+
// 14=公关 15=战略 16=风控 18=安全 (~47)
|
|
38
|
+
// 19=供应链 20=采购
|
|
39
|
+
//
|
|
40
|
+
// workArea city values (from GET /api/job/job_locations, 52 total):
|
|
41
|
+
// Top cities (2026-05): 北京市 (~838) 深圳市 上海市 杭州市 成都市 广州市
|
|
42
|
+
// Also: 武汉市 天津市 南京市 西安市 重庆市 厦门市 香港岛 九龙
|
|
43
|
+
// International: Mexico City Sao Paulo
|
|
44
|
+
//
|
|
45
|
+
// ============================================================
|
|
46
|
+
// Site URL pattern for campus-tab positions (talent.didiglobal.com):
|
|
47
|
+
// The portal has four tabs: 社会招聘 (social) / 校园招聘 (campus) / 实习生招聘 (intern) / 内推
|
|
48
|
+
// The API returns all listings without tab-level filtering — both campus (JR-prefix jdNo) and
|
|
49
|
+
// social (J-prefix jdNo) positions are included in every response.
|
|
50
|
+
// There is no public API filter to isolate campus-only listings.
|
|
51
|
+
// The campus.didiglobal.com (Moka) site would expose campus-only data but uses client-side AES
|
|
52
|
+
// encryption that cannot be bypassed without executing Moka's JavaScript.
|
|
53
|
+
//
|
|
54
|
+
// ============================================================
|
|
55
|
+
// Page size is always 16 (server-enforced). To fetch more positions use fetchAllPositions()
|
|
56
|
+
// which paginates up to maxPages.
|
|
57
|
+
//
|
|
58
|
+
// ============================================================
|
|
59
|
+
// ---- PositionSummary field mapping (Didi → canonical) ----
|
|
60
|
+
// post_id ← jdId (stringified) or jdNo as fallback
|
|
61
|
+
// title ← jobName (stripped of trailing "(jdNo)" suffix that Didi appends)
|
|
62
|
+
// project ← deptName (closest to Tencent's projectName / BG)
|
|
63
|
+
// recruit_label ← "" (recruitType field exists in /view but not in list response; campus vs social
|
|
64
|
+
// cannot be distinguished from the list API)
|
|
65
|
+
// bgs ← "" (Didi does not expose BG / 事业群 in public search)
|
|
66
|
+
// work_cities ← workArea
|
|
67
|
+
// apply_url ← https://talent.didiglobal.com/campus#/position/{jdId}/detail
|
|
68
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
69
|
+
export { checkResume };
|
|
70
|
+
const API_ROOT = "https://talent.didiglobal.com/recruit-portal-service/api";
|
|
71
|
+
const PORTAL_PAGE = "https://talent.didiglobal.com/";
|
|
72
|
+
const DETAIL_PAGE = (jdId) => `https://talent.didiglobal.com/campus#/position/${encodeURIComponent(jdId)}/detail`;
|
|
73
|
+
const DEFAULT_HEADERS = {
|
|
74
|
+
"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",
|
|
75
|
+
Accept: "application/json, text/plain, */*",
|
|
76
|
+
Referer: PORTAL_PAGE,
|
|
77
|
+
};
|
|
78
|
+
async function call(path, params = {}) {
|
|
79
|
+
// Build query string — omit undefined values
|
|
80
|
+
const qs = Object.entries(params)
|
|
81
|
+
.filter(([, v]) => v !== undefined && v !== "")
|
|
82
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
83
|
+
.join("&");
|
|
84
|
+
const url = `${API_ROOT}${path}${qs ? "?" + qs : ""}`;
|
|
85
|
+
let response;
|
|
86
|
+
try {
|
|
87
|
+
response = await fetch(url, { headers: DEFAULT_HEADERS });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
97
|
+
}
|
|
98
|
+
let payload;
|
|
99
|
+
try {
|
|
100
|
+
payload = (await response.json());
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
104
|
+
}
|
|
105
|
+
const code = payload.meta?.code ?? 0;
|
|
106
|
+
return {
|
|
107
|
+
ok: code === 0,
|
|
108
|
+
data: payload.data,
|
|
109
|
+
message: payload.meta?.message || (code === 0 ? "ok" : "upstream error"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Strip the "(JR2026XXXXXXX)" suffix that Didi appends to jobName in the list endpoint. */
|
|
113
|
+
function stripJdNoSuffix(jobName, jdNo) {
|
|
114
|
+
if (!jdNo)
|
|
115
|
+
return jobName;
|
|
116
|
+
const suffix = ` (${jdNo})`;
|
|
117
|
+
return jobName.endsWith(suffix) ? jobName.slice(0, -suffix.length) : jobName;
|
|
118
|
+
}
|
|
119
|
+
function summarizePosition(item) {
|
|
120
|
+
const jdId = String(item.jdId ?? "");
|
|
121
|
+
const rawName = item.jobName ?? "";
|
|
122
|
+
const title = stripJdNoSuffix(rawName, item.jdNo);
|
|
123
|
+
return {
|
|
124
|
+
post_id: jdId || (item.jdNo ?? ""),
|
|
125
|
+
title,
|
|
126
|
+
project: item.deptName ?? "",
|
|
127
|
+
recruit_label: "", // not available in list response
|
|
128
|
+
bgs: "",
|
|
129
|
+
work_cities: (item.workArea ?? "").trim(),
|
|
130
|
+
apply_url: jdId ? DETAIL_PAGE(jdId) : PORTAL_PAGE,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// ---------- searchPositions ----------
|
|
134
|
+
export async function searchPositions(opts = {}) {
|
|
135
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
136
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
137
|
+
const params = {
|
|
138
|
+
page,
|
|
139
|
+
size: 16, // server enforces 16; pass it explicitly for clarity
|
|
140
|
+
...(keyword ? { jobName: keyword } : {}),
|
|
141
|
+
...(opts.workArea ? { workArea: opts.workArea } : {}),
|
|
142
|
+
...(opts.jobType !== undefined ? { jobType: opts.jobType } : {}),
|
|
143
|
+
};
|
|
144
|
+
const response = await call("/job/front/list", params);
|
|
145
|
+
if (!response.ok || !response.data) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
message: response.message,
|
|
149
|
+
source: "talent.didiglobal.com",
|
|
150
|
+
query: params,
|
|
151
|
+
total: 0,
|
|
152
|
+
positions: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const rows = response.data.items ?? [];
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
source: "talent.didiglobal.com",
|
|
159
|
+
query: params,
|
|
160
|
+
page,
|
|
161
|
+
page_size: rows.length, // actual count (always ≤ 16)
|
|
162
|
+
total: response.data.total ?? rows.length,
|
|
163
|
+
positions: rows.map(summarizePosition),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// ---------- fetchAllPositions ----------
|
|
167
|
+
export async function fetchAllPositions(opts = {}) {
|
|
168
|
+
const maxPages = Math.max(1, opts.maxPages ?? 10); // default: up to 160 posts
|
|
169
|
+
const bucket = [];
|
|
170
|
+
let total;
|
|
171
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
172
|
+
const result = await searchPositions({ ...opts, page });
|
|
173
|
+
if (!result.ok) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
message: result.message,
|
|
177
|
+
source: "talent.didiglobal.com",
|
|
178
|
+
fetched: bucket.length,
|
|
179
|
+
positions: bucket,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (total === undefined)
|
|
183
|
+
total = result.total;
|
|
184
|
+
if (!result.positions.length)
|
|
185
|
+
break;
|
|
186
|
+
bucket.push(...result.positions);
|
|
187
|
+
if (total !== undefined && bucket.length >= total)
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
source: "talent.didiglobal.com",
|
|
193
|
+
total: total ?? bucket.length,
|
|
194
|
+
fetched: bucket.length,
|
|
195
|
+
positions: bucket,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// ---------- fetchPositionDetail ----------
|
|
199
|
+
export async function fetchPositionDetail(postId) {
|
|
200
|
+
const id = (postId ?? "").trim();
|
|
201
|
+
if (!id)
|
|
202
|
+
return { ok: false, source: "talent.didiglobal.com", message: "post_id is required" };
|
|
203
|
+
const response = await call(`/job/front/view/${encodeURIComponent(id)}`);
|
|
204
|
+
if (!response.ok || !response.data) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
source: "talent.didiglobal.com",
|
|
208
|
+
post_id: id,
|
|
209
|
+
message: response.message || "no detail returned",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const raw = response.data;
|
|
213
|
+
const jdId = String(raw.jdId ?? id);
|
|
214
|
+
const rawName = raw.jobName ?? "";
|
|
215
|
+
const title = stripJdNoSuffix(rawName, raw.jdNo);
|
|
216
|
+
return {
|
|
217
|
+
ok: true,
|
|
218
|
+
source: "talent.didiglobal.com",
|
|
219
|
+
post_id: jdId,
|
|
220
|
+
jd_no: raw.jdNo ?? "",
|
|
221
|
+
title,
|
|
222
|
+
project: raw.deptName ?? "",
|
|
223
|
+
recruit_label: raw.recruitType ?? "",
|
|
224
|
+
description: raw.jobDesc ?? "",
|
|
225
|
+
requirements: raw.qualification ?? "",
|
|
226
|
+
work_cities: (raw.workArea ?? "").trim(),
|
|
227
|
+
job_type: raw.jobType ?? "",
|
|
228
|
+
recruit_num: raw.recruitNum ?? null,
|
|
229
|
+
publish_time: raw.publishTime ?? "",
|
|
230
|
+
apply_url: DETAIL_PAGE(jdId),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// ---------- fetchDictionaries ----------
|
|
234
|
+
export async function fetchDictionaries() {
|
|
235
|
+
const [locations, jobTypes] = await Promise.all([
|
|
236
|
+
call("/job/job_locations"),
|
|
237
|
+
call("/job/jdpublish/confirm/listJdTypes"),
|
|
238
|
+
]);
|
|
239
|
+
return {
|
|
240
|
+
ok: locations.ok && jobTypes.ok,
|
|
241
|
+
source: "talent.didiglobal.com",
|
|
242
|
+
cities: locations.data ?? [],
|
|
243
|
+
job_types: (jobTypes.data ?? []).map((jt) => ({ code: jt.code, name: jt.name })),
|
|
244
|
+
note: [
|
|
245
|
+
"cities: pass as workArea filter (exact string match, e.g. '北京市').",
|
|
246
|
+
"job_types: pass as jobType filter (integer code, e.g. 1 for 技术).",
|
|
247
|
+
"recruitType filter is declared but NOT enforced — all values return the full dataset.",
|
|
248
|
+
"Page size is server-fixed at 16 items/page regardless of size param.",
|
|
249
|
+
].join(" "),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// ---------- stub notices ----------
|
|
253
|
+
const STUB_NOTICES = {
|
|
254
|
+
ok: false,
|
|
255
|
+
source: "talent.didiglobal.com",
|
|
256
|
+
message: "Didi: no public notices/announcements endpoint on talent.didiglobal.com",
|
|
257
|
+
};
|
|
258
|
+
export async function listNotices() {
|
|
259
|
+
return STUB_NOTICES;
|
|
260
|
+
}
|
|
261
|
+
export async function getNotice(_id) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
source: "talent.didiglobal.com",
|
|
265
|
+
message: "Didi: no public notices endpoint",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
source: "talent.didiglobal.com",
|
|
272
|
+
message: "Didi: no public notices endpoint",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// ---------- matchResume ----------
|
|
276
|
+
export async function matchResume(text, opts = {}) {
|
|
277
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
278
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
279
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
280
|
+
if (!terms.length) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
source: "talent.didiglobal.com",
|
|
284
|
+
message: "could not extract any technical signals from the text",
|
|
285
|
+
preview: (text ?? "").slice(0, 120),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
289
|
+
const list = await searchPositions({ keyword, page: 1 });
|
|
290
|
+
if (!list.ok) {
|
|
291
|
+
return { ok: false, source: "talent.didiglobal.com", message: list.message, positions: [] };
|
|
292
|
+
}
|
|
293
|
+
const scored = [];
|
|
294
|
+
for (const p of list.positions) {
|
|
295
|
+
const blob = [p.title, p.project, p.recruit_label, p.work_cities].join(" ");
|
|
296
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
297
|
+
if (score > 0) {
|
|
298
|
+
scored.push({ score, position: p, reasons });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
scored.sort((a, b) => b.score - a.score);
|
|
302
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
303
|
+
if (!shortlist.length) {
|
|
304
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({
|
|
305
|
+
score: 0,
|
|
306
|
+
position,
|
|
307
|
+
reasons: [],
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
const enriched = [];
|
|
311
|
+
for (const entry of shortlist.slice(0, candidates)) {
|
|
312
|
+
const detail = await fetchPositionDetail(entry.position.post_id);
|
|
313
|
+
if (detail.ok) {
|
|
314
|
+
const jdBlob = [detail.description, detail.requirements, detail.work_cities].join(" ");
|
|
315
|
+
const { score: extraScore, reasons: extraReasons } = scoreOverlap(jdBlob, terms, cities);
|
|
316
|
+
const combined = [...new Set([...entry.reasons, ...extraReasons])].slice(0, 5);
|
|
317
|
+
enriched.push({
|
|
318
|
+
...entry,
|
|
319
|
+
score: entry.score + extraScore,
|
|
320
|
+
reasons: combined,
|
|
321
|
+
description: detail.description,
|
|
322
|
+
requirements: detail.requirements,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
enriched.push(entry);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
enriched.sort((a, b) => b.score - a.score);
|
|
330
|
+
const matches = enriched.slice(0, topN).map((s) => {
|
|
331
|
+
const mr = s.reasons.length > 0
|
|
332
|
+
? s.reasons.slice(0, 5)
|
|
333
|
+
: ["no specific keyword overlap — surfaced from initial keyword search"];
|
|
334
|
+
return {
|
|
335
|
+
...s.position,
|
|
336
|
+
description: s.description,
|
|
337
|
+
requirements: s.requirements,
|
|
338
|
+
match_reasons: mr,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
ok: true,
|
|
343
|
+
source: "talent.didiglobal.com",
|
|
344
|
+
extracted_terms: terms,
|
|
345
|
+
city_preferences: cities,
|
|
346
|
+
matches,
|
|
347
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
348
|
+
"The only authority on selection is HR.",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
import { buildBespokeApplySchema as _buildBespokeApplySchema_didi } from "./apply.js";
|
|
352
|
+
export async function fetchApplicationSchema(postId) {
|
|
353
|
+
const id = (postId ?? "").trim();
|
|
354
|
+
if (!id)
|
|
355
|
+
return { ok: false, source: "talent.didiglobal.com", message: "post_id is required" };
|
|
356
|
+
let title = "";
|
|
357
|
+
let applyUrl = "https://talent.didiglobal.com";
|
|
358
|
+
try {
|
|
359
|
+
const detail = (await fetchPositionDetail(id));
|
|
360
|
+
if (detail?.ok === false) {
|
|
361
|
+
return { ok: false, source: "talent.didiglobal.com", message: detail.message ?? "post not found" };
|
|
362
|
+
}
|
|
363
|
+
title = detail?.title ?? "";
|
|
364
|
+
if (detail?.apply_url)
|
|
365
|
+
applyUrl = detail.apply_url;
|
|
366
|
+
}
|
|
367
|
+
catch { }
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
schema: _buildBespokeApplySchema_didi({
|
|
371
|
+
source: "talent.didiglobal.com",
|
|
372
|
+
postId: id,
|
|
373
|
+
jobTitle: title,
|
|
374
|
+
applyUrl,
|
|
375
|
+
submitEndpoint: "https://talent.didiglobal.com/talent-api/applyResume",
|
|
376
|
+
submitKind: "multipart-session",
|
|
377
|
+
endpointVerified: true,
|
|
378
|
+
submitNotes: "Didi — POST /talent-api/applyResume with session cookie. Endpoint anon-probed → HTTP 405 + Nginx page (routing table has this URL; the backend rejects POST without session/CSRF, not 404). Body shape still needs validation.",
|
|
379
|
+
}),
|
|
380
|
+
};
|
|
381
|
+
}
|