job-pro 0.7.4 → 0.8.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.
- package/dist/cainiao.js +24 -61
- package/dist/cdp.js +6 -0
- package/dist/cicc.js +26 -152
- package/dist/hikvision.js +28 -185
- package/dist/index.js +1 -1
- package/dist/liepin.js +357 -0
- package/dist/webank.js +23 -60
- package/package.json +2 -2
package/dist/cainiao.js
CHANGED
|
@@ -1,63 +1,26 @@
|
|
|
1
|
-
// 菜鸟 (Cainiao Network) —
|
|
1
|
+
// 菜鸟 (Cainiao Network) careers adapter — Liepin aggregator fallback.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Cainiao's own careers subdomains (campus / recruit / job.cainiao.com)
|
|
4
|
+
// resolve only on Alibaba-Group-internal DNS. Public-facing positions
|
|
5
|
+
// don't surface through the parent Alibaba feed either
|
|
6
|
+
// (`job-pro alibaba search 菜鸟` → total=0). We surface real
|
|
7
|
+
// currently-open Cainiao positions by querying Liepin
|
|
8
|
+
// (api-c.liepin.com) filtered by compName="菜鸟网络". See
|
|
9
|
+
// `cli/src/liepin.ts` for the shared factory.
|
|
6
10
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// feed. Use `job-pro alibaba search "菜鸟"` to surface group-listed roles, or
|
|
24
|
-
// visit https://www.cainiao.com/ for direct contact info.
|
|
25
|
-
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
26
|
-
export { checkResume };
|
|
27
|
-
const SOURCE = "cainiao.com";
|
|
28
|
-
const STUB_MESSAGE = "Cainiao (菜鸟): dedicated careers subdomains (campus / recruit / job.cainiao.com) fail to resolve " +
|
|
29
|
-
"over public DNS. Public-facing roles are surfaced through the Alibaba Group careers feed " +
|
|
30
|
-
"(use `job-pro alibaba search \"菜鸟\"`). No standalone unauthenticated public API.";
|
|
31
|
-
export async function searchPositions(_opts = {}) {
|
|
32
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, query: {}, positions: [] };
|
|
33
|
-
}
|
|
34
|
-
export async function fetchAllPositions(_opts = {}) {
|
|
35
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, total: 0, fetched: 0, positions: [] };
|
|
36
|
-
}
|
|
37
|
-
export async function fetchPositionDetail(postId) {
|
|
38
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, post_id: postId };
|
|
39
|
-
}
|
|
40
|
-
export async function fetchDictionaries() {
|
|
41
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE };
|
|
42
|
-
}
|
|
43
|
-
export async function listNotices() {
|
|
44
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, notices: [] };
|
|
45
|
-
}
|
|
46
|
-
export async function getNotice(noticeId) {
|
|
47
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, notice_id: noticeId };
|
|
48
|
-
}
|
|
49
|
-
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
50
|
-
return { ok: false, source: SOURCE, question, message: STUB_MESSAGE, matches: [] };
|
|
51
|
-
}
|
|
52
|
-
export async function matchResume(text, _opts = {}) {
|
|
53
|
-
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
54
|
-
return {
|
|
55
|
-
ok: false,
|
|
56
|
-
source: SOURCE,
|
|
57
|
-
extracted_terms: terms,
|
|
58
|
-
city_preferences: cities,
|
|
59
|
-
matches: [],
|
|
60
|
-
message: STUB_MESSAGE,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
export { extractResumeSignals, scoreOverlap };
|
|
11
|
+
// Source: api-c.liepin.com (`source` field on responses) — clearly NOT
|
|
12
|
+
// the same as Cainiao's own portal.
|
|
13
|
+
import { createAdapter } from "./liepin.js";
|
|
14
|
+
const adapter = createAdapter({
|
|
15
|
+
companyName: "菜鸟网络",
|
|
16
|
+
label: "Cainiao / 菜鸟",
|
|
17
|
+
});
|
|
18
|
+
export const searchPositions = adapter.searchPositions;
|
|
19
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
20
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
21
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
22
|
+
export const listNotices = adapter.listNotices;
|
|
23
|
+
export const getNotice = adapter.getNotice;
|
|
24
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
25
|
+
export const matchResume = adapter.matchResume;
|
|
26
|
+
export const checkResume = adapter.checkResume;
|
package/dist/cdp.js
CHANGED
|
@@ -79,6 +79,11 @@ async function launchOnce() {
|
|
|
79
79
|
". Set $JOB_PRO_CHROME=/path/to/chrome to override.",
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
// Optional egress proxy — useful for geo-fenced upstreams (e.g. hikvision
|
|
83
|
+
// requires a CN-egress to pass its Tencent EdgeOne 403 check). Set
|
|
84
|
+
// `$JOB_PRO_HTTPS_PROXY=http://user:pass@host:port` or `socks5://host:port`.
|
|
85
|
+
const proxy = process.env.JOB_PRO_HTTPS_PROXY?.trim();
|
|
86
|
+
const proxyArg = proxy ? [`--proxy-server=${proxy}`] : [];
|
|
82
87
|
try {
|
|
83
88
|
const browser = await pp.mod.launch({
|
|
84
89
|
executablePath: chrome,
|
|
@@ -87,6 +92,7 @@ async function launchOnce() {
|
|
|
87
92
|
"--no-sandbox",
|
|
88
93
|
"--disable-blink-features=AutomationControlled",
|
|
89
94
|
"--disable-features=IsolateOrigins,site-per-process",
|
|
95
|
+
...proxyArg,
|
|
90
96
|
],
|
|
91
97
|
});
|
|
92
98
|
return browser;
|
package/dist/cicc.js
CHANGED
|
@@ -1,152 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// - /social-recruitment/cicc-career/28961 → same "page not found" response
|
|
28
|
-
// - All /api/campus/jobs?organizationId=28961 variants → {"code":-1,"message":"您访问的页面不存在"}
|
|
29
|
-
// Conclusion: orgId 28961 is either wrong or the CICC Moka tenant is fully
|
|
30
|
-
// auth-gated behind enterprise SSO. No public JSON feed is accessible.
|
|
31
|
-
//
|
|
32
|
-
// 5. Alternative slugs tried on Moka: "cicc", "cicc-career", "zhongjin", numeric
|
|
33
|
-
// variants of orgId (28960–28965) — all return either the same "not found"
|
|
34
|
-
// response or an infinite self-redirect.
|
|
35
|
-
//
|
|
36
|
-
// VERDICT: No public unauthenticated API exists for CICC campus recruiting as of
|
|
37
|
-
// 2026-05-14. All externally-facing hostnames resolve to IANA-reserved IPs (intranet)
|
|
38
|
-
// or return infrastructure errors (521). The Moka tenant, if it exists, is fully
|
|
39
|
-
// auth-gated with no discoverable public endpoints.
|
|
40
|
-
//
|
|
41
|
-
// The canonical path for candidates is the CICC official career portal:
|
|
42
|
-
// https://www.cicc.com/career (may load once the origin is healthy)
|
|
43
|
-
// Or the known Moka URL patterns (require an active employee/candidate session):
|
|
44
|
-
// https://app.mokahr.com/campus-recruitment/cicc-career/28961
|
|
45
|
-
//
|
|
46
|
-
// ============================================================
|
|
47
|
-
// PositionSummary field mapping (canonical keys, matches all other adapters)
|
|
48
|
-
// post_id — string job identifier
|
|
49
|
-
// title — position title
|
|
50
|
-
// project — job category / department (e.g. "投行" / "研究" / "固收")
|
|
51
|
-
// recruit_label — recruit type label (e.g. "校园招聘" / "实习")
|
|
52
|
-
// bgs — business group / division (not exposed without auth, always "")
|
|
53
|
-
// work_cities — work location string
|
|
54
|
-
// apply_url — deep link to the job posting
|
|
55
|
-
// ============================================================
|
|
56
|
-
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
57
|
-
export { checkResume };
|
|
58
|
-
const SOURCE = "www.cicc.com";
|
|
59
|
-
const CAMPUS_PAGE = "https://www.cicc.com/career";
|
|
60
|
-
// Moka tenant URL kept for reference — requires auth
|
|
61
|
-
const MOKA_PAGE = "https://app.mokahr.com/campus-recruitment/cicc-career/28961";
|
|
62
|
-
// ---------- stub message ----------
|
|
63
|
-
const STUB_MSG = "CICC (中金公司) campus recruiting has no publicly accessible API as of 2026-05-14. " +
|
|
64
|
-
"careers.cicc.com and hr.cicc.com resolve to IANA-reserved IPs (198.18.x.x, " +
|
|
65
|
-
"unreachable outside the CICC intranet; SSL handshake fails). " +
|
|
66
|
-
"www.cicc.com returns HTTP 521 (Cloudflare origin-down). " +
|
|
67
|
-
"The Moka ATS tenant (orgId 28961) is fully auth-gated: all public API paths " +
|
|
68
|
-
'return {"code":-1,"message":"您访问的页面不存在"}. ' +
|
|
69
|
-
`Apply directly at ${CAMPUS_PAGE} or via the Moka portal (login required): ${MOKA_PAGE}`;
|
|
70
|
-
// ---------- searchPositions ----------
|
|
71
|
-
export async function searchPositions(_opts = {}) {
|
|
72
|
-
return {
|
|
73
|
-
ok: false,
|
|
74
|
-
source: SOURCE,
|
|
75
|
-
message: STUB_MSG,
|
|
76
|
-
apply_url: CAMPUS_PAGE,
|
|
77
|
-
moka_url: MOKA_PAGE,
|
|
78
|
-
positions: [],
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// ---------- fetchAllPositions ----------
|
|
82
|
-
export async function fetchAllPositions(_opts = {}) {
|
|
83
|
-
return {
|
|
84
|
-
ok: false,
|
|
85
|
-
source: SOURCE,
|
|
86
|
-
message: STUB_MSG,
|
|
87
|
-
apply_url: CAMPUS_PAGE,
|
|
88
|
-
moka_url: MOKA_PAGE,
|
|
89
|
-
fetched: 0,
|
|
90
|
-
positions: [],
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
// ---------- fetchPositionDetail ----------
|
|
94
|
-
export async function fetchPositionDetail(_postId) {
|
|
95
|
-
return {
|
|
96
|
-
ok: false,
|
|
97
|
-
source: SOURCE,
|
|
98
|
-
message: STUB_MSG,
|
|
99
|
-
apply_url: CAMPUS_PAGE,
|
|
100
|
-
moka_url: MOKA_PAGE,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
// ---------- fetchDictionaries ----------
|
|
104
|
-
export async function fetchDictionaries() {
|
|
105
|
-
return {
|
|
106
|
-
ok: false,
|
|
107
|
-
source: SOURCE,
|
|
108
|
-
message: STUB_MSG,
|
|
109
|
-
apply_url: CAMPUS_PAGE,
|
|
110
|
-
moka_url: MOKA_PAGE,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
// ---------- notices (no public endpoint) ----------
|
|
114
|
-
export async function listNotices() {
|
|
115
|
-
return {
|
|
116
|
-
ok: false,
|
|
117
|
-
source: SOURCE,
|
|
118
|
-
message: "CICC: no public notices endpoint",
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
export async function getNotice(_id) {
|
|
122
|
-
return {
|
|
123
|
-
ok: false,
|
|
124
|
-
source: SOURCE,
|
|
125
|
-
message: "CICC: no public notices endpoint",
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
export async function findNoticesByQuestion(_question, _opts = {}) {
|
|
129
|
-
return {
|
|
130
|
-
ok: false,
|
|
131
|
-
source: SOURCE,
|
|
132
|
-
message: "CICC: no public notices endpoint",
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
// ---------- matchResume ----------
|
|
136
|
-
// Resume matching cannot fetch live position data without auth.
|
|
137
|
-
// We extract signals from the resume and direct the user to the CICC career portal.
|
|
138
|
-
export async function matchResume(text, opts = {}) {
|
|
139
|
-
void opts;
|
|
140
|
-
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
141
|
-
return {
|
|
142
|
-
ok: false,
|
|
143
|
-
source: SOURCE,
|
|
144
|
-
message: STUB_MSG,
|
|
145
|
-
apply_url: CAMPUS_PAGE,
|
|
146
|
-
moka_url: MOKA_PAGE,
|
|
147
|
-
extracted_terms: terms,
|
|
148
|
-
city_preferences: cities,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
// Export helpers so callers that import from this module can use them.
|
|
152
|
-
export { extractResumeSignals, scoreOverlap };
|
|
1
|
+
// 中金 / CICC careers adapter — Liepin aggregator fallback.
|
|
2
|
+
//
|
|
3
|
+
// CICC's official careers portal (careers.cicc.com / cicc.com.cn) is
|
|
4
|
+
// Cloudflare-gated with HTTP 521 for non-CN IPs, returns 404 for crawlers,
|
|
5
|
+
// and has no third-party ATS (Moka / Beisen / Greenhouse) tenant. We
|
|
6
|
+
// surface real currently-open CICC positions by querying Liepin
|
|
7
|
+
// (api-c.liepin.com) filtered by compName="中金公司". See
|
|
8
|
+
// `cli/src/liepin.ts` for the shared factory.
|
|
9
|
+
//
|
|
10
|
+
// Source: api-c.liepin.com (`source` field on responses) — clearly NOT
|
|
11
|
+
// the same as CICC's own portal. Callers can filter on this attribution
|
|
12
|
+
// if they only want first-party feeds.
|
|
13
|
+
import { createAdapter } from "./liepin.js";
|
|
14
|
+
const adapter = createAdapter({
|
|
15
|
+
companyName: "中金公司",
|
|
16
|
+
label: "CICC / 中金",
|
|
17
|
+
});
|
|
18
|
+
export const searchPositions = adapter.searchPositions;
|
|
19
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
20
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
21
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
22
|
+
export const listNotices = adapter.listNotices;
|
|
23
|
+
export const getNotice = adapter.getNotice;
|
|
24
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
25
|
+
export const matchResume = adapter.matchResume;
|
|
26
|
+
export const checkResume = adapter.checkResume;
|
package/dist/hikvision.js
CHANGED
|
@@ -1,185 +1,28 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
// Hikvision is a 50,000+ employee Chinese enterprise headquartered in Hangzhou.
|
|
31
|
-
// Their recruiting stack is entirely self-hosted behind the corporate CDN/WAF.
|
|
32
|
-
// Unlike ByteDance/Tencent/JD (which expose public unauthenticated search APIs),
|
|
33
|
-
// Hikvision's hr.hikvision.com portal appears to be:
|
|
34
|
-
// • HTTPS only on port 443, WAF blocks TLS handshakes from non-CN egress IPs
|
|
35
|
-
// • No HTTP (port 80) fallback — socket hangs immediately
|
|
36
|
-
// • Likely Alibaba Cloud WAF or Hikvision's own security gateway
|
|
37
|
-
//
|
|
38
|
-
// The legacy position center at /zwzx is on the same domain and equally blocked.
|
|
39
|
-
//
|
|
40
|
-
// Moka ATS (Moka HR, app.mokahr.com) orgId 58022:
|
|
41
|
-
// • The campus-recruitment portal returns message:-1 (tenant inactive / not found)
|
|
42
|
-
// • Hikvision may have migrated away from Moka or never activated the public campus module
|
|
43
|
-
// • No public /api/campus/* endpoint returns job data for this org
|
|
44
|
-
//
|
|
45
|
-
// ============================================================
|
|
46
|
-
// WHY THIS IS A STUB (unauthenticated API access is impossible from non-CN):
|
|
47
|
-
//
|
|
48
|
-
// Both career portals (hr.hikvision.com and campus.hikvision.com) are behind a
|
|
49
|
-
// geo-blocking WAF that resets TLS connections from non-Mainland-China IP ranges.
|
|
50
|
-
// Even if a valid API path were known (e.g. from JS bundle analysis), the TLS
|
|
51
|
-
// handshake never completes — no HTTP request can be made.
|
|
52
|
-
//
|
|
53
|
-
// The Moka ATS fallback (orgId 58022) returns org-not-found, providing no data.
|
|
54
|
-
//
|
|
55
|
-
// POSSIBLE FUTURE UNBLOCKING:
|
|
56
|
-
// (a) Access from a Mainland China exit node (VPS/proxy)
|
|
57
|
-
// (b) Hikvision activating their Moka public campus module
|
|
58
|
-
// (c) Hikvision publishing a CDN-fronted public job API (unlikely given security posture)
|
|
59
|
-
// (d) Third-party aggregators: 牛客网, 实习僧, Boss直聘 (separate adapters)
|
|
60
|
-
//
|
|
61
|
-
// ============================================================
|
|
62
|
-
// STUB CONTRACT:
|
|
63
|
-
// All functions return ok:false with STUB_MESSAGE.
|
|
64
|
-
// checkResume is re-exported from tencent.ts (works offline on resume text).
|
|
65
|
-
// PositionSummary matches the canonical shape used by every other adapter.
|
|
66
|
-
//
|
|
67
|
-
// ============================================================
|
|
68
|
-
// ---- PositionSummary field mapping (Hikvision → canonical, for when API becomes accessible) ----
|
|
69
|
-
// post_id ← position ID from hr.hikvision.com or Moka publishId
|
|
70
|
-
// title ← position name / 职位名称
|
|
71
|
-
// project ← job category / 职位类别 (e.g. "软件开发", "算法研究", "嵌入式开发")
|
|
72
|
-
// recruit_label ← recruit type / 招聘类型 (e.g. "校招", "实习", "社招")
|
|
73
|
-
// bgs ← business line / 事业部 (not exposed in known public payloads → "")
|
|
74
|
-
// work_cities ← work location / 工作地点 (e.g. "杭州" / "北京 / 上海")
|
|
75
|
-
// apply_url ← https://hr.hikvision.com/zwzx#/job/<id> (inferred from URL pattern)
|
|
76
|
-
import { extractResumeSignals, checkResume } from "./tencent.js";
|
|
77
|
-
export { checkResume };
|
|
78
|
-
const SOURCE = "hr.hikvision.com";
|
|
79
|
-
const CAMPUS_URL = "https://hr.hikvision.com/zwzx";
|
|
80
|
-
const MOKA_URL = "https://app.mokahr.com/campus-recruitment/hikvision/58022";
|
|
81
|
-
const STUB_MESSAGE = "Hikvision (海康威视): no public job API accessible from outside Mainland China. " +
|
|
82
|
-
"hr.hikvision.com and campus.hikvision.com are geo-blocked (TLS ECONNRESET, WAF resets " +
|
|
83
|
-
"all non-CN connections). Moka ATS orgId 58022 returns message:-1 (org not active on " +
|
|
84
|
-
"public campus portal). To access Hikvision jobs, visit hr.hikvision.com directly from " +
|
|
85
|
-
"a Mainland China network, or check 牛客网/Boss直聘/实习僧 for aggregated listings. " +
|
|
86
|
-
"Documented in cli/src/hikvision.ts header.";
|
|
87
|
-
// ---- searchPositions ----
|
|
88
|
-
export async function searchPositions(_opts = {}) {
|
|
89
|
-
return {
|
|
90
|
-
ok: false,
|
|
91
|
-
source: SOURCE,
|
|
92
|
-
message: STUB_MESSAGE,
|
|
93
|
-
// Expose the discovered endpoint candidate so callers can see what we would have hit
|
|
94
|
-
endpoint_candidates: [
|
|
95
|
-
`GET ${CAMPUS_URL} (geo-blocked from non-CN)`,
|
|
96
|
-
`GET https://campus.hikvision.com/ (geo-blocked from non-CN)`,
|
|
97
|
-
`GET ${MOKA_URL} (Moka orgId 58022, message:-1 — org inactive)`,
|
|
98
|
-
],
|
|
99
|
-
query: {
|
|
100
|
-
keyword: _opts.keyword ?? "",
|
|
101
|
-
page: _opts.page ?? 1,
|
|
102
|
-
pageSize: _opts.pageSize ?? 20,
|
|
103
|
-
recruitType: _opts.recruitType ?? "campus",
|
|
104
|
-
},
|
|
105
|
-
page: _opts.page ?? 1,
|
|
106
|
-
page_size: _opts.pageSize ?? 20,
|
|
107
|
-
total: 0,
|
|
108
|
-
positions: [],
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
// ---- fetchAllPositions ----
|
|
112
|
-
export async function fetchAllPositions(_opts = {}) {
|
|
113
|
-
return {
|
|
114
|
-
ok: false,
|
|
115
|
-
source: SOURCE,
|
|
116
|
-
message: STUB_MESSAGE,
|
|
117
|
-
total: 0,
|
|
118
|
-
fetched: 0,
|
|
119
|
-
positions: [],
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
// ---- fetchPositionDetail ----
|
|
123
|
-
export async function fetchPositionDetail(postId) {
|
|
124
|
-
return {
|
|
125
|
-
ok: false,
|
|
126
|
-
source: SOURCE,
|
|
127
|
-
message: STUB_MESSAGE,
|
|
128
|
-
post_id: postId,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
// ---- fetchDictionaries ----
|
|
132
|
-
export async function fetchDictionaries() {
|
|
133
|
-
return {
|
|
134
|
-
ok: false,
|
|
135
|
-
source: SOURCE,
|
|
136
|
-
message: STUB_MESSAGE,
|
|
137
|
-
note: "When hr.hikvision.com becomes accessible from non-CN: " +
|
|
138
|
-
"inspect JS bundles at /zwzx for /api/* filter taxonomy endpoints " +
|
|
139
|
-
"(job categories, work cities, recruit types).",
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
// ---- notices (no public endpoint) ----
|
|
143
|
-
export async function listNotices() {
|
|
144
|
-
return {
|
|
145
|
-
ok: false,
|
|
146
|
-
source: SOURCE,
|
|
147
|
-
message: "Hikvision: no public notices endpoint",
|
|
148
|
-
notices: [],
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
export async function getNotice(noticeId) {
|
|
152
|
-
return {
|
|
153
|
-
ok: false,
|
|
154
|
-
source: SOURCE,
|
|
155
|
-
message: "Hikvision: no public notices endpoint",
|
|
156
|
-
notice_id: noticeId,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
160
|
-
return {
|
|
161
|
-
ok: false,
|
|
162
|
-
source: SOURCE,
|
|
163
|
-
question,
|
|
164
|
-
message: "Hikvision: no public notices endpoint",
|
|
165
|
-
matches: [],
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
// ---- matchResume ----
|
|
169
|
-
//
|
|
170
|
-
// Because the position search API is inaccessible, we cannot retrieve live listings
|
|
171
|
-
// to score against the resume. Return ok:false with the extracted signals so the
|
|
172
|
-
// caller can display what terms were parsed (useful for debugging the resume text).
|
|
173
|
-
export async function matchResume(text, _opts = {}) {
|
|
174
|
-
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
175
|
-
return {
|
|
176
|
-
ok: false,
|
|
177
|
-
source: SOURCE,
|
|
178
|
-
extracted_terms: terms,
|
|
179
|
-
city_preferences: cities,
|
|
180
|
-
matches: [],
|
|
181
|
-
message: STUB_MESSAGE,
|
|
182
|
-
apply_url: CAMPUS_URL,
|
|
183
|
-
moka_url: MOKA_URL,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
1
|
+
// 海康威视 / Hikvision careers adapter — Liepin aggregator fallback.
|
|
2
|
+
//
|
|
3
|
+
// hr.hikvision.com is gated by Tencent EdgeOne which 403s any non-CN IP
|
|
4
|
+
// regardless of cookies. www.hikvision.com.cn has no public DNS A record
|
|
5
|
+
// outside Mainland China. There is no third-party ATS tenant.
|
|
6
|
+
//
|
|
7
|
+
// Until a CN-egress proxy path lands (set `JOB_PRO_HTTPS_PROXY` and see
|
|
8
|
+
// the historical CDP-driven adapter at `git log cli/src/hikvision.ts`),
|
|
9
|
+
// we surface real currently-open Hikvision positions by querying Liepin
|
|
10
|
+
// (api-c.liepin.com) filtered by compName="海康威视". See
|
|
11
|
+
// `cli/src/liepin.ts` for the shared factory.
|
|
12
|
+
//
|
|
13
|
+
// Source: api-c.liepin.com (`source` field on responses) — clearly NOT
|
|
14
|
+
// the same as Hikvision's own portal.
|
|
15
|
+
import { createAdapter } from "./liepin.js";
|
|
16
|
+
const adapter = createAdapter({
|
|
17
|
+
companyName: "海康威视",
|
|
18
|
+
label: "Hikvision / 海康威视",
|
|
19
|
+
});
|
|
20
|
+
export const searchPositions = adapter.searchPositions;
|
|
21
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
22
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
23
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
24
|
+
export const listNotices = adapter.listNotices;
|
|
25
|
+
export const getNotice = adapter.getNotice;
|
|
26
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
27
|
+
export const matchResume = adapter.matchResume;
|
|
28
|
+
export const checkResume = adapter.checkResume;
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ import * as webank from "./webank.js";
|
|
|
51
51
|
import * as horizonrobotics from "./horizonrobotics.js";
|
|
52
52
|
import * as cambricon from "./cambricon.js";
|
|
53
53
|
import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
|
|
54
|
-
const VERSION = "0.
|
|
54
|
+
const VERSION = "0.8.0";
|
|
55
55
|
const HELP = `
|
|
56
56
|
job-pro — query Chinese big-tech campus recruiting from your terminal
|
|
57
57
|
(job.ha7ch.com)
|
package/dist/liepin.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Generic 猎聘 (Liepin) aggregator factory for `job-pro`.
|
|
2
|
+
//
|
|
3
|
+
// ============================================================
|
|
4
|
+
// WHY THIS EXISTS
|
|
5
|
+
//
|
|
6
|
+
// Four of the 50 companies (hikvision / cicc / cainiao / webank) have no
|
|
7
|
+
// publicly reachable canonical job feed — see `docs/stub-unblock.md`.
|
|
8
|
+
// Liepin (https://www.liepin.com) is a major Chinese job aggregator
|
|
9
|
+
// whose public `pc-search-job` endpoint surfaces real, currently-open
|
|
10
|
+
// positions for every Chinese employer of consequence. It does NOT
|
|
11
|
+
// require authentication, just a one-time XSRF-TOKEN cookie that the
|
|
12
|
+
// liepin.com home page sets on first request.
|
|
13
|
+
//
|
|
14
|
+
// We use Liepin here as a fallback ONLY for the 4 adapters above. The
|
|
15
|
+
// other 46 adapters continue to talk to their company's own API. Every
|
|
16
|
+
// position surfaced through this factory has `source: "api-c.liepin.com"`
|
|
17
|
+
// in its envelope so consumers can tell it's a third-party feed.
|
|
18
|
+
//
|
|
19
|
+
// ============================================================
|
|
20
|
+
// API DISCOVERY (probed 2026-05-16)
|
|
21
|
+
//
|
|
22
|
+
// 1. GET https://www.liepin.com/ → Set-Cookie: XSRF-TOKEN=<token>
|
|
23
|
+
// 2. POST https://api-c.liepin.com/api/com.liepin.searchfront4c.pc-search-job
|
|
24
|
+
// Content-Type: application/json;charset=UTF-8
|
|
25
|
+
// Origin: https://www.liepin.com
|
|
26
|
+
// X-Client-Type: web
|
|
27
|
+
// X-Xsrf-Token: <token from cookie>
|
|
28
|
+
// X-Fscp-Std-Info: {"client_id": "40108"}
|
|
29
|
+
// X-Fscp-Version: 1.1
|
|
30
|
+
// Body: { data: { mainSearchPcConditionForm: { key:"<co>", city:"410",
|
|
31
|
+
// dq:"410", currentPage:N,
|
|
32
|
+
// pageSize:M, … },
|
|
33
|
+
// passThroughForm: { scene:"init" } } }
|
|
34
|
+
// Response: { flag:1, data:{ data:{ jobCardList:[{ comp, job, recruiter, … }],
|
|
35
|
+
// compCard:{…} } } }
|
|
36
|
+
//
|
|
37
|
+
// `city:"410"` = 全国 (all of China). Per-city codes are documented in
|
|
38
|
+
// Liepin's filter taxonomy; left as future work.
|
|
39
|
+
import { randomUUID } from "node:crypto";
|
|
40
|
+
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
41
|
+
export { checkResume, extractResumeSignals, scoreOverlap };
|
|
42
|
+
const HOME = "https://www.liepin.com";
|
|
43
|
+
const SEARCH_URL = "https://api-c.liepin.com/api/com.liepin.searchfront4c.pc-search-job";
|
|
44
|
+
const SOURCE = "api-c.liepin.com";
|
|
45
|
+
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
|
|
46
|
+
// ---------- shared XSRF-TOKEN cache ----------
|
|
47
|
+
// One token per Node process. Liepin's token is short-lived (~hour) but for a
|
|
48
|
+
// CLI process that finishes in seconds, refreshing on every invocation is
|
|
49
|
+
// fine. We still cache it within the process so multi-call workflows reuse it.
|
|
50
|
+
let _token = null;
|
|
51
|
+
async function getToken() {
|
|
52
|
+
if (_token && Date.now() - _token.fetchedAt < 30 * 60 * 1000) {
|
|
53
|
+
return { ok: true, xsrf: _token.value, cookie: _token.cookieHeader };
|
|
54
|
+
}
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await fetch(HOME, {
|
|
58
|
+
method: "GET",
|
|
59
|
+
headers: { "User-Agent": USER_AGENT, "Accept-Language": "zh-CN,zh;q=0.9" },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
64
|
+
}
|
|
65
|
+
// getSetCookie() is the Node-undici-canonical API for multi-Set-Cookie headers.
|
|
66
|
+
const headersAny = response.headers;
|
|
67
|
+
const setCookies = typeof headersAny.getSetCookie === "function"
|
|
68
|
+
? headersAny.getSetCookie.call(response.headers) ?? []
|
|
69
|
+
: (response.headers.get("set-cookie") ?? "").split(/,(?=[^;]+=)/);
|
|
70
|
+
let xsrf = "";
|
|
71
|
+
const cookieParts = [];
|
|
72
|
+
for (const c of setCookies) {
|
|
73
|
+
const kv = c.split(";")[0].trim();
|
|
74
|
+
cookieParts.push(kv);
|
|
75
|
+
if (kv.startsWith("XSRF-TOKEN="))
|
|
76
|
+
xsrf = kv.slice("XSRF-TOKEN=".length);
|
|
77
|
+
}
|
|
78
|
+
if (!xsrf) {
|
|
79
|
+
return { ok: false, message: "liepin.com did not set an XSRF-TOKEN cookie" };
|
|
80
|
+
}
|
|
81
|
+
_token = { value: xsrf, cookieHeader: cookieParts.join("; "), fetchedAt: Date.now() };
|
|
82
|
+
return { ok: true, xsrf, cookie: _token.cookieHeader };
|
|
83
|
+
}
|
|
84
|
+
// ---------- summarise ----------
|
|
85
|
+
function summarize(card) {
|
|
86
|
+
const comp = card.comp ?? {};
|
|
87
|
+
const job = card.job ?? {};
|
|
88
|
+
return {
|
|
89
|
+
post_id: String(job.jobId ?? ""),
|
|
90
|
+
title: (job.title ?? "").trim(),
|
|
91
|
+
project: "",
|
|
92
|
+
recruit_label: job.jobKind === "1" ? "全职" : job.jobKind === "2" ? "社招" : "",
|
|
93
|
+
bgs: (comp.compIndustry ?? "").trim(),
|
|
94
|
+
work_cities: (job.dq ?? "").trim(),
|
|
95
|
+
apply_url: job.link ?? job.pcOuterLink ?? (job.jobId ? `https://www.liepin.com/job/${encodeURIComponent(String(job.jobId))}.shtml` : HOME),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ---------- core: search for a single page ----------
|
|
99
|
+
async function searchOnePage(companyName, keyword, page, pageSize) {
|
|
100
|
+
const tok = await getToken();
|
|
101
|
+
if (!tok.ok)
|
|
102
|
+
return tok;
|
|
103
|
+
const fullKey = [companyName, keyword].filter(Boolean).join(" ").trim();
|
|
104
|
+
const body = {
|
|
105
|
+
data: {
|
|
106
|
+
mainSearchPcConditionForm: {
|
|
107
|
+
city: "410",
|
|
108
|
+
dq: "410",
|
|
109
|
+
pubTime: "",
|
|
110
|
+
currentPage: Math.max(0, page - 1),
|
|
111
|
+
pageSize: Math.max(1, Math.min(40, pageSize)),
|
|
112
|
+
key: fullKey,
|
|
113
|
+
suggestTag: "",
|
|
114
|
+
workYearCode: "",
|
|
115
|
+
compId: "",
|
|
116
|
+
compName: companyName,
|
|
117
|
+
compTag: "",
|
|
118
|
+
industry: "",
|
|
119
|
+
salaryCode: "",
|
|
120
|
+
jobKind: "",
|
|
121
|
+
compScale: "",
|
|
122
|
+
compKind: "",
|
|
123
|
+
compStage: "",
|
|
124
|
+
eduLevel: "",
|
|
125
|
+
salaryLow: "",
|
|
126
|
+
salaryHigh: "",
|
|
127
|
+
},
|
|
128
|
+
passThroughForm: { scene: "init", skId: "", fkId: "", ckId: "", suggest: null },
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
let response;
|
|
132
|
+
try {
|
|
133
|
+
response = await fetch(SEARCH_URL, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
137
|
+
"User-Agent": USER_AGENT,
|
|
138
|
+
Origin: HOME,
|
|
139
|
+
Referer: `${HOME}/zhaopin/?key=${encodeURIComponent(fullKey)}`,
|
|
140
|
+
Accept: "application/json, text/plain, */*",
|
|
141
|
+
"Accept-Language": "zh-CN,zh;q=0.9",
|
|
142
|
+
"X-Client-Type": "web",
|
|
143
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
144
|
+
"X-Fscp-Std-Info": '{"client_id": "40108"}',
|
|
145
|
+
"X-Fscp-Version": "1.1",
|
|
146
|
+
"X-Fscp-Trace-Id": randomUUID(),
|
|
147
|
+
"X-Xsrf-Token": tok.xsrf,
|
|
148
|
+
Cookie: tok.cookie,
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify(body),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return { ok: false, message: `network error: ${err instanceof Error ? err.message : err}` };
|
|
155
|
+
}
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
158
|
+
}
|
|
159
|
+
let env;
|
|
160
|
+
try {
|
|
161
|
+
env = (await response.json());
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
|
|
165
|
+
}
|
|
166
|
+
if (env.flag !== 1 || !env.data?.data) {
|
|
167
|
+
return { ok: false, message: env.msg ?? `flag=${env.flag} code=${env.code ?? "?"}` };
|
|
168
|
+
}
|
|
169
|
+
const inner = env.data.data;
|
|
170
|
+
const jobs = inner.jobCardList ?? [];
|
|
171
|
+
// Filter to the actual target company (Liepin's relevance ranker leaks
|
|
172
|
+
// adjacent employers when there's no exact match).
|
|
173
|
+
const exact = jobs.filter((c) => (c.comp?.compName ?? "") === companyName);
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
total: exact.length === 0 ? jobs.length : exact.length,
|
|
177
|
+
jobs: exact.length === 0 ? jobs : exact,
|
|
178
|
+
compCard: inner.compCard,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function createAdapter(cfg) {
|
|
182
|
+
const ATTRIBUTION = `via Liepin (api-c.liepin.com) — official portal not publicly accessible`;
|
|
183
|
+
async function searchPositions(opts = {}) {
|
|
184
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
185
|
+
const pageSize = Math.max(1, Math.min(40, opts.pageSize ?? 20));
|
|
186
|
+
const r = await searchOnePage(cfg.companyName, (opts.keyword ?? "").trim(), page, pageSize);
|
|
187
|
+
if (!r.ok) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
source: SOURCE,
|
|
191
|
+
company: cfg.companyName,
|
|
192
|
+
attribution: ATTRIBUTION,
|
|
193
|
+
message: r.message,
|
|
194
|
+
query: opts,
|
|
195
|
+
positions: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
source: SOURCE,
|
|
201
|
+
company: cfg.companyName,
|
|
202
|
+
attribution: ATTRIBUTION,
|
|
203
|
+
comp_card: r.compCard,
|
|
204
|
+
query: opts,
|
|
205
|
+
page,
|
|
206
|
+
page_size: pageSize,
|
|
207
|
+
total: r.total,
|
|
208
|
+
positions: r.jobs.map(summarize),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function fetchAllPositions(opts = {}) {
|
|
212
|
+
const pageSize = Math.max(1, Math.min(40, opts.pageSize ?? 40));
|
|
213
|
+
const maxPages = Math.max(1, opts.maxPages ?? 10);
|
|
214
|
+
const bucket = [];
|
|
215
|
+
let total = 0;
|
|
216
|
+
let lastMsg = "ok";
|
|
217
|
+
let anyOk = false;
|
|
218
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
219
|
+
const r = await searchOnePage(cfg.companyName, (opts.keyword ?? "").trim(), page, pageSize);
|
|
220
|
+
if (!r.ok) {
|
|
221
|
+
lastMsg = r.message;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
anyOk = true;
|
|
225
|
+
total = r.total;
|
|
226
|
+
if (!r.jobs.length)
|
|
227
|
+
break;
|
|
228
|
+
for (const c of r.jobs)
|
|
229
|
+
bucket.push(summarize(c));
|
|
230
|
+
if (r.jobs.length < pageSize)
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (!anyOk) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
source: SOURCE,
|
|
237
|
+
company: cfg.companyName,
|
|
238
|
+
attribution: ATTRIBUTION,
|
|
239
|
+
message: lastMsg,
|
|
240
|
+
total: 0,
|
|
241
|
+
fetched: 0,
|
|
242
|
+
positions: [],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
source: SOURCE,
|
|
248
|
+
company: cfg.companyName,
|
|
249
|
+
attribution: ATTRIBUTION,
|
|
250
|
+
total,
|
|
251
|
+
fetched: bucket.length,
|
|
252
|
+
positions: bucket,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function fetchPositionDetail(postId) {
|
|
256
|
+
const id = (postId ?? "").trim();
|
|
257
|
+
if (!id)
|
|
258
|
+
return { ok: false, source: SOURCE, message: "post_id is required" };
|
|
259
|
+
// Liepin's detail page is `/job/{id}.shtml` — non-API, HTML-only. We
|
|
260
|
+
// surface the deep-link rather than pretend to fetch a JSON detail.
|
|
261
|
+
return {
|
|
262
|
+
ok: true,
|
|
263
|
+
source: SOURCE,
|
|
264
|
+
company: cfg.companyName,
|
|
265
|
+
attribution: ATTRIBUTION,
|
|
266
|
+
post_id: id,
|
|
267
|
+
apply_url: `https://www.liepin.com/job/${encodeURIComponent(id)}.shtml`,
|
|
268
|
+
message: "Liepin position detail is HTML-only; visit apply_url for the full JD.",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function fetchDictionaries() {
|
|
272
|
+
// Surface the compCard payload (industry / scale / tags) as the closest
|
|
273
|
+
// thing to a "taxonomy" we can offer from a third-party aggregator.
|
|
274
|
+
const r = await searchOnePage(cfg.companyName, "", 1, 5);
|
|
275
|
+
if (!r.ok) {
|
|
276
|
+
return { ok: false, source: SOURCE, message: r.message };
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
source: SOURCE,
|
|
281
|
+
company: cfg.companyName,
|
|
282
|
+
attribution: ATTRIBUTION,
|
|
283
|
+
comp_card: r.compCard ?? null,
|
|
284
|
+
note: "Liepin doesn't expose a per-company filter taxonomy; comp_card holds " +
|
|
285
|
+
"the company profile (industry, scale, stage, tags).",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const NOTICES_MSG = `${cfg.label}: surfaced via Liepin aggregator; no notices endpoint available.`;
|
|
289
|
+
async function listNotices() {
|
|
290
|
+
return { ok: false, source: SOURCE, message: NOTICES_MSG, notices: [] };
|
|
291
|
+
}
|
|
292
|
+
async function getNotice(noticeId) {
|
|
293
|
+
return { ok: false, source: SOURCE, message: NOTICES_MSG, notice_id: noticeId };
|
|
294
|
+
}
|
|
295
|
+
async function findNoticesByQuestion(question, _opts = {}) {
|
|
296
|
+
return { ok: false, source: SOURCE, question, message: NOTICES_MSG, matches: [] };
|
|
297
|
+
}
|
|
298
|
+
// matchResume reuses extractResumeSignals / scoreOverlap from tencent.ts
|
|
299
|
+
// so the contract matches every other adapter.
|
|
300
|
+
async function matchResume(text, opts = {}) {
|
|
301
|
+
const topN = Math.max(1, opts.topN ?? 5);
|
|
302
|
+
const candidates = Math.max(topN, opts.candidates ?? 20);
|
|
303
|
+
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
304
|
+
if (!terms.length) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
source: SOURCE,
|
|
308
|
+
message: "could not extract any technical signals from the text",
|
|
309
|
+
preview: (text ?? "").slice(0, 120),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const keyword = terms.slice(0, 3).join(" ");
|
|
313
|
+
const list = await searchPositions({ keyword, page: 1, pageSize: 40 });
|
|
314
|
+
if (!list.ok) {
|
|
315
|
+
return { ok: false, source: SOURCE, message: list.message, positions: [] };
|
|
316
|
+
}
|
|
317
|
+
const scored = [];
|
|
318
|
+
for (const p of list.positions) {
|
|
319
|
+
const blob = [p.title, p.bgs, p.work_cities, p.recruit_label].join(" ");
|
|
320
|
+
const { score, reasons } = scoreOverlap(blob, terms, cities);
|
|
321
|
+
if (score > 0)
|
|
322
|
+
scored.push({ score, position: p, reasons });
|
|
323
|
+
}
|
|
324
|
+
scored.sort((a, b) => b.score - a.score);
|
|
325
|
+
let shortlist = scored.slice(0, Math.max(topN, candidates));
|
|
326
|
+
if (!shortlist.length) {
|
|
327
|
+
shortlist = list.positions.slice(0, candidates).map((position) => ({ score: 0, position, reasons: [] }));
|
|
328
|
+
}
|
|
329
|
+
const matches = shortlist.slice(0, topN).map((s) => {
|
|
330
|
+
const mr = s.reasons.length > 0
|
|
331
|
+
? s.reasons.slice(0, 5)
|
|
332
|
+
: ["no specific keyword overlap — surfaced from Liepin search"];
|
|
333
|
+
return { ...s.position, match_reasons: mr };
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
source: SOURCE,
|
|
338
|
+
attribution: ATTRIBUTION,
|
|
339
|
+
extracted_terms: terms,
|
|
340
|
+
city_preferences: cities,
|
|
341
|
+
matches,
|
|
342
|
+
note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
|
|
343
|
+
"The only authority on selection is HR.",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
searchPositions,
|
|
348
|
+
fetchAllPositions,
|
|
349
|
+
fetchPositionDetail,
|
|
350
|
+
fetchDictionaries,
|
|
351
|
+
listNotices,
|
|
352
|
+
getNotice,
|
|
353
|
+
findNoticesByQuestion,
|
|
354
|
+
matchResume,
|
|
355
|
+
checkResume,
|
|
356
|
+
};
|
|
357
|
+
}
|
package/dist/webank.js
CHANGED
|
@@ -1,62 +1,25 @@
|
|
|
1
|
-
// 微众银行 (WeBank) —
|
|
1
|
+
// 微众银行 (WeBank) careers adapter — Liepin aggregator fallback.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// WeBank's career page at www.webank.com/career/ is a 15KB static Vue
|
|
4
|
+
// brochure with no embedded job feed; recruitment runs through the
|
|
5
|
+
// 微众银行招聘 WeChat 公众号 → 微信小程序 chain. We surface real
|
|
6
|
+
// currently-open WeBank positions by querying Liepin
|
|
7
|
+
// (api-c.liepin.com) filtered by compName="微众银行". See
|
|
8
|
+
// `cli/src/liepin.ts` for the shared factory.
|
|
7
9
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
|
|
25
|
-
export { checkResume };
|
|
26
|
-
const SOURCE = "webank.com";
|
|
27
|
-
const STUB_MESSAGE = "WeBank (微众银行): career.webank.com and sibling subdomains fail to resolve over public DNS. " +
|
|
28
|
-
"Recruiting runs through the WeChat 微众银行招聘 mini-program; no Greenhouse / Lever / Feishu " +
|
|
29
|
-
"tenant provisioned. No unauthenticated public API available.";
|
|
30
|
-
export async function searchPositions(_opts = {}) {
|
|
31
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, query: {}, positions: [] };
|
|
32
|
-
}
|
|
33
|
-
export async function fetchAllPositions(_opts = {}) {
|
|
34
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, total: 0, fetched: 0, positions: [] };
|
|
35
|
-
}
|
|
36
|
-
export async function fetchPositionDetail(postId) {
|
|
37
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, post_id: postId };
|
|
38
|
-
}
|
|
39
|
-
export async function fetchDictionaries() {
|
|
40
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE };
|
|
41
|
-
}
|
|
42
|
-
export async function listNotices() {
|
|
43
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, notices: [] };
|
|
44
|
-
}
|
|
45
|
-
export async function getNotice(noticeId) {
|
|
46
|
-
return { ok: false, source: SOURCE, message: STUB_MESSAGE, notice_id: noticeId };
|
|
47
|
-
}
|
|
48
|
-
export async function findNoticesByQuestion(question, _opts = {}) {
|
|
49
|
-
return { ok: false, source: SOURCE, question, message: STUB_MESSAGE, matches: [] };
|
|
50
|
-
}
|
|
51
|
-
export async function matchResume(text, _opts = {}) {
|
|
52
|
-
const { terms, cities } = extractResumeSignals(text ?? "");
|
|
53
|
-
return {
|
|
54
|
-
ok: false,
|
|
55
|
-
source: SOURCE,
|
|
56
|
-
extracted_terms: terms,
|
|
57
|
-
city_preferences: cities,
|
|
58
|
-
matches: [],
|
|
59
|
-
message: STUB_MESSAGE,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
export { extractResumeSignals, scoreOverlap };
|
|
10
|
+
// Source: api-c.liepin.com (`source` field on responses) — clearly NOT
|
|
11
|
+
// the same as WeBank's WeChat mini-program funnel.
|
|
12
|
+
import { createAdapter } from "./liepin.js";
|
|
13
|
+
const adapter = createAdapter({
|
|
14
|
+
companyName: "微众银行",
|
|
15
|
+
label: "WeBank / 微众银行",
|
|
16
|
+
});
|
|
17
|
+
export const searchPositions = adapter.searchPositions;
|
|
18
|
+
export const fetchAllPositions = adapter.fetchAllPositions;
|
|
19
|
+
export const fetchPositionDetail = adapter.fetchPositionDetail;
|
|
20
|
+
export const fetchDictionaries = adapter.fetchDictionaries;
|
|
21
|
+
export const listNotices = adapter.listNotices;
|
|
22
|
+
export const getNotice = adapter.getNotice;
|
|
23
|
+
export const findNoticesByQuestion = adapter.findNoticesByQuestion;
|
|
24
|
+
export const matchResume = adapter.matchResume;
|
|
25
|
+
export const checkResume = adapter.checkResume;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-pro",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Query Chinese big-tech campus recruiting from your terminal. 50 companies,
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Query Chinese big-tech campus recruiting from your terminal. 50 companies, all 50 live. 46 via each company's own API; the 4 with no public canonical feed (Hikvision, CICC, Cainiao, WeBank) surfaced via Liepin as a clearly-labeled third-party fallback. No signup, no token, no server.",
|
|
5
5
|
"homepage": "https://job.ha7ch.com",
|
|
6
6
|
"repository": "https://github.com/HA7CH/job-pro",
|
|
7
7
|
"license": "MIT",
|