job-pro 0.4.0 → 0.6.0

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.
@@ -0,0 +1,201 @@
1
+ // Thin client for Ant Group's campus-recruiting portal at talent.antgroup.com.
2
+ //
3
+ // ============================================================
4
+ // API Discovery (probed 2026-05, JS bundle + network analysis):
5
+ //
6
+ // Portal URL: https://talent.antgroup.com/campus-list (public list view)
7
+ // https://talent.antgroup.com/campus-full-list (full list view)
8
+ // JS bundles: gw.alipayobjects.com/render/p/yuyan/180020010001257966/umi.6f081e74.js
9
+ // render.alipay.com/p/yuyan/180020010001257966/p__CampusRecruitment__CRList__index.*.async.js
10
+ // render.alipay.com/p/yuyan/180020010001257966/p__CampusRecruitment__CRFullList__index.*.async.js
11
+ // Gateway host: talent.antgroup.com (Spanner CDN/WAF, Alipay's proprietary gateway)
12
+ // Backend host: antwork-prod.antgroup-inc.cn (actual API server)
13
+ //
14
+ // ============================================================
15
+ // Endpoint inventory (extracted from JS bundle module 64588 + full UMI bundle):
16
+ //
17
+ // POST /api/campus/position/search — paginated job search
18
+ // POST /api/campus/position/detail — single position detail
19
+ // POST /api/campus/position/queryDept — dept tree for a position group
20
+ // POST /api/campus/positionGroup/queryBatchConfig — batch config
21
+ // POST /api/campus/positionGroup/queryBatchDetailById — batch detail
22
+ // POST /api/searchCondition/list — filter taxonomy (categories, cities, depts)
23
+ // POST /api/searchCondition/listPositionGroup
24
+ // POST /api/searchCondition/listTalentPlan
25
+ //
26
+ // Canonical position detail URL: /campus-position?positionId=<id>
27
+ //
28
+ // ============================================================
29
+ // AUTH STATUS — GATED (Alipay OAuth / buservice SDK):
30
+ //
31
+ // EVERY endpoint (including /api/campus/position/search and
32
+ // /api/searchCondition/list) requires an authenticated Alipay/Ant Group
33
+ // session. Without login, the backend returns:
34
+ //
35
+ // { "buserviceErrorCode": "USER_NOT_LOGIN",
36
+ // "buserviceErrorMsg": "https://pubbuservice.alipay.com/…" }
37
+ //
38
+ // The buservice middleware intercepts ALL routes as a catch-all auth gate
39
+ // before any controller logic runs. There is no guest/anonymous tier.
40
+ //
41
+ // The talent.antgroup.com Spanner gateway additionally returns 405 Method
42
+ // Not Allowed for POST requests that lack valid Alipay session cookies,
43
+ // preventing even the USER_NOT_LOGIN response from being seen in most cases.
44
+ // Direct calls to antwork-prod.antgroup-inc.cn reveal the auth error clearly.
45
+ //
46
+ // ============================================================
47
+ // CSRF / session flow (observed but INSUFFICIENT for anonymous access):
48
+ //
49
+ // GET /campus-list sets:
50
+ // ALIPAYJSESSIONID=<token>; domain=.antgroup.com
51
+ // _CHIPS-ALIPAYJSESSIONID=<same_token>; samesite=none; partitioned
52
+ // spanner=<signed_value>; path=/; secure
53
+ //
54
+ // These cookies are required for CORS (Access-Control-Allow-Credentials: true)
55
+ // but the buservice SDK then validates the session against Alipay's auth
56
+ // infrastructure — a simple GET-derived cookie has no authenticated user.
57
+ // Unlike Alibaba's portal (campus-talent.alibaba.com) which only needs an
58
+ // XSRF-TOKEN for public search, Ant Group's portal requires full Alipay OAuth.
59
+ //
60
+ // ============================================================
61
+ // Ant Group vs Alibaba — KEY DIFFERENCES:
62
+ //
63
+ // Portal: talent.antgroup.com vs campus-talent.alibaba.com
64
+ // Auth: Alipay OAuth (gated) vs XSRF-TOKEN only (public search works)
65
+ // CSRF: Not sufficient alone vs Sufficient for anonymous search
66
+ // Backend host: antwork-prod.antgroup-inc.cn vs campus-talent.alibaba.com
67
+ // Auth MW: buservice SDK (blocks all) vs Spring XSRF (only mutating ops)
68
+ //
69
+ // ============================================================
70
+ // FILTER TAXONOMY (from JS bundle, not verified against live API):
71
+ // channel values: "campus_group_official_site" (zh), "en_official_site" (en)
72
+ // searchCondition/list returns: searchItems with types "workCity", "category", "dept", "recruitType"
73
+ // Position fields: id, categoryName, workLocations, graduationTime, circleNames (BU)
74
+ //
75
+ // ============================================================
76
+ // ---- PositionSummary field mapping (Ant Group → canonical) ----
77
+ // post_id ← item.id (stringified)
78
+ // title ← item.name
79
+ // project ← item.categoryName ?? "" (e.g. "技术类", "产品类")
80
+ // recruit_label ← item.recruitType ?? "" (e.g. "实习生", "校招生")
81
+ // bgs ← item.circleNames?.[0] ?? "" (BU / business unit)
82
+ // work_cities ← item.workLocations?.join(" / ") ?? ""
83
+ // apply_url ← https://talent.antgroup.com/campus-position?positionId=<id>
84
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
85
+ export { extractResumeSignals, scoreOverlap, checkResume };
86
+ const PORTAL_ROOT = "https://talent.antgroup.com";
87
+ const CAMPUS_PAGE = `${PORTAL_ROOT}/campus-list`;
88
+ const DETAIL_PAGE = (id) => `${PORTAL_ROOT}/campus-position?positionId=${encodeURIComponent(String(id))}`;
89
+ // ---------- stub reason constant ----------
90
+ const STUB_MESSAGE = "Ant Group (talent.antgroup.com): all API endpoints require Alipay OAuth login. " +
91
+ "POST /api/campus/position/search returns buserviceErrorCode=USER_NOT_LOGIN for " +
92
+ "unauthenticated requests. The Spanner CDN gateway additionally returns HTTP 405 " +
93
+ "for POST requests lacking a valid Alipay session cookie. No anonymous/guest tier exists. " +
94
+ "To use this portal, the user must log in at talent.antgroup.com with an Alipay account " +
95
+ "and supply a valid ALIPAYJSESSIONID cookie.";
96
+ // ---------- searchPositions (stub) ----------
97
+ export async function searchPositions(opts = {}) {
98
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
99
+ const page = Math.max(1, opts.page ?? 1);
100
+ const channel = opts.channel ?? "campus_group_official_site";
101
+ const query = {
102
+ pageIndex: page,
103
+ pageSize,
104
+ channel,
105
+ language: "zh",
106
+ };
107
+ if (opts.keyword?.trim())
108
+ query.keyword = opts.keyword.trim().slice(0, 60);
109
+ if (opts.category)
110
+ query.category = opts.category;
111
+ if (opts.region)
112
+ query.region = opts.region;
113
+ if (opts.deptCode)
114
+ query.deptCode = opts.deptCode;
115
+ return {
116
+ ok: false,
117
+ source: PORTAL_ROOT,
118
+ message: STUB_MESSAGE,
119
+ query,
120
+ page,
121
+ page_size: pageSize,
122
+ total: null,
123
+ positions: [],
124
+ };
125
+ }
126
+ // ---------- fetchAllPositions (stub) ----------
127
+ export async function fetchAllPositions(opts = {}) {
128
+ return {
129
+ ok: false,
130
+ source: PORTAL_ROOT,
131
+ message: STUB_MESSAGE,
132
+ fetched: 0,
133
+ total: null,
134
+ positions: [],
135
+ };
136
+ }
137
+ // ---------- fetchPositionDetail (stub) ----------
138
+ export async function fetchPositionDetail(postId) {
139
+ const id = (postId ?? "").trim();
140
+ if (!id) {
141
+ return {
142
+ ok: false,
143
+ source: PORTAL_ROOT,
144
+ message: "post_id is required",
145
+ };
146
+ }
147
+ return {
148
+ ok: false,
149
+ source: PORTAL_ROOT,
150
+ message: STUB_MESSAGE,
151
+ post_id: id,
152
+ apply_url: DETAIL_PAGE(id),
153
+ };
154
+ }
155
+ // ---------- fetchDictionaries (stub) ----------
156
+ export async function fetchDictionaries() {
157
+ return {
158
+ ok: false,
159
+ source: PORTAL_ROOT,
160
+ message: STUB_MESSAGE,
161
+ note: "Filter taxonomy (categories, cities, depts) is served via POST /api/searchCondition/list " +
162
+ "with body {channel:'campus_group_official_site', language:'zh'}. " +
163
+ "Response shape: { searchItems: [{type:'workCity'|'category'|'dept'|'recruitType', items:[{label,value}]}] }. " +
164
+ "All require Alipay login.",
165
+ };
166
+ }
167
+ // ---------- notices (stub) ----------
168
+ export async function listNotices() {
169
+ return {
170
+ ok: false,
171
+ source: PORTAL_ROOT,
172
+ message: "Ant Group: no public notices endpoint",
173
+ };
174
+ }
175
+ export async function getNotice(_id) {
176
+ return {
177
+ ok: false,
178
+ source: PORTAL_ROOT,
179
+ message: "Ant Group: no public notices endpoint",
180
+ };
181
+ }
182
+ export async function findNoticesByQuestion(_question, _opts = {}) {
183
+ return {
184
+ ok: false,
185
+ source: PORTAL_ROOT,
186
+ message: "Ant Group: no public notices endpoint",
187
+ matches: [],
188
+ };
189
+ }
190
+ // ---------- matchResume (stub) ----------
191
+ export async function matchResume(text, opts = {}) {
192
+ const { terms, cities } = extractResumeSignals(text ?? "");
193
+ return {
194
+ ok: false,
195
+ source: PORTAL_ROOT,
196
+ message: STUB_MESSAGE,
197
+ extracted_terms: terms,
198
+ city_preferences: cities,
199
+ matches: [],
200
+ };
201
+ }
package/dist/byd.js ADDED
@@ -0,0 +1,160 @@
1
+ // Thin client stub for BYD (比亚迪) campus-recruiting portal at job.byd.com.
2
+ //
3
+ // ============================================================
4
+ // Endpoint discovery (probed 2026-05, JS bundle app.e46eb97b.js +
5
+ // chunk-e8fe.d262cda1.js, chunk-ac75.7dee0692.js, chunk-a7e5.62aed375.js,
6
+ // chunk-76ac.cedb4013.js, chunk-dbeb.0075e53e.js):
7
+ //
8
+ // Portal entry:
9
+ // https://job.byd.com/ → redirects to https://job.byd.com/portal/pc/
10
+ // https://careers.byd.com/ → Vite/Vue marketing page (static, no job listings)
11
+ // https://job.byd.com/portal/pc/ → main Vue SPA (webpack, ElementUI)
12
+ //
13
+ // Axios instance (t3Un module in app.e46eb97b.js):
14
+ // baseURL = "/portal/api"
15
+ // Interceptor: adds header Authorization: "bearer <token>" from Vuex store.
16
+ // Code 4001 → "Token无效或已过期: Not Authenticated" (auto-redirect to login).
17
+ //
18
+ // Campus-related API endpoints found in JS bundles:
19
+ // POST /portal/api/school/queryJobList → campus job list
20
+ // POST /portal/api/position/queryList → position list (also skiller/social)
21
+ // POST /portal/api/position/queryDetail → position detail
22
+ // POST /portal/api/other-info/notice/query-list → campus notices
23
+ // POST /portal/api/position/schedule/query-list → campus schedule / timeline
24
+ // GET /portal/api/siteInfo/faq → FAQ
25
+ // POST /portal/api/common/queryCodeTree → code dictionary
26
+ //
27
+ // All endpoints probed 2026-05: EVERY request returns:
28
+ // HTTP 200, body: {"code":4001,"timestamp":...,"msg":"Token无效或已过期: Not Authenticated"}
29
+ //
30
+ // Auth model:
31
+ // Requires a JWT bearer token obtained through BYD account login
32
+ // (POST /portal/api/account/login, then GET /portal/api/account/user-info).
33
+ // There is NO public/anonymous browsing API — even the FAQ and code-tree
34
+ // endpoints are gated behind a valid token.
35
+ //
36
+ // careers.byd.com investigation:
37
+ // careers.byd.com is an internationalised marketing SPA (Vite + Vue 3).
38
+ // Its BydPage-6104aa3e.js uses baseURL "/global-portal/api" with two
39
+ // known endpoints:
40
+ // GET /global-portal-api/global-material/getGlobalMaterial → 404
41
+ // GET /global-portal-api/global-country/getCountryNetwork → 404
42
+ // The site is a static landing page that links to job.byd.com; it does
43
+ // not expose any independent job-search API.
44
+ //
45
+ // ============================================================
46
+ // Summary: BYD has no publicly accessible campus-job search API.
47
+ // All API calls require a logged-in user JWT.
48
+ // This adapter is an honest stub — every function returns ok:false with
49
+ // an informative message. It will be upgraded once an authenticated
50
+ // (scrape-friendly) path is identified.
51
+ //
52
+ // ============================================================
53
+ // PositionSummary field mapping (BYD → canonical, documented for future use):
54
+ // post_id ← item.positionId or item.id (string)
55
+ // title ← item.positionName (e.g. "校招-软件开发工程师")
56
+ // project ← item.positionTypeName (职位类型, e.g. "研发")
57
+ // recruit_label ← item.recruitTypeName (e.g. "应届生" / "实习生")
58
+ // bgs ← "" (not exposed in API)
59
+ // work_cities ← item.workPlace or item.city (free-text Chinese city string)
60
+ // apply_url ← https://job.byd.com/portal/pc/school/schoolPositionApply
61
+ // ?positionId={id}
62
+ //
63
+ // ============================================================
64
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
65
+ export { checkResume };
66
+ const SOURCE = "job.byd.com";
67
+ const CAMPUS_PAGE = "https://job.byd.com/portal/pc/school/home";
68
+ // ---- stub reason ----
69
+ const STUB_REASON = "BYD job.byd.com: all API endpoints require a valid JWT bearer token " +
70
+ "(code 4001 — Token无效或已过期). No public/anonymous job search API exists. " +
71
+ "Visit https://job.byd.com/portal/pc/school/home to browse positions after login.";
72
+ // ---- searchPositions ----
73
+ export async function searchPositions(_opts = {}) {
74
+ return {
75
+ ok: false,
76
+ source: SOURCE,
77
+ message: STUB_REASON,
78
+ campus_page: CAMPUS_PAGE,
79
+ positions: [],
80
+ };
81
+ }
82
+ // ---- fetchAllPositions ----
83
+ export async function fetchAllPositions(_opts = {}) {
84
+ return {
85
+ ok: false,
86
+ source: SOURCE,
87
+ message: STUB_REASON,
88
+ campus_page: CAMPUS_PAGE,
89
+ fetched: 0,
90
+ positions: [],
91
+ };
92
+ }
93
+ // ---- fetchPositionDetail ----
94
+ export async function fetchPositionDetail(_postId) {
95
+ return { ok: false, source: SOURCE, message: STUB_REASON };
96
+ }
97
+ // ---- fetchDictionaries ----
98
+ export async function fetchDictionaries() {
99
+ return {
100
+ ok: false,
101
+ source: SOURCE,
102
+ message: STUB_REASON,
103
+ note: "BYD: no public filter-taxonomy endpoint. " +
104
+ "POST /portal/api/common/queryCodeTree returns 4001 without a token.",
105
+ known_endpoints: [
106
+ "POST /portal/api/school/queryJobList (campus job list — auth required)",
107
+ "POST /portal/api/position/queryList (position list — auth required)",
108
+ "POST /portal/api/position/queryDetail (position detail — auth required)",
109
+ "POST /portal/api/other-info/notice/query-list (notices — auth required)",
110
+ "POST /portal/api/position/schedule/query-list (campus schedule — auth required)",
111
+ "GET /portal/api/siteInfo/faq (FAQ — auth required)",
112
+ "POST /portal/api/common/queryCodeTree (code tree — auth required)",
113
+ ],
114
+ };
115
+ }
116
+ // ---- listNotices ----
117
+ export async function listNotices() {
118
+ return {
119
+ ok: false,
120
+ source: SOURCE,
121
+ message: "BYD: notices endpoint (POST /portal/api/other-info/notice/query-list) requires authentication.",
122
+ };
123
+ }
124
+ // ---- getNotice ----
125
+ export async function getNotice(_id) {
126
+ return {
127
+ ok: false,
128
+ source: SOURCE,
129
+ message: "BYD: no public notices endpoint (auth required).",
130
+ };
131
+ }
132
+ // ---- findNoticesByQuestion ----
133
+ export async function findNoticesByQuestion(_question, _opts = {}) {
134
+ return {
135
+ ok: false,
136
+ source: SOURCE,
137
+ message: "BYD: no public notices endpoint (auth required).",
138
+ };
139
+ }
140
+ // ---- matchResume ----
141
+ // Resume matching is best-effort using extractResumeSignals/scoreOverlap from
142
+ // tencent.ts, but since the position listing API is gated, we can only return
143
+ // a stub with the extracted signals and a pointer to the campus page.
144
+ export async function matchResume(text, opts = {}) {
145
+ // Extract signals so the caller knows what was parsed from the resume
146
+ const { terms, cities } = extractResumeSignals(text ?? "");
147
+ void opts; // unused until API becomes accessible
148
+ return {
149
+ ok: false,
150
+ source: SOURCE,
151
+ message: "BYD: cannot search positions — API requires authentication. " +
152
+ `Extracted resume signals: [${terms.slice(0, 10).join(", ")}]. ` +
153
+ "Visit the campus page to search manually.",
154
+ campus_page: CAMPUS_PAGE,
155
+ extracted_terms: terms,
156
+ city_preferences: cities,
157
+ };
158
+ }
159
+ // ---- re-export helpers so the tencent resume signals are accessible ----
160
+ export { extractResumeSignals, scoreOverlap };