job-pro 0.6.0 → 0.7.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/agibot.js ADDED
@@ -0,0 +1,390 @@
1
+ // Thin client for 智元机器人 (Agibot / AGIBOT Innovation) campus & social recruiting.
2
+ //
3
+ // ============================================================
4
+ // API DISCOVERY (probed 2026-05)
5
+ //
6
+ // Infrastructure:
7
+ // The corporate site www.agibot.com links to the Lark Hire (飞书招聘) SaaS portal:
8
+ // https://agirobot.jobs.feishu.cn/
9
+ // which hosts four separate recruiting portals:
10
+ // /index — 高端岗位 (senior / executive) website_id: 7314554416651995443
11
+ // /socialrecruitment — 社会招聘 (social / experienced) website_id: 7212468858346785082
12
+ // /campusrecruitment — 校园招聘 (campus / new-grad) website_id: 7212468542670309689
13
+ // /internrecruitment — 实习招聘 (intern)
14
+ //
15
+ // Dead ends probed:
16
+ // https://www.zhiyuan-robot.com/careers — returns 404 (redirects to agibot.com.cn)
17
+ // https://careers.agibot.com/ — connection refused / no server
18
+ // https://hr.agibot.com/ — connection refused / no server
19
+ // Moka orgId 145143 — auth-gated, not publicly accessible
20
+ //
21
+ // WORKING APPROACH — Lark Hire SaaS JSON API:
22
+ // All four portals share a single unauthenticated POST endpoint:
23
+ // POST https://agirobot.jobs.feishu.cn/api/v1/search/job/posts
24
+ // The API returns all 661+ positions (social + campus + intern combined) without
25
+ // any portal-type filter; the Referer header does not affect which posts are returned.
26
+ //
27
+ // Discovered by reverse-engineering the webpack bundle
28
+ // lf-package-cn.feishucdn.com/…/saas-career/static/js/4026.f23f1edc.js:
29
+ // Module 59235 sets eW = "" (relative host), so i = "/api/v1".
30
+ // getPositionList = i + "/search/job/posts" (POST, page_index + page_size)
31
+ // getPositionDetail = i + "/job/posts/" + id (GET)
32
+ // getPositionFilter = i + "/config/job/filters/" + path (GET)
33
+ //
34
+ // API call details (POST /api/v1/search/job/posts):
35
+ // Request body: { keyword, page_size, page_index, ... }
36
+ // Response: { code:0, data:{ job_post_list:[...], count:<int> } }
37
+ // count: 661 (all portals combined, 2026-05 snapshot)
38
+ //
39
+ // Note: department_id is always null in public search results — no BG/部门 field available.
40
+ //
41
+ // ============================================================
42
+ // PositionSummary field mapping (canonical keys — matches all other adapters):
43
+ // post_id — item.id (string)
44
+ // title — item.title
45
+ // project — item.job_category.name (e.g. "研发" / "智能制造 / 工业互联网")
46
+ // recruit_label — item.recruit_type.name + " / " + item.recruit_type.parent.name
47
+ // (e.g. "全职 / 社招" / "实习 / 校招")
48
+ // bgs — "" (department_id is always null in public API)
49
+ // work_cities — city_list[].name joined with " / " (e.g. "上海" / "北京 / 上海")
50
+ // apply_url — https://agirobot.jobs.feishu.cn/socialrecruitment/position/{id}/detail
51
+ // ============================================================
52
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
53
+ export { checkResume };
54
+ const SOURCE = "agirobot.jobs.feishu.cn";
55
+ const API_ROOT = "https://agirobot.jobs.feishu.cn/api/v1";
56
+ const PORTAL_BASE = "https://agirobot.jobs.feishu.cn";
57
+ const LIST_PAGE = `${PORTAL_BASE}/socialrecruitment`;
58
+ const DETAIL_URL = (id) => `${PORTAL_BASE}/socialrecruitment/position/${encodeURIComponent(id)}/detail`;
59
+ const DEFAULT_HEADERS = {
60
+ "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",
61
+ Accept: "application/json, text/plain, */*",
62
+ "Content-Type": "application/json",
63
+ Origin: PORTAL_BASE,
64
+ Referer: LIST_PAGE,
65
+ };
66
+ async function call(path, body) {
67
+ const url = `${API_ROOT}${path}`;
68
+ let response;
69
+ try {
70
+ response = await fetch(url, {
71
+ method: "POST",
72
+ headers: DEFAULT_HEADERS,
73
+ body: JSON.stringify(body),
74
+ });
75
+ }
76
+ catch (err) {
77
+ return {
78
+ ok: false,
79
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
80
+ };
81
+ }
82
+ if (!response.ok) {
83
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
84
+ }
85
+ let payload;
86
+ try {
87
+ payload = (await response.json());
88
+ }
89
+ catch (err) {
90
+ return { ok: false, message: `bad JSON: ${err instanceof Error ? err.message : err}` };
91
+ }
92
+ return {
93
+ ok: payload.code === 0,
94
+ data: payload.data,
95
+ message: payload.message || (payload.code === 0 ? "ok" : "upstream error"),
96
+ };
97
+ }
98
+ function summarizePosition(item) {
99
+ const id = String(item.id ?? "");
100
+ // work_cities: prefer city_list for multi-city; fall back to city_info
101
+ const cityList = item.city_list ?? [];
102
+ let work_cities;
103
+ if (cityList.length >= 1) {
104
+ work_cities = cityList.map((c) => c.name ?? "").filter(Boolean).join(" / ");
105
+ }
106
+ else {
107
+ work_cities = item.city_info?.name ?? "";
108
+ }
109
+ // recruit_label: "全职 / 社招" or "实习 / 校招" style
110
+ const rt = item.recruit_type;
111
+ const rtName = rt?.name ?? "";
112
+ const rtParent = rt?.parent?.name ?? "";
113
+ const recruit_label = rtParent ? `${rtName} / ${rtParent}` : rtName;
114
+ return {
115
+ post_id: id,
116
+ title: item.title ?? "",
117
+ project: item.job_category?.name ?? "",
118
+ recruit_label,
119
+ bgs: "", // department_id is always null in public search results
120
+ work_cities,
121
+ apply_url: id ? DETAIL_URL(id) : LIST_PAGE,
122
+ };
123
+ }
124
+ // ---------- searchPositions ----------
125
+ export async function searchPositions(opts = {}) {
126
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 20));
127
+ const page = Math.max(1, opts.page ?? 1);
128
+ const keyword = (opts.keyword ?? "").trim().slice(0, 60);
129
+ const payload = {
130
+ keyword,
131
+ page_size: pageSize,
132
+ page_index: page,
133
+ };
134
+ const response = await call("/search/job/posts", payload);
135
+ if (!response.ok || !response.data) {
136
+ return {
137
+ ok: false,
138
+ message: response.message,
139
+ source: SOURCE,
140
+ query: payload,
141
+ positions: [],
142
+ };
143
+ }
144
+ const rows = response.data.job_post_list ?? [];
145
+ return {
146
+ ok: true,
147
+ source: SOURCE,
148
+ query: payload,
149
+ page,
150
+ page_size: pageSize,
151
+ total: response.data.count ?? rows.length,
152
+ positions: rows.map(summarizePosition),
153
+ };
154
+ }
155
+ // ---------- fetchAllPositions ----------
156
+ export async function fetchAllPositions(opts = {}) {
157
+ const pageSize = Math.max(1, Math.min(100, opts.pageSize ?? 100));
158
+ const maxPages = Math.max(1, opts.maxPages ?? 10); // up to 1000 posts
159
+ const bucket = [];
160
+ let total;
161
+ for (let page = 1; page <= maxPages; page++) {
162
+ const result = await searchPositions({ ...opts, page, pageSize });
163
+ if (!result.ok) {
164
+ return {
165
+ ok: false,
166
+ message: result.message,
167
+ source: SOURCE,
168
+ fetched: bucket.length,
169
+ positions: bucket,
170
+ };
171
+ }
172
+ if (total === undefined)
173
+ total = result.total;
174
+ if (!result.positions.length)
175
+ break;
176
+ bucket.push(...result.positions);
177
+ if (total !== undefined && bucket.length >= total)
178
+ break;
179
+ }
180
+ return {
181
+ ok: true,
182
+ source: SOURCE,
183
+ total: total ?? bucket.length,
184
+ fetched: bucket.length,
185
+ positions: bucket,
186
+ };
187
+ }
188
+ export async function fetchPositionDetail(postId) {
189
+ const id = (postId ?? "").trim();
190
+ if (!id) {
191
+ return { ok: false, source: SOURCE, message: "post_id is required" };
192
+ }
193
+ const url = `${API_ROOT}/job/posts/${encodeURIComponent(id)}`;
194
+ let response;
195
+ try {
196
+ response = await fetch(url, {
197
+ method: "GET",
198
+ headers: { ...DEFAULT_HEADERS, Referer: DETAIL_URL(id) },
199
+ });
200
+ }
201
+ catch (err) {
202
+ return {
203
+ ok: false,
204
+ source: SOURCE,
205
+ post_id: id,
206
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
207
+ };
208
+ }
209
+ if (!response.ok) {
210
+ return {
211
+ ok: false,
212
+ source: SOURCE,
213
+ post_id: id,
214
+ message: `HTTP ${response.status}: ${response.statusText}`,
215
+ };
216
+ }
217
+ let payload;
218
+ try {
219
+ payload = (await response.json());
220
+ }
221
+ catch (err) {
222
+ return {
223
+ ok: false,
224
+ source: SOURCE,
225
+ post_id: id,
226
+ message: `bad JSON: ${err instanceof Error ? err.message : err}`,
227
+ };
228
+ }
229
+ if (payload.code !== 0 || !payload.data?.job_post_detail) {
230
+ return {
231
+ ok: false,
232
+ source: SOURCE,
233
+ post_id: id,
234
+ message: payload.message ?? "upstream error",
235
+ };
236
+ }
237
+ const d = payload.data.job_post_detail;
238
+ const cities = (d.city_list ?? []).map((c) => c.name ?? "").filter(Boolean);
239
+ return {
240
+ ok: true,
241
+ source: SOURCE,
242
+ post_id: String(d.id ?? id),
243
+ title: d.title ?? "",
244
+ direction: d.sub_title ?? "",
245
+ project: d.job_category?.name ?? "",
246
+ recruit_label: d.recruit_type?.name ?? "",
247
+ description: d.description ?? "",
248
+ requirements: d.requirement ?? "",
249
+ work_cities: cities,
250
+ apply_url: DETAIL_URL(String(d.id ?? id)),
251
+ };
252
+ }
253
+ export async function fetchDictionaries() {
254
+ const url = `${API_ROOT}/config/job/filters/index`;
255
+ let response;
256
+ try {
257
+ response = await fetch(url, { method: "GET", headers: DEFAULT_HEADERS });
258
+ }
259
+ catch (err) {
260
+ return {
261
+ ok: false,
262
+ source: SOURCE,
263
+ message: `network error: ${err instanceof Error ? err.message : String(err)}`,
264
+ };
265
+ }
266
+ if (!response.ok) {
267
+ return {
268
+ ok: false,
269
+ source: SOURCE,
270
+ message: `HTTP ${response.status}`,
271
+ };
272
+ }
273
+ let payload;
274
+ try {
275
+ payload = (await response.json());
276
+ }
277
+ catch (err) {
278
+ return {
279
+ ok: false,
280
+ source: SOURCE,
281
+ message: `bad JSON: ${err instanceof Error ? err.message : err}`,
282
+ };
283
+ }
284
+ if (payload.code !== 0 || !payload.data) {
285
+ return {
286
+ ok: false,
287
+ source: SOURCE,
288
+ message: payload.message ?? "upstream error",
289
+ };
290
+ }
291
+ const d = payload.data;
292
+ const jobCategories = (d.job_type_list ?? []).map((c) => ({
293
+ id: c.id ?? "",
294
+ name: c.name ?? "",
295
+ en_name: c.en_name ?? "",
296
+ depth: c.depth ?? 1,
297
+ parent_id: c.parent?.id ?? null,
298
+ }));
299
+ const cities = (d.city_list ?? []).map((c) => ({
300
+ code: c.code ?? "",
301
+ name: c.name ?? "",
302
+ en_name: c.en_name ?? "",
303
+ }));
304
+ return {
305
+ ok: true,
306
+ source: SOURCE,
307
+ portal: PORTAL_BASE,
308
+ portals: {
309
+ index: `${PORTAL_BASE}/index`,
310
+ social: `${PORTAL_BASE}/socialrecruitment`,
311
+ campus: `${PORTAL_BASE}/campusrecruitment`,
312
+ intern: `${PORTAL_BASE}/internrecruitment`,
313
+ },
314
+ note: "All four Agibot recruiting portals share a single public API endpoint at " +
315
+ "/api/v1/search/job/posts. department_id is always null in public results " +
316
+ "(no BG/部门 exposed). Total ~661 positions across social + campus + intern.",
317
+ jobCategories,
318
+ cities,
319
+ };
320
+ }
321
+ // ---------- notices (no public endpoint) ----------
322
+ const NOTICES_STUB = {
323
+ ok: false,
324
+ source: SOURCE,
325
+ message: "Agibot: no public notices or announcement endpoint available",
326
+ };
327
+ export async function listNotices() {
328
+ return NOTICES_STUB;
329
+ }
330
+ export async function getNotice(_id) {
331
+ return NOTICES_STUB;
332
+ }
333
+ export async function findNoticesByQuestion(_question, _opts = {}) {
334
+ return NOTICES_STUB;
335
+ }
336
+ // ---------- matchResume ----------
337
+ export async function matchResume(text, opts = {}) {
338
+ const topN = Math.max(1, opts.topN ?? 5);
339
+ const candidates = Math.max(topN, opts.candidates ?? 20);
340
+ const { terms, cities } = extractResumeSignals(text ?? "");
341
+ if (!terms.length) {
342
+ return {
343
+ ok: false,
344
+ source: SOURCE,
345
+ message: "could not extract any technical signals from the text",
346
+ preview: (text ?? "").slice(0, 120),
347
+ };
348
+ }
349
+ const keyword = terms.slice(0, 3).join(" ");
350
+ const list = await searchPositions({ keyword, page: 1, pageSize: 100 });
351
+ if (!list.ok) {
352
+ return {
353
+ ok: false,
354
+ source: SOURCE,
355
+ message: list.message,
356
+ positions: [],
357
+ };
358
+ }
359
+ const scored = [];
360
+ for (const p of list.positions) {
361
+ const blob = [p.title, p.project, p.recruit_label, p.work_cities, p.post_id].join(" ");
362
+ const { score, reasons } = scoreOverlap(blob, terms, cities);
363
+ if (score > 0)
364
+ scored.push({ score, position: p, reasons });
365
+ }
366
+ scored.sort((a, b) => b.score - a.score);
367
+ let shortlist = scored.slice(0, Math.max(topN, candidates));
368
+ if (!shortlist.length) {
369
+ shortlist = list.positions.slice(0, candidates).map((position) => ({
370
+ score: 0,
371
+ position,
372
+ reasons: [],
373
+ }));
374
+ }
375
+ const matches = shortlist.slice(0, topN).map((s) => {
376
+ const mr = s.reasons.length > 0
377
+ ? s.reasons.slice(0, 5)
378
+ : ["no specific keyword overlap — surfaced from initial keyword search"];
379
+ return { ...s.position, match_reasons: mr };
380
+ });
381
+ return {
382
+ ok: true,
383
+ source: SOURCE,
384
+ extracted_terms: terms,
385
+ city_preferences: cities,
386
+ matches,
387
+ note: "match_reasons surfaces overlapping keywords, not a probability of getting an interview. " +
388
+ "The only authority on selection is HR.",
389
+ };
390
+ }
@@ -0,0 +1,133 @@
1
+ // 百川智能 (Baichuan AI) — stub adapter for `job-pro`.
2
+ //
3
+ // STATUS: stub-only. No public unauthenticated job API was found.
4
+ //
5
+ // ============================================================
6
+ // RECONNAISSANCE RESULTS (probed 2026-05):
7
+ //
8
+ // www.baichuan-ai.com/careers → 404 (Next.js SPA, page removed)
9
+ // www.baichuan-ai.com/join → 404
10
+ // www.baichuan-ai.com/ → React SPA; no inline job data;
11
+ // JS bundles contain no ATS references
12
+ //
13
+ // Feishu ATSX tenants probed:
14
+ // baichuan.jobs.feishu.cn → HTTP 400 empty body for all channels
15
+ // (TLS cert resolves — wildcard *.jobs.feishu.cn —
16
+ // but no portal is configured on the ATSX backend)
17
+ // baichuan-ai.jobs.feishu.cn → HTTP 404 (DNS only, no TLS/host)
18
+ // baichuan-inc.jobs.feishu.cn → HTTP 404
19
+ // baichuanai.jobs.feishu.cn → HTTP 404
20
+ //
21
+ // HTTP 400 + empty body is Feishu's "site not exist / tenant has no portal"
22
+ // response (documented in feishu.ts discovery notes). The subdomain is
23
+ // registered at the CDN level but has no active recruiting portal behind it.
24
+ //
25
+ // Moka ATS:
26
+ // app.mokahr.com/campus-recruitment/baichuan → "您访问的页面不存在" (error page)
27
+ // app.mokahr.com/social-recruitment/baichuan → same error
28
+ // Moka orgId probes (numeric range) — all return the Moka SPA error shell;
29
+ // no slug "baichuan" maps to a live Moka org.
30
+ //
31
+ // Greenhouse:
32
+ // boards.greenhouse.io/baichuan → 404 "job board no longer active"
33
+ // (Greenhouse board existed historically but has been deactivated.)
34
+ //
35
+ // Lever, BOSS Zhipin, Lagou, Liepin — no unauthenticated public API found.
36
+ //
37
+ // Conclusion: Baichuan currently posts positions through internal/gated channels
38
+ // only. When a public JSON endpoint becomes available this adapter can be
39
+ // upgraded to a thin wrapper around feishu.ts (createAdapter) or a bespoke
40
+ // client in a single pass.
41
+ //
42
+ // ============================================================
43
+ // PositionSummary field mapping (canonical — matches all other adapters):
44
+ // post_id — position identifier
45
+ // title — position title
46
+ // project — job category / department
47
+ // recruit_label — recruit type (e.g. "实习" / "社招")
48
+ // bgs — business group (Baichuan has no public BG dimension)
49
+ // work_cities — work location(s)
50
+ // apply_url — deep-link to the position detail page
51
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
52
+ export { checkResume };
53
+ const SOURCE = "www.baichuan-ai.com";
54
+ const STUB_MESSAGE = "Baichuan (百川智能): no public job API — Feishu ATSX subdomain " +
55
+ "baichuan.jobs.feishu.cn is registered but has no portal configured " +
56
+ "(HTTP 400 empty body); Moka slug 'baichuan' returns a page-not-found " +
57
+ "error; Greenhouse board has been deactivated. " +
58
+ "Visit https://www.baichuan-ai.com/ for any current career links.";
59
+ // ---------- stub functions ----------
60
+ export async function searchPositions(_opts = {}) {
61
+ return {
62
+ ok: false,
63
+ source: SOURCE,
64
+ message: STUB_MESSAGE,
65
+ query: {},
66
+ positions: [],
67
+ };
68
+ }
69
+ export async function fetchAllPositions(_opts = {}) {
70
+ return {
71
+ ok: false,
72
+ source: SOURCE,
73
+ message: STUB_MESSAGE,
74
+ total: 0,
75
+ fetched: 0,
76
+ positions: [],
77
+ };
78
+ }
79
+ export async function fetchPositionDetail(postId) {
80
+ return {
81
+ ok: false,
82
+ source: SOURCE,
83
+ message: STUB_MESSAGE,
84
+ post_id: postId,
85
+ };
86
+ }
87
+ export async function fetchDictionaries() {
88
+ return {
89
+ ok: false,
90
+ source: SOURCE,
91
+ message: STUB_MESSAGE,
92
+ };
93
+ }
94
+ export async function listNotices() {
95
+ return {
96
+ ok: false,
97
+ source: SOURCE,
98
+ message: STUB_MESSAGE,
99
+ notices: [],
100
+ };
101
+ }
102
+ export async function getNotice(noticeId) {
103
+ return {
104
+ ok: false,
105
+ source: SOURCE,
106
+ message: STUB_MESSAGE,
107
+ notice_id: noticeId,
108
+ };
109
+ }
110
+ export async function findNoticesByQuestion(question, _opts = {}) {
111
+ return {
112
+ ok: false,
113
+ source: SOURCE,
114
+ question,
115
+ message: STUB_MESSAGE,
116
+ matches: [],
117
+ };
118
+ }
119
+ // ---------- matchResume ----------
120
+ // Extracts signals so callers can see what terms were parsed, even though
121
+ // no live positions are available to score against.
122
+ export async function matchResume(text, _opts = {}) {
123
+ const { terms, cities } = extractResumeSignals(text ?? "");
124
+ return {
125
+ ok: false,
126
+ source: SOURCE,
127
+ extracted_terms: terms,
128
+ city_preferences: cities,
129
+ matches: [],
130
+ message: STUB_MESSAGE,
131
+ };
132
+ }
133
+ export { extractResumeSignals, scoreOverlap };
@@ -0,0 +1,63 @@
1
+ // 菜鸟 (Cainiao Network) — stub adapter for `job-pro`.
2
+ //
3
+ // STATUS: stub-only. Both campus and social recruiting are routed through
4
+ // Alibaba's unified careers infrastructure, which is hosted on subdomains
5
+ // that fail to resolve over public DNS (likely group-network-only A records).
6
+ //
7
+ // ============================================================
8
+ // RECONNAISSANCE RESULTS (probed 2026-05):
9
+ //
10
+ // https://campus.cainiao.com — 000 (no public DNS / unreachable)
11
+ // https://recruit.cainiao.com — 000 (no public DNS / unreachable)
12
+ // https://job.cainiao.com — 000 (no public DNS / unreachable)
13
+ //
14
+ // The corporate careers blurb on www.cainiao.com links out to
15
+ // "campus-talent.alibaba.com" (already covered by the `alibaba` adapter),
16
+ // suggesting cainiao postings are merged into the Alibaba Group careers feed
17
+ // when they go public. The dedicated Cainiao SPA is internal-only.
18
+ //
19
+ // Feishu ATSX: cainiao.jobs.feishu.cn — HTTP 400 (no portal configured)
20
+ // Greenhouse / Lever / Moka: no `cainiao` slug found on any of them.
21
+ //
22
+ // Conclusion: no unauthenticated public API outside of the Alibaba Group
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 };
@@ -0,0 +1,64 @@
1
+ // 寒武纪 (Cambricon) — stub adapter for `job-pro`.
2
+ //
3
+ // STATUS: stub-only. Cambricon's careers domains do not resolve over public
4
+ // DNS, and no third-party ATS tenant (Feishu, Moka, Greenhouse, Lever) is
5
+ // provisioned for the company. Recruiting runs through internal channels.
6
+ //
7
+ // ============================================================
8
+ // RECONNAISSANCE RESULTS (probed 2026-05):
9
+ //
10
+ // https://hr.cambricon.com — 000 (no public DNS / unreachable)
11
+ // https://careers.cambricon.com — 000 (no public DNS / unreachable)
12
+ // https://campus.cambricon.com — 000 (no public DNS / unreachable)
13
+ //
14
+ // Feishu ATSX: cambricon.jobs.feishu.cn — HTTP 400 (no portal)
15
+ // Greenhouse: cambricon — HTTP 404 (no board)
16
+ // Lever: cambricon — HTTP 404 (no posting)
17
+ // Moka: app.mokahr.com/social-recruitment/cambricon → 302 (unprovisioned)
18
+ //
19
+ // Cambricon's official careers blurb on cambricon.com points to the
20
+ // public WeChat 寒武纪招聘 official account, which posts openings as
21
+ // articles and routes applications to internal HR contacts.
22
+ //
23
+ // Conclusion: no unauthenticated public API. Visit the 寒武纪招聘 WeChat
24
+ // official account, or send a resume to the careers email listed at
25
+ // https://www.cambricon.com/.
26
+ import { extractResumeSignals, scoreOverlap, checkResume } from "./tencent.js";
27
+ export { checkResume };
28
+ const SOURCE = "cambricon.com";
29
+ const STUB_MESSAGE = "Cambricon (寒武纪): hr / careers / campus.cambricon.com all fail to resolve over public DNS. " +
30
+ "No Greenhouse / Lever / Feishu / Moka tenant provisioned. Recruiting runs through the " +
31
+ "WeChat 寒武纪招聘 official account. No unauthenticated public API available.";
32
+ export async function searchPositions(_opts = {}) {
33
+ return { ok: false, source: SOURCE, message: STUB_MESSAGE, query: {}, positions: [] };
34
+ }
35
+ export async function fetchAllPositions(_opts = {}) {
36
+ return { ok: false, source: SOURCE, message: STUB_MESSAGE, total: 0, fetched: 0, positions: [] };
37
+ }
38
+ export async function fetchPositionDetail(postId) {
39
+ return { ok: false, source: SOURCE, message: STUB_MESSAGE, post_id: postId };
40
+ }
41
+ export async function fetchDictionaries() {
42
+ return { ok: false, source: SOURCE, message: STUB_MESSAGE };
43
+ }
44
+ export async function listNotices() {
45
+ return { ok: false, source: SOURCE, message: STUB_MESSAGE, notices: [] };
46
+ }
47
+ export async function getNotice(noticeId) {
48
+ return { ok: false, source: SOURCE, message: STUB_MESSAGE, notice_id: noticeId };
49
+ }
50
+ export async function findNoticesByQuestion(question, _opts = {}) {
51
+ return { ok: false, source: SOURCE, question, message: STUB_MESSAGE, matches: [] };
52
+ }
53
+ export async function matchResume(text, _opts = {}) {
54
+ const { terms, cities } = extractResumeSignals(text ?? "");
55
+ return {
56
+ ok: false,
57
+ source: SOURCE,
58
+ extracted_terms: terms,
59
+ city_preferences: cities,
60
+ matches: [],
61
+ message: STUB_MESSAGE,
62
+ };
63
+ }
64
+ export { extractResumeSignals, scoreOverlap };