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:
xuelinandClaude Opus 4.6 committed 2026-03-16 18:09:49 +08:00
1 parent 3b2f88b2cf
commit c230f3e5ad
1 file changed
+111
+111
View File
@@ -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'
: '',
}];
},
});