@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/trip.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// Thin client for Trip.com / Ctrip (携程) public campus-recruiting API.
|
|
2
|
+
//
|
|
3
|
+
// Both portals are backed by the same API server:
|
|
4
|
+
// careers.ctrip.com — Chinese domestic portal (携程招聘)
|
|
5
|
+
// careers.trip.com — International portal (Trip.com Group Careers)
|
|
6
|
+
//
|
|
7
|
+
// This adapter targets careers.ctrip.com since it hosts the authoritative
|
|
8
|
+
// Chinese campus job feed. All JSON endpoints are unauthenticated; the server
|
|
9
|
+
// validates the presence of a mandatory `condition` wrapper in the POST body.
|
|
10
|
+
//
|
|
11
|
+
// ============================================================
|
|
12
|
+
// Endpoint inventory (probed 2026-05, JS bundle main.ad2ffe67.js):
|
|
13
|
+
//
|
|
14
|
+
// POST https://careers.ctrip.com/api/hrrecruit/getJobAd
|
|
15
|
+
// Payload (all fields inside a "condition" key):
|
|
16
|
+
// { condition: {
|
|
17
|
+
// pageIndex: <int>, // 1-based
|
|
18
|
+
// pageSize: <int>, // max tested: 100
|
|
19
|
+
// category: "2", // "2"=校招/campus, "1"=社招/social hire
|
|
20
|
+
// searchText: <string>, // keyword filter (free-text)
|
|
21
|
+
// city: <string>, // e.g. "CO0009" = Shanghai
|
|
22
|
+
// jobFamilyGroupCode: n/a // rejected with 202 — do not send
|
|
23
|
+
// } }
|
|
24
|
+
// Response: { retCode:"201", retMessage:"调用成功",
|
|
25
|
+
// retValue:{ total:<int>, recruitJobAdList:[...] } }
|
|
26
|
+
// retCode "201" = success (not HTTP 201).
|
|
27
|
+
// retCode "501" = validation error (missing `condition`).
|
|
28
|
+
// retCode "202" = data-validation error (bad field value).
|
|
29
|
+
//
|
|
30
|
+
// POST https://careers.ctrip.com/api/hrrecruit/getJobCount
|
|
31
|
+
// Payload: { source:"ctrip" }
|
|
32
|
+
// Response: retValue: [{categoryCode:"Categroy_1",total:44}, ...]
|
|
33
|
+
// Used for statistics only; not required for job search.
|
|
34
|
+
//
|
|
35
|
+
// IMPORTANT QUIRKS:
|
|
36
|
+
// 1. The `keyword` field (inside condition) crashes the server with a
|
|
37
|
+
// NullPointerException when combined with pagination. Use `searchText`
|
|
38
|
+
// instead — it is the working search field.
|
|
39
|
+
// 2. Combining `searchText` with `category` is accepted by the server but
|
|
40
|
+
// the server ignores searchText (returns all campus results). Keyword
|
|
41
|
+
// filtering therefore works only without the category filter.
|
|
42
|
+
// Practical consequence: when campus=true, keyword is applied client-side
|
|
43
|
+
// on the title after fetching the full campus set.
|
|
44
|
+
// 3. `category:"2"` (校招/fresh graduates) gives ~112 positions;
|
|
45
|
+
// no intern-only category exists (intern jobs appear mixed inside category 1
|
|
46
|
+
// or surface via keyword "实习" across all listings).
|
|
47
|
+
//
|
|
48
|
+
// ============================================================
|
|
49
|
+
// Field mapping (API response → PositionSummary)
|
|
50
|
+
// post_id ← item.id (numeric string, e.g. "27655163")
|
|
51
|
+
// title ← item.jobTitle (may include code suffix "(MJ034955)")
|
|
52
|
+
// project ← item.jobFamilyGroupName (e.g. "Software development")
|
|
53
|
+
// recruit_label ← item.kindName (e.g. "Fresh Graduates")
|
|
54
|
+
// bgs ← item.buName (BU = Business Unit, e.g. "International business")
|
|
55
|
+
// work_cities ← item.cityName
|
|
56
|
+
// apply_url ← https://careers.ctrip.com/campus/job-detail/<jobId>
|
|
57
|
+
// (uses UUID `jobId`, not numeric `id`)
|
|
58
|
+
//
|
|
59
|
+
// ============================================================
|
|
60
|
+
// Category/filter values probed 2026-05:
|
|
61
|
+
// category "1" = 社招 (social/experienced hire) ~657 positions
|
|
62
|
+
// category "2" = 校招 (campus / fresh graduates) ~112 positions
|
|
63
|
+
// No category (omit field) = all listings ~769 positions
|
|
64
|
+
//
|
|
65
|
+
// City codes (from item.city in responses):
|
|
66
|
+
// CO0009 = Shanghai CO0001 = Beijing CO0013 = Xiamen
|
|
67
|
+
// CO0004 = Shenzhen CO0006 = Chengdu (+ many others not enumerated)
|
|
68
|
+
//
|
|
69
|
+
// jobFamilyGroupName values seen in responses:
|
|
70
|
+
// "Software development", "Admin", "Business development",
|
|
71
|
+
// "Marketing & PR", "Finance", "Data & Analytics", "Product management"
|
|
72
|
+
//
|
|
73
|
+
// ============================================================
|
|
74
|
+
// Workday dead-end investigation:
|
|
75
|
+
// trip.wd1.myworkdayjobs.com — resolves and is behind Cloudflare but
|
|
76
|
+
// all POST attempts to /wday/cxs/trip/<slug>/jobs return HTTP 422 (no slug
|
|
77
|
+
// identifiable without an active UI page). The Workday tenant appears to be
|
|
78
|
+
// a legacy artifact from Trip.com's international hiring pre-2024.
|
|
79
|
+
// Not used in this adapter.
|
|
80
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
81
|
+
export { checkResume };
|
|
82
|
+
const API_ROOT = "https://careers.ctrip.com/api/hrrecruit";
|
|
83
|
+
const CAMPUS_PAGE = "https://careers.ctrip.com/campus";
|
|
84
|
+
const DETAIL_PAGE = (jobId) => `https://careers.ctrip.com/campus/job-detail/${encodeURIComponent(jobId)}`;
|
|
85
|
+
const DEFAULT_HEADERS = {
|
|
86
|
+
"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",
|
|
87
|
+
Accept: "application/json, text/plain, */*",
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
Origin: "https://careers.ctrip.com",
|
|
90
|
+
Referer: CAMPUS_PAGE,
|
|
91
|
+
};
|
|
92
|
+
async function call(path, body) {
|
|
93
|
+
const url = `${API_ROOT}${path}`;
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: DEFAULT_HEADERS,
|
|
99
|
+
body: JSON.stringify(body),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
message: `network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
110
|
+
}
|
|
111
|
+
let payload;
|
|
112
|
+
try {
|
|
113
|
+
payload = (await response.json());
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
117
|
+
}
|
|
118
|
+
// retCode "201" = success; any other value is an error.
|
|
119
|
+
const ok = payload.retCode === "201";
|
|
120
|
+
return {
|
|
121
|
+
ok,
|
|
122
|
+
data: ok ? payload.retValue : undefined,
|
|
123
|
+
message: payload.retMessage || (ok ? "ok" : `upstream error (code ${payload.retCode})`),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function summarizePosition(item) {
|
|
127
|
+
const id = String(item.id ?? "");
|
|
128
|
+
const jobId = item.jobId ?? "";
|
|
129
|
+
return {
|
|
130
|
+
post_id: id,
|
|
131
|
+
title: item.jobTitle ?? "",
|
|
132
|
+
project: item.jobFamilyGroupName ?? "",
|
|
133
|
+
recruit_label: item.kindName ?? "",
|
|
134
|
+
bgs: (item.buName ?? "").trim(),
|
|
135
|
+
work_cities: item.cityName ?? "",
|
|
136
|
+
apply_url: jobId ? DETAIL_PAGE(jobId) : CAMPUS_PAGE,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ---------- searchPositions ----------
|
|
140
|
+
export async function searchPositions(opts = {}) {
|
|
141
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
|
|
142
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
143
|
+
const keyword = (opts.keyword ?? "").trim().slice(0, 60);
|
|
144
|
+
const campusOnly = opts.campusOnly !== false; // default true
|
|
145
|
+
// Build the condition object.
|
|
146
|
+
// NOTE: `keyword` crashes the server with a NullPointerException when combined
|
|
147
|
+
// with pagination; use `searchText` for safe text search. However, when
|
|
148
|
+
// `category` is also set, the server silently ignores `searchText`, so keyword
|
|
149
|
+
// filtering is applied client-side after the response is received.
|
|
150
|
+
const condition = {
|
|
151
|
+
pageIndex: page,
|
|
152
|
+
pageSize,
|
|
153
|
+
};
|
|
154
|
+
if (campusOnly) {
|
|
155
|
+
condition.category = "2";
|
|
156
|
+
// searchText is ignored by server when category is set; skip it to avoid confusion
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Without category filter, searchText works correctly
|
|
160
|
+
if (keyword)
|
|
161
|
+
condition.searchText = keyword;
|
|
162
|
+
}
|
|
163
|
+
if (opts.cityCode?.trim()) {
|
|
164
|
+
condition.city = opts.cityCode.trim();
|
|
165
|
+
}
|
|
166
|
+
const response = await call("/getJobAd", { condition });
|
|
167
|
+
if (!response.ok || !response.data) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
message: response.message,
|
|
171
|
+
source: "careers.ctrip.com",
|
|
172
|
+
query: condition,
|
|
173
|
+
positions: [],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
let rows = response.data.recruitJobAdList ?? [];
|
|
177
|
+
// Client-side keyword filter when campusOnly is active (server ignores searchText in that mode)
|
|
178
|
+
if (campusOnly && keyword) {
|
|
179
|
+
const lk = keyword.toLowerCase();
|
|
180
|
+
rows = rows.filter((r) => (r.jobTitle ?? "").toLowerCase().includes(lk));
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
source: "careers.ctrip.com",
|
|
185
|
+
query: condition,
|
|
186
|
+
page,
|
|
187
|
+
page_size: pageSize,
|
|
188
|
+
total: campusOnly && keyword ? rows.length : (response.data.total ?? rows.length),
|
|
189
|
+
positions: rows.map(summarizePosition),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// ---------- fetchAllPositions ----------
|
|
193
|
+
export async function fetchAllPositions(opts = {}) {
|
|
194
|
+
const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
|
|
195
|
+
const maxPages = Math.max(1, opts.maxPages ?? 5);
|
|
196
|
+
const bucket = [];
|
|
197
|
+
let total;
|
|
198
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
199
|
+
const result = await searchPositions({ ...opts, page, pageSize });
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
message: result.message,
|
|
204
|
+
source: "careers.ctrip.com",
|
|
205
|
+
fetched: bucket.length,
|
|
206
|
+
positions: bucket,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (total === undefined)
|
|
210
|
+
total = result.total;
|
|
211
|
+
if (!result.positions.length)
|
|
212
|
+
break;
|
|
213
|
+
bucket.push(...result.positions);
|
|
214
|
+
if (total !== undefined && bucket.length >= total)
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
ok: true,
|
|
219
|
+
source: "careers.ctrip.com",
|
|
220
|
+
total: total ?? bucket.length,
|
|
221
|
+
fetched: bucket.length,
|
|
222
|
+
positions: bucket,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// ---------- fetchPositionDetail ----------
|
|
226
|
+
// The API exposes the full `requirements` HTML in the search response itself,
|
|
227
|
+
// so detail is derived from the search list without a separate round-trip.
|
|
228
|
+
// We page through the campus listing to find the matching id.
|
|
229
|
+
export async function fetchPositionDetail(postId) {
|
|
230
|
+
const id = (postId ?? "").trim();
|
|
231
|
+
if (!id)
|
|
232
|
+
return { ok: false, source: "careers.ctrip.com", message: "post_id is required" };
|
|
233
|
+
const pageSize = 100;
|
|
234
|
+
const maxPages = 5;
|
|
235
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
236
|
+
const condition = { pageIndex: page, pageSize, category: "2" };
|
|
237
|
+
const resp = await call("/getJobAd", { condition });
|
|
238
|
+
if (!resp.ok || !resp.data)
|
|
239
|
+
break;
|
|
240
|
+
const items = resp.data.recruitJobAdList ?? [];
|
|
241
|
+
const found = items.find((p) => String(p.id) === id);
|
|
242
|
+
if (found) {
|
|
243
|
+
const summary = summarizePosition(found);
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
source: "careers.ctrip.com",
|
|
247
|
+
post_id: id,
|
|
248
|
+
job_id: found.jobId ?? "",
|
|
249
|
+
title: found.jobTitle ?? "",
|
|
250
|
+
requirements_html: found.requirements ?? "",
|
|
251
|
+
recruit_label: found.kindName ?? "",
|
|
252
|
+
job_family: found.jobFamilyGroupName ?? "",
|
|
253
|
+
bu: found.buName ?? "",
|
|
254
|
+
city: found.cityName ?? "",
|
|
255
|
+
publish_date: found.publishDate ?? "",
|
|
256
|
+
apply_url: summary.apply_url,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (items.length < pageSize)
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
source: "careers.ctrip.com",
|
|
265
|
+
post_id: id,
|
|
266
|
+
message: `post ${id} not found in campus search results (searched up to ${maxPages * pageSize} posts)`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// ---------- fetchDictionaries ----------
|
|
270
|
+
export async function fetchDictionaries() {
|
|
271
|
+
// getJobCount returns a breakdown by internal category code; not a full
|
|
272
|
+
// taxonomy, but useful for getting totals.
|
|
273
|
+
const response = await call("/getJobCount", { source: "ctrip" });
|
|
274
|
+
const knownCategories = [
|
|
275
|
+
{ category: "2", label: "校招 / Campus (Fresh Graduates)", note: "~112 positions as of 2026-05" },
|
|
276
|
+
{ category: "1", label: "社招 / Social (Experienced Hire)", note: "~657 positions" },
|
|
277
|
+
];
|
|
278
|
+
return {
|
|
279
|
+
ok: response.ok,
|
|
280
|
+
source: "careers.ctrip.com",
|
|
281
|
+
campus_page: CAMPUS_PAGE,
|
|
282
|
+
categories: knownCategories,
|
|
283
|
+
job_count_by_family: response.ok ? (response.data ?? []) : [],
|
|
284
|
+
message: response.ok ? "ok" : response.message,
|
|
285
|
+
note: "Filter taxonomy: use category='2' for campus jobs in searchPositions(). " +
|
|
286
|
+
"City codes are in item.city of API responses (e.g. CO0009=Shanghai, CO0001=Beijing).",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// ---------- notices (no public endpoint) ----------
|
|
290
|
+
const STUB_NOTICE = {
|
|
291
|
+
ok: false,
|
|
292
|
+
source: "careers.ctrip.com",
|
|
293
|
+
message: "Trip.com / Ctrip: no public notices/announcements endpoint",
|
|
294
|
+
};
|
|
295
|
+
export async function listNotices() {
|
|
296
|
+
return STUB_NOTICE;
|
|
297
|
+
}
|
|
298
|
+
export async function getNotice(_id) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
source: "careers.ctrip.com",
|
|
302
|
+
message: "Trip.com / Ctrip: no public notices endpoint",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
source: "careers.ctrip.com",
|
|
309
|
+
message: "Trip.com / Ctrip: no public notices endpoint",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// ---------- matchResume ----------
|
|
313
|
+
export async function matchResume(text, opts = {}) {
|
|
314
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
315
|
+
const candidates = Math.max(topN, opts.candidates ?? 50);
|
|
316
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
317
|
+
if (!terms.length) {
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
source: "careers.ctrip.com",
|
|
321
|
+
message: "could not extract any technical signals from the text",
|
|
322
|
+
preview: (text ?? "").slice(0, 120),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// Fetch campus listings. Keyword is applied client-side when campusOnly=true.
|
|
326
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
327
|
+
const list = await fetchAllPositions({ campusOnly: true, pageSize: 100, maxPages: 2 });
|
|
328
|
+
if (!list.ok) {
|
|
329
|
+
return { ok: false, source: "careers.ctrip.com", message: list.message, positions: [] };
|
|
330
|
+
}
|
|
331
|
+
const scored = [];
|
|
332
|
+
for (const p of list.positions) {
|
|
333
|
+
const blob = [p.title, p.project, p.recruit_label, p.bgs, p.work_cities].join(" ");
|
|
334
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
335
|
+
if (score > 0)
|
|
336
|
+
scored.push({ score, position: p, reasons });
|
|
337
|
+
}
|
|
338
|
+
scored.sort((a, b) => b.score - a.score);
|
|
339
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
340
|
+
if (!shortlist.length) {
|
|
341
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({
|
|
342
|
+
score: 0,
|
|
343
|
+
position,
|
|
344
|
+
reasons: [],
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
348
|
+
const mr = s.reasons.length > 0
|
|
349
|
+
? s.reasons.slice(0, 5)
|
|
350
|
+
: ["no specific keyword overlap — surfaced from campus listing"];
|
|
351
|
+
return { ...s.position, match_reasons: mr };
|
|
352
|
+
});
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
source: "careers.ctrip.com",
|
|
356
|
+
extracted_terms: terms,
|
|
357
|
+
city_preferences: cities,
|
|
358
|
+
keyword_used: keyword,
|
|
359
|
+
matches,
|
|
360
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
361
|
+
"The only authority on selection is HR.",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// Export helpers so other modules can import them from trip.js
|
|
365
|
+
export { extractResumeSignals, scoreOverlap };
|
|
366
|
+
import { buildBespokeApplySchema as _buildBespokeApplySchema_trip } from "./apply.js";
|
|
367
|
+
export async function fetchApplicationSchema(postId) {
|
|
368
|
+
const id = (postId ?? "").trim();
|
|
369
|
+
if (!id)
|
|
370
|
+
return { ok: false, source: "careers.ctrip.com", message: "post_id is required" };
|
|
371
|
+
let title = "";
|
|
372
|
+
let applyUrl = "https://careers.ctrip.com";
|
|
373
|
+
try {
|
|
374
|
+
const detail = (await fetchPositionDetail(id));
|
|
375
|
+
if (detail?.ok === false) {
|
|
376
|
+
return { ok: false, source: "careers.ctrip.com", message: detail.message ?? "post not found" };
|
|
377
|
+
}
|
|
378
|
+
title = detail?.title ?? "";
|
|
379
|
+
if (detail?.apply_url)
|
|
380
|
+
applyUrl = detail.apply_url;
|
|
381
|
+
}
|
|
382
|
+
catch { }
|
|
383
|
+
return {
|
|
384
|
+
ok: true,
|
|
385
|
+
schema: _buildBespokeApplySchema_trip({
|
|
386
|
+
source: "careers.ctrip.com",
|
|
387
|
+
postId: id,
|
|
388
|
+
jobTitle: title,
|
|
389
|
+
applyUrl,
|
|
390
|
+
submitEndpoint: "https://careers.ctrip.com/api/hrrecruit/applyJob",
|
|
391
|
+
submitKind: "multipart-session",
|
|
392
|
+
endpointVerified: true,
|
|
393
|
+
submitNotes: "Trip.com — POST /api/hrrecruit/applyJob with session cookie. Endpoint extracted from the careers SPA main.ad2ffe67.js bundle (sibling routes /api/hrrecruit/getJobAd, /getLoginInfo, /getNewsDetail, etc). Anon-probed → HTTP 200 + Ctrip ResponseStatus envelope {Ack:\"Success\", retCode:\"402\", retMessage:\"没有当前用户\"} — real API, auth-gated. Body shape still needs validation.",
|
|
394
|
+
}),
|
|
395
|
+
};
|
|
396
|
+
}
|