@jackwener/opencli 0.6.2 β†’ 0.6.3

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/README.md CHANGED
@@ -139,7 +139,7 @@ npm install -g @jackwener/opencli@latest
139
139
  | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `following` `followers` `notifications` `post` `reply` `delete` `like` | πŸ” Browser |
140
140
  | **reddit** | `hot` `frontpage` `search` `subreddit` | πŸ” Browser |
141
141
  | **weibo** | `hot` | πŸ” Browser |
142
- | **boss** | `search` | πŸ” Browser |
142
+ | **boss** | `search` `detail` | πŸ” Browser |
143
143
  | **coupang** | `search` `add-to-cart` | πŸ” Browser |
144
144
  | **youtube** | `search` | πŸ” Browser |
145
145
  | **yahoo-finance** | `quote` | πŸ” Browser |
package/README.zh-CN.md CHANGED
@@ -138,7 +138,7 @@ npm install -g @jackwener/opencli@latest
138
138
  | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `following` `followers` `notifications` `post` `reply` `delete` `like` | πŸ” ζ΅θ§ˆε™¨ |
139
139
  | **reddit** | `hot` `frontpage` `search` `subreddit` | πŸ” ζ΅θ§ˆε™¨ |
140
140
  | **weibo** | `hot` | πŸ” ζ΅θ§ˆε™¨ |
141
- | **boss** | `search` | πŸ” ζ΅θ§ˆε™¨ |
141
+ | **boss** | `search` `detail` | πŸ” ζ΅θ§ˆε™¨ |
142
142
  | **coupang** | `search` `add-to-cart` | πŸ” ζ΅θ§ˆε™¨ |
143
143
  | **youtube** | `search` | πŸ” ζ΅θ§ˆε™¨ |
144
144
  | **yahoo-finance** | `quote` | πŸ” ζ΅θ§ˆε™¨ |
@@ -392,6 +392,44 @@
392
392
  "url"
393
393
  ]
394
394
  },
395
+ {
396
+ "site": "boss",
397
+ "name": "detail",
398
+ "description": "BOSSη›΄θ˜ζŸ₯ηœ‹θŒδ½θ―¦ζƒ…",
399
+ "strategy": "cookie",
400
+ "browser": true,
401
+ "args": [
402
+ {
403
+ "name": "security_id",
404
+ "type": "str",
405
+ "required": true,
406
+ "help": "Security ID from search results (securityId field)"
407
+ }
408
+ ],
409
+ "type": "ts",
410
+ "modulePath": "boss/detail.js",
411
+ "domain": "www.zhipin.com",
412
+ "columns": [
413
+ "name",
414
+ "salary",
415
+ "experience",
416
+ "degree",
417
+ "city",
418
+ "district",
419
+ "description",
420
+ "skills",
421
+ "welfare",
422
+ "boss_name",
423
+ "boss_title",
424
+ "active_time",
425
+ "company",
426
+ "industry",
427
+ "scale",
428
+ "stage",
429
+ "address",
430
+ "url"
431
+ ]
432
+ },
395
433
  {
396
434
  "site": "boss",
397
435
  "name": "search",
@@ -467,6 +505,7 @@
467
505
  "degree",
468
506
  "skills",
469
507
  "boss",
508
+ "security_id",
470
509
  "url"
471
510
  ]
472
511
  },
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,104 @@
1
+ /**
2
+ * BOSSη›΄θ˜ job detail β€” fetch full job posting details via browser cookie API.
3
+ *
4
+ * Uses securityId from search results to call the detail API.
5
+ * Returns: job description, skills, welfare, boss info, company info, address.
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ cli({
9
+ site: 'boss',
10
+ name: 'detail',
11
+ description: 'BOSSη›΄θ˜ζŸ₯ηœ‹θŒδ½θ―¦ζƒ…',
12
+ domain: 'www.zhipin.com',
13
+ strategy: Strategy.COOKIE,
14
+ browser: true,
15
+ args: [
16
+ { name: 'security_id', required: true, help: 'Security ID from search results (securityId field)' },
17
+ ],
18
+ columns: [
19
+ 'name', 'salary', 'experience', 'degree', 'city', 'district',
20
+ 'description', 'skills', 'welfare',
21
+ 'boss_name', 'boss_title', 'active_time',
22
+ 'company', 'industry', 'scale', 'stage',
23
+ 'address', 'url',
24
+ ],
25
+ func: async (page, kwargs) => {
26
+ if (!page)
27
+ throw new Error('Browser page required');
28
+ const securityId = kwargs.security_id;
29
+ // Navigate to zhipin.com first to establish cookie context (referrer + cookies)
30
+ await page.goto('https://www.zhipin.com/web/geek/job');
31
+ await page.wait({ time: 1 });
32
+ const targetUrl = `https://www.zhipin.com/wapi/zpgeek/job/detail.json?securityId=${encodeURIComponent(securityId)}`;
33
+ if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) {
34
+ console.error(`[opencli:boss] Fetching job detail...`);
35
+ }
36
+ const evaluateScript = `
37
+ async () => {
38
+ return new Promise((resolve, reject) => {
39
+ const xhr = new window.XMLHttpRequest();
40
+ xhr.open('GET', ${JSON.stringify(targetUrl)}, true);
41
+ xhr.withCredentials = true;
42
+ xhr.timeout = 15000;
43
+ xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
44
+ xhr.onload = () => {
45
+ if (xhr.status >= 200 && xhr.status < 300) {
46
+ try {
47
+ resolve(JSON.parse(xhr.responseText));
48
+ } catch (e) {
49
+ reject(new Error('Failed to parse JSON. Raw (200 chars): ' + xhr.responseText.substring(0, 200)));
50
+ }
51
+ } else {
52
+ reject(new Error('XHR HTTP Status: ' + xhr.status));
53
+ }
54
+ };
55
+ xhr.onerror = () => reject(new Error('XHR Network Error'));
56
+ xhr.ontimeout = () => reject(new Error('XHR Timeout'));
57
+ xhr.send();
58
+ });
59
+ }
60
+ `;
61
+ let data;
62
+ try {
63
+ data = await page.evaluate(evaluateScript);
64
+ }
65
+ catch (e) {
66
+ throw new Error('API evaluate failed: ' + e.message);
67
+ }
68
+ if (data.code !== 0) {
69
+ if (data.code === 37) {
70
+ throw new Error('Cookie ε·²θΏ‡ζœŸοΌθ―·εœ¨ε½“ε‰ Chrome ζ΅θ§ˆε™¨δΈ­ι‡ζ–°η™»ε½• BOSS η›΄θ˜γ€‚');
71
+ }
72
+ throw new Error(`BOSS API error: ${data.message || 'Unknown'} (code=${data.code})`);
73
+ }
74
+ const zpData = data.zpData || {};
75
+ const jobInfo = zpData.jobInfo || {};
76
+ const bossInfo = zpData.bossInfo || {};
77
+ const brandComInfo = zpData.brandComInfo || {};
78
+ if (!jobInfo.jobName) {
79
+ throw new Error('θ―₯θŒδ½δΏ‘ζ―δΈε­˜εœ¨ζˆ–ε·²δΈ‹ζžΆ');
80
+ }
81
+ return [{
82
+ name: jobInfo.jobName || '',
83
+ salary: jobInfo.salaryDesc || '',
84
+ experience: jobInfo.experienceName || '',
85
+ degree: jobInfo.degreeName || '',
86
+ city: jobInfo.locationName || '',
87
+ district: [jobInfo.areaDistrict, jobInfo.businessDistrict].filter(Boolean).join('Β·'),
88
+ description: jobInfo.postDescription || '',
89
+ skills: (jobInfo.showSkills || []).join(', '),
90
+ welfare: (brandComInfo.labels || []).join(', '),
91
+ boss_name: bossInfo.name || '',
92
+ boss_title: bossInfo.title || '',
93
+ active_time: bossInfo.activeTimeDesc || '',
94
+ company: brandComInfo.brandName || bossInfo.brandName || '',
95
+ industry: brandComInfo.industryName || '',
96
+ scale: brandComInfo.scaleName || '',
97
+ stage: brandComInfo.stageName || '',
98
+ address: jobInfo.address || '',
99
+ url: jobInfo.encryptId
100
+ ? 'https://www.zhipin.com/job_detail/' + jobInfo.encryptId + '.html'
101
+ : '',
102
+ }];
103
+ },
104
+ });
@@ -78,7 +78,7 @@ cli({
78
78
  { name: 'page', type: 'int', default: 1, help: 'Page number' },
79
79
  { name: 'limit', type: 'int', default: 15, help: 'Number of results' },
80
80
  ],
81
- columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'url'],
81
+ columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'security_id', 'url'],
82
82
  func: async (page, kwargs) => {
83
83
  if (!page)
84
84
  throw new Error('Browser page required');
@@ -180,6 +180,7 @@ cli({
180
180
  degree: j.jobDegree,
181
181
  skills: (j.skills || []).join(','),
182
182
  boss: j.bossName + ' Β· ' + j.bossTitle,
183
+ security_id: j.securityId || '',
183
184
  url: 'https://www.zhipin.com/job_detail/' + j.encryptJobId + '.html',
184
185
  });
185
186
  addedInBatch++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,115 @@
1
+ /**
2
+ * BOSSη›΄θ˜ job detail β€” fetch full job posting details via browser cookie API.
3
+ *
4
+ * Uses securityId from search results to call the detail API.
5
+ * Returns: job description, skills, welfare, boss info, company info, address.
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ import type { IPage } from '../../types.js';
9
+
10
+ cli({
11
+ site: 'boss',
12
+ name: 'detail',
13
+ description: 'BOSSη›΄θ˜ζŸ₯ηœ‹θŒδ½θ―¦ζƒ…',
14
+ domain: 'www.zhipin.com',
15
+ strategy: Strategy.COOKIE,
16
+
17
+ browser: true,
18
+ args: [
19
+ { name: 'security_id', required: true, help: 'Security ID from search results (securityId field)' },
20
+ ],
21
+ columns: [
22
+ 'name', 'salary', 'experience', 'degree', 'city', 'district',
23
+ 'description', 'skills', 'welfare',
24
+ 'boss_name', 'boss_title', 'active_time',
25
+ 'company', 'industry', 'scale', 'stage',
26
+ 'address', 'url',
27
+ ],
28
+ func: async (page: IPage | null, kwargs) => {
29
+ if (!page) throw new Error('Browser page required');
30
+
31
+ const securityId = kwargs.security_id;
32
+
33
+ // Navigate to zhipin.com first to establish cookie context (referrer + cookies)
34
+ await page.goto('https://www.zhipin.com/web/geek/job');
35
+ await page.wait({ time: 1 });
36
+
37
+ const targetUrl = `https://www.zhipin.com/wapi/zpgeek/job/detail.json?securityId=${encodeURIComponent(securityId)}`;
38
+
39
+ if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) {
40
+ console.error(`[opencli:boss] Fetching job detail...`);
41
+ }
42
+
43
+ const evaluateScript = `
44
+ async () => {
45
+ return new Promise((resolve, reject) => {
46
+ const xhr = new window.XMLHttpRequest();
47
+ xhr.open('GET', ${JSON.stringify(targetUrl)}, true);
48
+ xhr.withCredentials = true;
49
+ xhr.timeout = 15000;
50
+ xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
51
+ xhr.onload = () => {
52
+ if (xhr.status >= 200 && xhr.status < 300) {
53
+ try {
54
+ resolve(JSON.parse(xhr.responseText));
55
+ } catch (e) {
56
+ reject(new Error('Failed to parse JSON. Raw (200 chars): ' + xhr.responseText.substring(0, 200)));
57
+ }
58
+ } else {
59
+ reject(new Error('XHR HTTP Status: ' + xhr.status));
60
+ }
61
+ };
62
+ xhr.onerror = () => reject(new Error('XHR Network Error'));
63
+ xhr.ontimeout = () => reject(new Error('XHR Timeout'));
64
+ xhr.send();
65
+ });
66
+ }
67
+ `;
68
+
69
+ let data: any;
70
+ try {
71
+ data = await page.evaluate(evaluateScript);
72
+ } catch (e: any) {
73
+ throw new Error('API evaluate failed: ' + e.message);
74
+ }
75
+
76
+ if (data.code !== 0) {
77
+ if (data.code === 37) {
78
+ throw new Error('Cookie ε·²θΏ‡ζœŸοΌθ―·εœ¨ε½“ε‰ Chrome ζ΅θ§ˆε™¨δΈ­ι‡ζ–°η™»ε½• BOSS η›΄θ˜γ€‚');
79
+ }
80
+ throw new Error(`BOSS API error: ${data.message || 'Unknown'} (code=${data.code})`);
81
+ }
82
+
83
+ const zpData = data.zpData || {};
84
+ const jobInfo = zpData.jobInfo || {};
85
+ const bossInfo = zpData.bossInfo || {};
86
+ const brandComInfo = zpData.brandComInfo || {};
87
+
88
+ if (!jobInfo.jobName) {
89
+ throw new Error('θ―₯θŒδ½δΏ‘ζ―δΈε­˜εœ¨ζˆ–ε·²δΈ‹ζžΆ');
90
+ }
91
+
92
+ return [{
93
+ name: jobInfo.jobName || '',
94
+ salary: jobInfo.salaryDesc || '',
95
+ experience: jobInfo.experienceName || '',
96
+ degree: jobInfo.degreeName || '',
97
+ city: jobInfo.locationName || '',
98
+ district: [jobInfo.areaDistrict, jobInfo.businessDistrict].filter(Boolean).join('Β·'),
99
+ description: jobInfo.postDescription || '',
100
+ skills: (jobInfo.showSkills || []).join(', '),
101
+ welfare: (brandComInfo.labels || []).join(', '),
102
+ boss_name: bossInfo.name || '',
103
+ boss_title: bossInfo.title || '',
104
+ active_time: bossInfo.activeTimeDesc || '',
105
+ company: brandComInfo.brandName || bossInfo.brandName || '',
106
+ industry: brandComInfo.industryName || '',
107
+ scale: brandComInfo.scaleName || '',
108
+ stage: brandComInfo.stageName || '',
109
+ address: jobInfo.address || '',
110
+ url: jobInfo.encryptId
111
+ ? 'https://www.zhipin.com/job_detail/' + jobInfo.encryptId + '.html'
112
+ : '',
113
+ }];
114
+ },
115
+ });
@@ -81,7 +81,7 @@ cli({
81
81
  { name: 'page', type: 'int', default: 1, help: 'Page number' },
82
82
  { name: 'limit', type: 'int', default: 15, help: 'Number of results' },
83
83
  ],
84
- columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'url'],
84
+ columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'security_id', 'url'],
85
85
  func: async (page: IPage | null, kwargs) => {
86
86
  if (!page) throw new Error('Browser page required');
87
87
 
@@ -191,6 +191,7 @@ cli({
191
191
  degree: j.jobDegree,
192
192
  skills: (j.skills || []).join(','),
193
193
  boss: j.bossName + ' Β· ' + j.bossTitle,
194
+ security_id: j.securityId || '',
194
195
  url: 'https://www.zhipin.com/job_detail/' + j.encryptJobId + '.html',
195
196
  });
196
197
  addedInBatch++;