feat(boss): add job detail adapter
Add `boss detail` command to fetch full job posting details using securityId from search results. Fields returned: job description, skills, welfare, boss info (name, title, active time), company info (industry, scale, stage), address. Tested with real API calls against multiple job postings. Usage: opencli boss detail --security_id <id_from_search> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b2f88b2cf
commit
c230f3e5ad
1 file changed
+111
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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
|
||||
await page.goto('https://www.zhipin.com/web/geek/job');
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
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', '${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 || {};
|
||||
|
||||
return [{
|
||||
name: jobInfo.jobName || '',
|
||||
salary: jobInfo.salaryDesc || '',
|
||||
experience: jobInfo.experienceName || '',
|
||||
degree: jobInfo.degreeName || '',
|
||||
city: jobInfo.locationName || '',
|
||||
district: jobInfo.address || '',
|
||||
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'
|
||||
: '',
|
||||
}];
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user