diff --git a/README.md b/README.md index b3adcb1..6846502 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ npm install -g @jackwener/opencli@latest | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `following` `followers` `notifications` `post` `reply` `delete` `like` | πŸ” Browser | | **reddit** | `hot` `frontpage` `search` `subreddit` | πŸ” Browser | | **weibo** | `hot` | πŸ” Browser | -| **boss** | `search` | πŸ” Browser | +| **boss** | `search` `detail` | πŸ” Browser | | **coupang** | `search` `add-to-cart` | πŸ” Browser | | **youtube** | `search` | πŸ” Browser | | **yahoo-finance** | `quote` | πŸ” Browser | diff --git a/README.zh-CN.md b/README.zh-CN.md index ec409c6..fbf30a5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -138,7 +138,7 @@ npm install -g @jackwener/opencli@latest | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `following` `followers` `notifications` `post` `reply` `delete` `like` | πŸ” ζ΅θ§ˆε™¨ | | **reddit** | `hot` `frontpage` `search` `subreddit` | πŸ” ζ΅θ§ˆε™¨ | | **weibo** | `hot` | πŸ” ζ΅θ§ˆε™¨ | -| **boss** | `search` | πŸ” ζ΅θ§ˆε™¨ | +| **boss** | `search` `detail` | πŸ” ζ΅θ§ˆε™¨ | | **coupang** | `search` `add-to-cart` | πŸ” ζ΅θ§ˆε™¨ | | **youtube** | `search` | πŸ” ζ΅θ§ˆε™¨ | | **yahoo-finance** | `quote` | πŸ” ζ΅θ§ˆε™¨ | diff --git a/src/clis/boss/detail.ts b/src/clis/boss/detail.ts new file mode 100644 index 0000000..26416c2 --- /dev/null +++ b/src/clis/boss/detail.ts @@ -0,0 +1,115 @@ +/** + * BOSSη›΄θ˜ job detail β€” fetch full job posting details via browser cookie API. + * + * Uses securityId from search results to call the detail API. + * Returns: job description, skills, welfare, boss info, company info, address. + */ +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; + +cli({ + site: 'boss', + name: 'detail', + description: 'BOSSη›΄θ˜ζŸ₯ηœ‹θŒδ½θ―¦ζƒ…', + domain: 'www.zhipin.com', + strategy: Strategy.COOKIE, + + browser: true, + args: [ + { name: 'security_id', required: true, help: 'Security ID from search results (securityId field)' }, + ], + columns: [ + 'name', 'salary', 'experience', 'degree', 'city', 'district', + 'description', 'skills', 'welfare', + 'boss_name', 'boss_title', 'active_time', + 'company', 'industry', 'scale', 'stage', + 'address', 'url', + ], + func: async (page: IPage | null, kwargs) => { + if (!page) throw new Error('Browser page required'); + + const securityId = kwargs.security_id; + + // Navigate to zhipin.com first to establish cookie context (referrer + cookies) + await page.goto('https://www.zhipin.com/web/geek/job'); + await page.wait({ time: 1 }); + + const targetUrl = `https://www.zhipin.com/wapi/zpgeek/job/detail.json?securityId=${encodeURIComponent(securityId)}`; + + if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) { + console.error(`[opencli:boss] Fetching job detail...`); + } + + const evaluateScript = ` + async () => { + return new Promise((resolve, reject) => { + const xhr = new window.XMLHttpRequest(); + xhr.open('GET', ${JSON.stringify(targetUrl)}, true); + xhr.withCredentials = true; + xhr.timeout = 15000; + xhr.setRequestHeader('Accept', 'application/json, text/plain, */*'); + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + try { + resolve(JSON.parse(xhr.responseText)); + } catch (e) { + reject(new Error('Failed to parse JSON. Raw (200 chars): ' + xhr.responseText.substring(0, 200))); + } + } else { + reject(new Error('XHR HTTP Status: ' + xhr.status)); + } + }; + xhr.onerror = () => reject(new Error('XHR Network Error')); + xhr.ontimeout = () => reject(new Error('XHR Timeout')); + xhr.send(); + }); + } + `; + + let data: any; + try { + data = await page.evaluate(evaluateScript); + } catch (e: any) { + throw new Error('API evaluate failed: ' + e.message); + } + + if (data.code !== 0) { + if (data.code === 37) { + throw new Error('Cookie ε·²θΏ‡ζœŸοΌθ―·εœ¨ε½“ε‰ Chrome ζ΅θ§ˆε™¨δΈ­ι‡ζ–°η™»ε½• BOSS η›΄θ˜γ€‚'); + } + throw new Error(`BOSS API error: ${data.message || 'Unknown'} (code=${data.code})`); + } + + const zpData = data.zpData || {}; + const jobInfo = zpData.jobInfo || {}; + const bossInfo = zpData.bossInfo || {}; + const brandComInfo = zpData.brandComInfo || {}; + + if (!jobInfo.jobName) { + throw new Error('θ―₯θŒδ½δΏ‘ζ―δΈε­˜εœ¨ζˆ–ε·²δΈ‹ζžΆ'); + } + + return [{ + name: jobInfo.jobName || '', + salary: jobInfo.salaryDesc || '', + experience: jobInfo.experienceName || '', + degree: jobInfo.degreeName || '', + city: jobInfo.locationName || '', + district: [jobInfo.areaDistrict, jobInfo.businessDistrict].filter(Boolean).join('Β·'), + description: jobInfo.postDescription || '', + skills: (jobInfo.showSkills || []).join(', '), + welfare: (brandComInfo.labels || []).join(', '), + boss_name: bossInfo.name || '', + boss_title: bossInfo.title || '', + active_time: bossInfo.activeTimeDesc || '', + company: brandComInfo.brandName || bossInfo.brandName || '', + industry: brandComInfo.industryName || '', + scale: brandComInfo.scaleName || '', + stage: brandComInfo.stageName || '', + address: jobInfo.address || '', + url: jobInfo.encryptId + ? 'https://www.zhipin.com/job_detail/' + jobInfo.encryptId + '.html' + : '', + }]; + }, +}); diff --git a/src/clis/boss/search.ts b/src/clis/boss/search.ts index 984ee89..8db70ed 100644 --- a/src/clis/boss/search.ts +++ b/src/clis/boss/search.ts @@ -81,7 +81,7 @@ cli({ { name: 'page', type: 'int', default: 1, help: 'Page number' }, { name: 'limit', type: 'int', default: 15, help: 'Number of results' }, ], - columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'url'], + columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'security_id', 'url'], func: async (page: IPage | null, kwargs) => { if (!page) throw new Error('Browser page required'); @@ -191,6 +191,7 @@ cli({ degree: j.jobDegree, skills: (j.skills || []).join(','), boss: j.bossName + ' Β· ' + j.bossTitle, + security_id: j.securityId || '', url: 'https://www.zhipin.com/job_detail/' + j.encryptJobId + '.html', }); addedInBatch++;