feat(boss): add 8 new recruitment management commands (#133)
New commands: - joblist: view my published jobs - recommend: view recommended candidates (new greetings list) - greet: send greeting to initiate chat with candidate - mark: add/remove labels on candidates - invite: send interview invitation - stats: job statistics (chats count) - batchgreet: batch greet recommended candidates - exchange: request phone/wechat exchange All commands tested and build passing (222 entries).
This commit is contained in:
8 files changed
+1110
No files matched your search
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* BOSS直聘 batchgreet — batch greet recommended candidates.
|
||||
*
|
||||
* Combines recommend (greetRecSortList) + greet (UI automation).
|
||||
* Sends greeting messages to multiple candidates sequentially.
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
name: 'batchgreet',
|
||||
description: 'BOSS直聘批量向推荐候选人发送招呼',
|
||||
domain: 'www.zhipin.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'job_id', default: '', help: 'Filter by encrypted job ID (greet all jobs if empty)' },
|
||||
{ name: 'limit', type: 'int', default: 5, help: 'Max candidates to greet' },
|
||||
{ name: 'text', default: '', help: 'Custom greeting message (uses default if empty)' },
|
||||
],
|
||||
columns: ['name', 'status', 'detail'],
|
||||
func: async (page: IPage | null, kwargs) => {
|
||||
if (!page) throw new Error('Browser page required');
|
||||
|
||||
const filterJobId = kwargs.job_id || '';
|
||||
const limit = kwargs.limit || 5;
|
||||
const text = kwargs.text || '你好,请问您对这个职位感兴趣吗?';
|
||||
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error(`[opencli:boss] Batch greeting up to ${limit} candidates...`);
|
||||
}
|
||||
|
||||
await page.goto('https://www.zhipin.com/web/chat/index');
|
||||
await page.wait({ time: 3 });
|
||||
|
||||
// Get recommended candidates
|
||||
const listData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (listData.code !== 0) {
|
||||
if (listData.code === 7 || listData.code === 37) {
|
||||
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
||||
}
|
||||
throw new Error(`获取推荐列表失败: ${listData.message}`);
|
||||
}
|
||||
|
||||
let candidates = listData.zpData?.friendList || [];
|
||||
if (filterJobId) {
|
||||
candidates = candidates.filter((f: any) => f.encryptJobId === filterJobId);
|
||||
}
|
||||
candidates = candidates.slice(0, limit);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return [{ name: '-', status: '⚠️ 无候选人', detail: '当前没有待招呼的推荐候选人' }];
|
||||
}
|
||||
|
||||
const results: any[] = [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const numericUid = candidate.uid;
|
||||
const friendName = candidate.name || '候选人';
|
||||
|
||||
try {
|
||||
// Click on candidate
|
||||
const clicked: any = await page.evaluate(`
|
||||
async () => {
|
||||
const item = document.querySelector('#_${numericUid}-0') || document.querySelector('[id^="_${numericUid}"]');
|
||||
if (item) {
|
||||
item.click();
|
||||
return { clicked: true };
|
||||
}
|
||||
const items = document.querySelectorAll('.geek-item');
|
||||
for (const el of items) {
|
||||
if (el.id && el.id.startsWith('_${numericUid}')) {
|
||||
el.click();
|
||||
return { clicked: true };
|
||||
}
|
||||
}
|
||||
return { clicked: false };
|
||||
}
|
||||
`);
|
||||
|
||||
if (!clicked.clicked) {
|
||||
results.push({ name: friendName, status: '❌ 跳过', detail: '在聊天列表中未找到' });
|
||||
continue;
|
||||
}
|
||||
|
||||
await page.wait({ time: 2 });
|
||||
|
||||
// Type message
|
||||
const typed: any = await page.evaluate(`
|
||||
async () => {
|
||||
const selectors = [
|
||||
'.chat-editor [contenteditable="true"]',
|
||||
'.chat-input [contenteditable="true"]',
|
||||
'[contenteditable="true"]',
|
||||
'textarea',
|
||||
];
|
||||
for (const sel of selectors) {
|
||||
const el = document.querySelector(sel);
|
||||
if (el && el.offsetParent !== null) {
|
||||
el.focus();
|
||||
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
||||
el.value = ${JSON.stringify(text)};
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
} else {
|
||||
el.textContent = '';
|
||||
el.focus();
|
||||
document.execCommand('insertText', false, ${JSON.stringify(text)});
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
return { found: true };
|
||||
}
|
||||
}
|
||||
return { found: false };
|
||||
}
|
||||
`);
|
||||
|
||||
if (!typed.found) {
|
||||
results.push({ name: friendName, status: '❌ 失败', detail: '找不到消息输入框' });
|
||||
continue;
|
||||
}
|
||||
|
||||
await page.wait({ time: 0.5 });
|
||||
|
||||
// Click send
|
||||
const sent: any = await page.evaluate(`
|
||||
async () => {
|
||||
const btn = document.querySelector('.conversation-editor .submit')
|
||||
|| document.querySelector('.submit-content .submit')
|
||||
|| document.querySelector('.conversation-operate .submit');
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return { clicked: true };
|
||||
}
|
||||
return { clicked: false };
|
||||
}
|
||||
`);
|
||||
|
||||
if (!sent.clicked) {
|
||||
await page.pressKey('Enter');
|
||||
}
|
||||
|
||||
await page.wait({ time: 1.5 });
|
||||
|
||||
results.push({ name: friendName, status: '✅ 已发送', detail: text });
|
||||
} catch (e: any) {
|
||||
results.push({ name: friendName, status: '❌ 失败', detail: e.message?.substring(0, 80) || '未知错误' });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* BOSS直聘 exchange — request phone/wechat exchange with a candidate.
|
||||
*
|
||||
* Uses POST /wapi/zpchat/exchange/request to send an exchange request.
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
name: 'exchange',
|
||||
description: 'BOSS直聘交换联系方式(请求手机/微信)',
|
||||
domain: 'www.zhipin.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate' },
|
||||
{ name: 'type', default: 'phone', choices: ['phone', 'wechat'], help: 'Exchange type: phone or wechat' },
|
||||
],
|
||||
columns: ['status', 'detail'],
|
||||
func: async (page: IPage | null, kwargs) => {
|
||||
if (!page) throw new Error('Browser page required');
|
||||
|
||||
const uid = kwargs.uid;
|
||||
const exchangeType = kwargs.type || 'phone';
|
||||
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error(`[opencli:boss] Requesting ${exchangeType} exchange for ${uid}...`);
|
||||
}
|
||||
|
||||
await page.goto('https://www.zhipin.com/web/chat/index');
|
||||
await page.wait({ time: 2 });
|
||||
|
||||
// Find candidate
|
||||
let friend: any = null;
|
||||
|
||||
// Check greet list
|
||||
const greetData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (greetData.code === 0) {
|
||||
friend = (greetData.zpData?.friendList || []).find((f: any) => f.encryptUid === uid);
|
||||
}
|
||||
|
||||
if (!friend) {
|
||||
const friendData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
if (friendData.code === 0) {
|
||||
friend = (friendData.zpData?.friendList || []).find((f: any) => f.encryptUid === uid);
|
||||
}
|
||||
}
|
||||
|
||||
if (!friend) {
|
||||
throw new Error('未找到该候选人');
|
||||
}
|
||||
|
||||
const numericUid = friend.uid;
|
||||
const friendName = friend.name || '候选人';
|
||||
const securityId = friend.securityId || '';
|
||||
|
||||
// type mapping from JS source: 1=phone, 2=wechat, 4=resume
|
||||
const typeId = exchangeType === 'wechat' ? 2 : 1;
|
||||
|
||||
// Params from JS: {type, securityId, uniqueId, name}
|
||||
const params = new URLSearchParams({
|
||||
type: String(typeId),
|
||||
securityId: securityId,
|
||||
uniqueId: String(numericUid),
|
||||
name: friendName,
|
||||
});
|
||||
|
||||
// POST with form-urlencoded (discovered from 336.js bundle)
|
||||
const data: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', 'https://www.zhipin.com/wapi/zpchat/exchange/request', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send(${JSON.stringify(params.toString())});
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (data.code !== 0) {
|
||||
if (data.code === 7 || data.code === 37) {
|
||||
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
||||
}
|
||||
throw new Error(`交换请求失败: ${data.message} (code=${data.code})`);
|
||||
}
|
||||
|
||||
const typeLabel = exchangeType === 'wechat' ? '微信' : '手机号';
|
||||
return [{
|
||||
status: '✅ 交换请求已发送',
|
||||
detail: `已向 ${friendName} 发送${typeLabel}交换请求`,
|
||||
}];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* BOSS直聘 greet — send greeting to a new candidate (initiate chat).
|
||||
*
|
||||
* This is different from send.ts which messages existing contacts.
|
||||
* For new candidates (from recommend list), we navigate to their chat page
|
||||
* and use UI automation to send the greeting message.
|
||||
*
|
||||
* The greetRecSortList provides candidates who have applied or been recommended.
|
||||
* We click on them in the list and send a greeting.
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
name: 'greet',
|
||||
description: 'BOSS直聘向新候选人发送招呼(开始聊天)',
|
||||
domain: 'www.zhipin.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate (from recommend)' },
|
||||
{ name: 'security_id', required: true, help: 'Security ID of the candidate' },
|
||||
{ name: 'job_id', required: true, help: 'Encrypted job ID' },
|
||||
{ name: 'text', default: '', help: 'Custom greeting message (uses default template if empty)' },
|
||||
],
|
||||
columns: ['status', 'detail'],
|
||||
func: async (page: IPage | null, kwargs) => {
|
||||
if (!page) throw new Error('Browser page required');
|
||||
|
||||
const uid = kwargs.uid;
|
||||
const securityId = kwargs.security_id;
|
||||
const jobId = kwargs.job_id;
|
||||
const text = kwargs.text;
|
||||
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error(`[opencli:boss] Greeting candidate ${uid}...`);
|
||||
}
|
||||
|
||||
// Navigate to chat page
|
||||
await page.goto('https://www.zhipin.com/web/chat/index');
|
||||
await page.wait({ time: 3 });
|
||||
|
||||
// Find the candidate in the greet list by encryptUid
|
||||
const listData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (listData.code !== 0) {
|
||||
if (listData.code === 7 || listData.code === 37) {
|
||||
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
||||
}
|
||||
throw new Error(`获取候选人列表失败: ${listData.message}`);
|
||||
}
|
||||
|
||||
// Also check the regular friend list
|
||||
let target: any = null;
|
||||
const greetList = listData.zpData?.friendList || [];
|
||||
target = greetList.find((f: any) => f.encryptUid === uid);
|
||||
|
||||
let numericUid: string | null = null;
|
||||
let friendName = '候选人';
|
||||
|
||||
if (target) {
|
||||
numericUid = target.uid;
|
||||
friendName = target.name || friendName;
|
||||
}
|
||||
|
||||
if (!numericUid) {
|
||||
// Try to find in friend list
|
||||
const friendData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (friendData.code === 0) {
|
||||
const allFriends = friendData.zpData?.friendList || [];
|
||||
const found = allFriends.find((f: any) => f.encryptUid === uid);
|
||||
if (found) {
|
||||
numericUid = found.uid;
|
||||
friendName = found.name || friendName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!numericUid) {
|
||||
throw new Error('未找到该候选人,请确认 uid 是否正确(可从 recommend 命令获取)');
|
||||
}
|
||||
|
||||
// Click on the candidate in the chat list
|
||||
const clicked: any = await page.evaluate(`
|
||||
async () => {
|
||||
const item = document.querySelector('#_${numericUid}-0') || document.querySelector('[id^="_${numericUid}"]');
|
||||
if (item) {
|
||||
item.click();
|
||||
return { clicked: true, id: item.id };
|
||||
}
|
||||
const items = document.querySelectorAll('.geek-item');
|
||||
for (const el of items) {
|
||||
if (el.id && el.id.startsWith('_${numericUid}')) {
|
||||
el.click();
|
||||
return { clicked: true, id: el.id };
|
||||
}
|
||||
}
|
||||
return { clicked: false };
|
||||
}
|
||||
`);
|
||||
|
||||
if (!clicked.clicked) {
|
||||
throw new Error('无法在聊天列表中找到该用户,候选人可能不在当前列表中');
|
||||
}
|
||||
|
||||
await page.wait({ time: 2 });
|
||||
|
||||
// Type the message
|
||||
const msgText = text || '你好,请问您对这个职位感兴趣吗?';
|
||||
|
||||
const typed: any = await page.evaluate(`
|
||||
async () => {
|
||||
const selectors = [
|
||||
'.chat-editor [contenteditable="true"]',
|
||||
'.chat-input [contenteditable="true"]',
|
||||
'.message-editor [contenteditable="true"]',
|
||||
'.chat-conversation [contenteditable="true"]',
|
||||
'[contenteditable="true"]',
|
||||
'textarea',
|
||||
];
|
||||
|
||||
for (const sel of selectors) {
|
||||
const el = document.querySelector(sel);
|
||||
if (el && el.offsetParent !== null) {
|
||||
el.focus();
|
||||
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
||||
el.value = ${JSON.stringify(msgText)};
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
} else {
|
||||
el.textContent = '';
|
||||
el.focus();
|
||||
document.execCommand('insertText', false, ${JSON.stringify(msgText)});
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
return { found: true, selector: sel };
|
||||
}
|
||||
}
|
||||
return { found: false };
|
||||
}
|
||||
`);
|
||||
|
||||
if (!typed.found) {
|
||||
throw new Error('找不到消息输入框');
|
||||
}
|
||||
|
||||
await page.wait({ time: 0.5 });
|
||||
|
||||
// Click send button
|
||||
const sent: any = await page.evaluate(`
|
||||
async () => {
|
||||
const btn = document.querySelector('.conversation-editor .submit')
|
||||
|| document.querySelector('.submit-content .submit')
|
||||
|| document.querySelector('.conversation-operate .submit');
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return { clicked: true };
|
||||
}
|
||||
return { clicked: false };
|
||||
}
|
||||
`);
|
||||
|
||||
if (!sent.clicked) {
|
||||
await page.pressKey('Enter');
|
||||
}
|
||||
|
||||
await page.wait({ time: 1 });
|
||||
|
||||
return [{ status: '✅ 招呼已发送', detail: `已向 ${friendName} 发送: ${msgText}` }];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* BOSS直聘 invite — send interview invitation to a candidate.
|
||||
*
|
||||
* Uses POST /wapi/zpinterview/boss/interview/invite to send interview invitations.
|
||||
* Address and contact info come from the boss's saved settings.
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
name: 'invite',
|
||||
description: 'BOSS直聘发送面试邀请',
|
||||
domain: 'www.zhipin.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate' },
|
||||
{ name: 'time', required: true, help: 'Interview time (e.g. 2025-04-01 14:00)' },
|
||||
{ name: 'address', default: '', help: 'Interview address (uses saved address if empty)' },
|
||||
{ name: 'contact', default: '', help: 'Contact person name (uses saved contact if empty)' },
|
||||
],
|
||||
columns: ['status', 'detail'],
|
||||
func: async (page: IPage | null, kwargs) => {
|
||||
if (!page) throw new Error('Browser page required');
|
||||
|
||||
const uid = kwargs.uid;
|
||||
const timeStr = kwargs.time;
|
||||
const address = kwargs.address;
|
||||
const contact = kwargs.contact;
|
||||
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error(`[opencli:boss] Sending interview invitation to ${uid}...`);
|
||||
}
|
||||
|
||||
await page.goto('https://www.zhipin.com/web/chat/index');
|
||||
await page.wait({ time: 2 });
|
||||
|
||||
// Get candidate info
|
||||
let friend: any = null;
|
||||
|
||||
// Check greet list first
|
||||
const greetData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (greetData.code === 0) {
|
||||
friend = (greetData.zpData?.friendList || []).find((f: any) => f.encryptUid === uid);
|
||||
}
|
||||
|
||||
if (!friend) {
|
||||
const friendData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
if (friendData.code === 0) {
|
||||
friend = (friendData.zpData?.friendList || []).find((f: any) => f.encryptUid === uid);
|
||||
}
|
||||
}
|
||||
|
||||
if (!friend) {
|
||||
throw new Error('未找到该候选人');
|
||||
}
|
||||
|
||||
const numericUid = friend.uid;
|
||||
const friendName = friend.name || '候选人';
|
||||
const securityId = friend.securityId || '';
|
||||
const encJobId = friend.encryptJobId || '';
|
||||
|
||||
// Get saved contact info
|
||||
const contactData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zpinterview/boss/interview/contactInit', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 10000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
const contactId = contactData.zpData?.contactId || '';
|
||||
const contactName = contact || contactData.zpData?.contactName || '';
|
||||
const contactPhone = contactData.zpData?.contactPhone || '';
|
||||
|
||||
// Get saved address
|
||||
const addressData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zpinterview/boss/interview/listAddress', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 10000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
const savedAddress = addressData.zpData?.list?.[0] || {};
|
||||
const addressText = address || savedAddress.cityAddressText || savedAddress.addressText || '';
|
||||
|
||||
// Parse interview time
|
||||
const interviewTime = new Date(timeStr).getTime();
|
||||
if (isNaN(interviewTime)) {
|
||||
throw new Error(`时间格式错误: ${timeStr},请使用格式如 2025-04-01 14:00`);
|
||||
}
|
||||
|
||||
// Send interview invitation
|
||||
const params = new URLSearchParams({
|
||||
uid: String(numericUid),
|
||||
securityId: securityId,
|
||||
encryptJobId: encJobId,
|
||||
interviewTime: String(interviewTime),
|
||||
contactId: contactId,
|
||||
contactName: contactName,
|
||||
contactPhone: contactPhone,
|
||||
address: addressText,
|
||||
interviewType: '1', // 1 = onsite
|
||||
});
|
||||
|
||||
const data: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', 'https://www.zhipin.com/wapi/zpinterview/boss/interview/invite.json', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send(${JSON.stringify(params.toString())});
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (data.code !== 0) {
|
||||
if (data.code === 7 || data.code === 37) {
|
||||
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
||||
}
|
||||
throw new Error(`面试邀请发送失败: ${data.message} (code=${data.code})`);
|
||||
}
|
||||
|
||||
return [{
|
||||
status: '✅ 面试邀请已发送',
|
||||
detail: `已向 ${friendName} 发送面试邀请\n时间: ${timeStr}\n地点: ${addressText}\n联系人: ${contactName}`,
|
||||
}];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* BOSS直聘 job list — list my published jobs via boss API.
|
||||
*
|
||||
* Uses /wapi/zpjob/job/chatted/jobList to get job list with status info.
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
name: 'joblist',
|
||||
description: 'BOSS直聘查看我发布的职位列表',
|
||||
domain: 'www.zhipin.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [],
|
||||
columns: ['job_name', 'salary', 'city', 'status', 'encrypt_job_id'],
|
||||
func: async (page: IPage | null, kwargs) => {
|
||||
if (!page) throw new Error('Browser page required');
|
||||
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error('[opencli:boss] Fetching job list...');
|
||||
}
|
||||
|
||||
await page.goto('https://www.zhipin.com/web/chat/index');
|
||||
await page.wait({ time: 2 });
|
||||
|
||||
const targetUrl = 'https://www.zhipin.com/wapi/zpjob/job/chatted/jobList';
|
||||
|
||||
const data: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '${targetUrl}', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.ontimeout = () => reject(new Error('Timeout'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (data.code !== 0) {
|
||||
if (data.code === 7 || data.code === 37) {
|
||||
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
||||
}
|
||||
throw new Error(`API error: ${data.message} (code=${data.code})`);
|
||||
}
|
||||
|
||||
const jobs = data.zpData || [];
|
||||
|
||||
return jobs.map((j: any) => ({
|
||||
job_name: j.jobName || '',
|
||||
salary: j.salaryDesc || '',
|
||||
city: j.address || '',
|
||||
status: j.jobOnlineStatus === 1 ? '在线' : '已关闭',
|
||||
encrypt_job_id: j.encryptJobId || '',
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* BOSS直聘 mark — label/mark a candidate.
|
||||
*
|
||||
* Uses /wapi/zprelation/friend/label/addMark to add a label to a candidate,
|
||||
* and /wapi/zprelation/friend/label/deleteMark to remove one.
|
||||
*
|
||||
* Available labels (from /wapi/zprelation/friend/label/get):
|
||||
* 1=新招呼, 2=沟通中, 3=已约面, 4=已获取简历, 5=已交换电话,
|
||||
* 6=已交换微信, 7=不合适, 8=牛人发起, 11=收藏
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
const LABEL_MAP: Record<string, number> = {
|
||||
'新招呼': 1, '沟通中': 2, '已约面': 3, '已获取简历': 4,
|
||||
'已交换电话': 5, '已交换微信': 6, '不合适': 7, '牛人发起': 8, '收藏': 11,
|
||||
};
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
name: 'mark',
|
||||
description: 'BOSS直聘给候选人添加标签',
|
||||
domain: 'www.zhipin.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate' },
|
||||
{ name: 'label', required: true, help: 'Label name (新招呼/沟通中/已约面/已获取简历/已交换电话/已交换微信/不合适/收藏) or label ID' },
|
||||
{ name: 'remove', type: 'boolean', default: false, help: 'Remove the label instead of adding' },
|
||||
],
|
||||
columns: ['status', 'detail'],
|
||||
func: async (page: IPage | null, kwargs) => {
|
||||
if (!page) throw new Error('Browser page required');
|
||||
|
||||
const uid = kwargs.uid;
|
||||
const labelInput = kwargs.label;
|
||||
const remove = kwargs.remove || false;
|
||||
|
||||
// Resolve label to ID
|
||||
let labelId: number;
|
||||
if (LABEL_MAP[labelInput]) {
|
||||
labelId = LABEL_MAP[labelInput];
|
||||
} else if (!isNaN(Number(labelInput))) {
|
||||
labelId = Number(labelInput);
|
||||
} else {
|
||||
// Try partial match
|
||||
const entry = Object.entries(LABEL_MAP).find(([k]) => k.includes(labelInput));
|
||||
if (entry) {
|
||||
labelId = entry[1];
|
||||
} else {
|
||||
throw new Error(`未知标签: ${labelInput}。可用标签: ${Object.keys(LABEL_MAP).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error(`[opencli:boss] ${remove ? 'Removing' : 'Adding'} label ${labelId} for ${uid}...`);
|
||||
}
|
||||
|
||||
await page.goto('https://www.zhipin.com/web/chat/index');
|
||||
await page.wait({ time: 2 });
|
||||
|
||||
// First get numeric UID from friend list
|
||||
const friendData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (friendData.code !== 0) {
|
||||
if (friendData.code === 7 || friendData.code === 37) {
|
||||
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
||||
}
|
||||
throw new Error(`获取好友列表失败: ${friendData.message}`);
|
||||
}
|
||||
|
||||
// Find in friend list (check multiple pages)
|
||||
let friend: any = null;
|
||||
let allFriends = friendData.zpData?.friendList || [];
|
||||
friend = allFriends.find((f: any) => f.encryptUid === uid);
|
||||
|
||||
if (!friend) {
|
||||
// Also check greetRecSortList
|
||||
const greetData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
if (greetData.code === 0) {
|
||||
friend = (greetData.zpData?.friendList || []).find((f: any) => f.encryptUid === uid);
|
||||
}
|
||||
}
|
||||
|
||||
if (!friend) {
|
||||
throw new Error('未找到该候选人');
|
||||
}
|
||||
|
||||
const numericUid = friend.uid;
|
||||
const friendName = friend.name || '候选人';
|
||||
const friendSource = friend.friendSource ?? 0;
|
||||
|
||||
const action = remove ? 'deleteMark' : 'addMark';
|
||||
const targetUrl = `https://www.zhipin.com/wapi/zprelation/friend/label/${action}`;
|
||||
|
||||
// The API uses friendId + friendSource + labelId (discovered from JS bundles)
|
||||
const params = new URLSearchParams({
|
||||
friendId: String(numericUid),
|
||||
friendSource: String(friendSource),
|
||||
labelId: String(labelId),
|
||||
});
|
||||
|
||||
// Try GET first (the N() wrapper in boss JS uses GET with query params)
|
||||
const data: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '${targetUrl}?${params.toString()}', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (data.code !== 0) {
|
||||
throw new Error(`标签操作失败: ${data.message} (code=${data.code})`);
|
||||
}
|
||||
|
||||
const labelName = Object.entries(LABEL_MAP).find(([, v]) => v === labelId)?.[0] || String(labelId);
|
||||
return [{
|
||||
status: remove ? '✅ 标签已移除' : '✅ 标签已添加',
|
||||
detail: `${friendName}: ${remove ? '移除' : '添加'}标签「${labelName}」`,
|
||||
}];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* BOSS直聘 recommend — view recommended candidates (新招呼/greet sort list).
|
||||
*
|
||||
* Uses /wapi/zprelation/friend/greetRecSortList to get system-recommended candidates.
|
||||
* These are candidates who have greeted or been recommended by the system.
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
name: 'recommend',
|
||||
description: 'BOSS直聘查看推荐候选人(新招呼列表)',
|
||||
domain: 'www.zhipin.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'limit', type: 'int', default: 20, help: 'Number of results to return' },
|
||||
],
|
||||
columns: ['name', 'job_name', 'last_time', 'labels', 'encrypt_uid', 'security_id', 'encrypt_job_id'],
|
||||
func: async (page: IPage | null, kwargs) => {
|
||||
if (!page) throw new Error('Browser page required');
|
||||
|
||||
const limit = kwargs.limit || 20;
|
||||
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error('[opencli:boss] Fetching recommended candidates...');
|
||||
}
|
||||
|
||||
await page.goto('https://www.zhipin.com/web/chat/index');
|
||||
await page.wait({ time: 2 });
|
||||
|
||||
// Get label definitions for mapping
|
||||
const labelData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/label/get', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 10000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({}); } };
|
||||
xhr.onerror = () => resolve({});
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
const labelMap: Record<number, string> = {};
|
||||
if (labelData.code === 0 && labelData.zpData?.labels) {
|
||||
for (const l of labelData.zpData.labels) {
|
||||
labelMap[l.labelId] = l.label;
|
||||
}
|
||||
}
|
||||
|
||||
// Get recommended candidates
|
||||
const targetUrl = 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList';
|
||||
|
||||
const data: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '${targetUrl}', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.ontimeout = () => reject(new Error('Timeout'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (data.code !== 0) {
|
||||
if (data.code === 7 || data.code === 37) {
|
||||
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
||||
}
|
||||
throw new Error(`API error: ${data.message} (code=${data.code})`);
|
||||
}
|
||||
|
||||
const friends = (data.zpData?.friendList || []).slice(0, limit);
|
||||
|
||||
return friends.map((f: any) => ({
|
||||
name: f.name || '',
|
||||
job_name: f.jobName || '',
|
||||
last_time: f.lastTime || '',
|
||||
labels: (f.relationLabelList || []).map((id: number) => labelMap[id] || String(id)).join(', '),
|
||||
encrypt_uid: f.encryptUid || '',
|
||||
security_id: f.securityId || '',
|
||||
encrypt_job_id: f.encryptJobId || '',
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* BOSS直聘 stats — job statistics overview.
|
||||
*
|
||||
* Uses /wapi/zpchat/chatHelper/statistics for total friend count,
|
||||
* and /wapi/zpjob/job/chatted/jobList for per-job info.
|
||||
* Since BOSS doesn't expose detailed per-job stats via API,
|
||||
* we show what's available: job status, chat info, and total stats.
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
name: 'stats',
|
||||
description: 'BOSS直聘职位数据统计',
|
||||
domain: 'www.zhipin.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'job_id', default: '', help: 'Encrypted job ID (show all if empty)' },
|
||||
],
|
||||
columns: ['job_name', 'salary', 'city', 'status', 'total_chats', 'encrypt_job_id'],
|
||||
func: async (page: IPage | null, kwargs) => {
|
||||
if (!page) throw new Error('Browser page required');
|
||||
|
||||
const filterJobId = kwargs.job_id || '';
|
||||
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error('[opencli:boss] Fetching job statistics...');
|
||||
}
|
||||
|
||||
await page.goto('https://www.zhipin.com/web/chat/index');
|
||||
await page.wait({ time: 2 });
|
||||
|
||||
// Get job list
|
||||
const jobData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zpjob/job/chatted/jobList', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
|
||||
xhr.onerror = () => reject(new Error('Network Error'));
|
||||
xhr.ontimeout = () => reject(new Error('Timeout'));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
if (jobData.code !== 0) {
|
||||
if (jobData.code === 7 || jobData.code === 37) {
|
||||
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
||||
}
|
||||
throw new Error(`API error: ${jobData.message} (code=${jobData.code})`);
|
||||
}
|
||||
|
||||
// Get total chat stats
|
||||
const chatStats: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zpchat/chatHelper/statistics', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 10000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({}); } };
|
||||
xhr.onerror = () => resolve({});
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
const totalFriends = chatStats.zpData?.totalFriendCount || 0;
|
||||
|
||||
// Get per-job chat counts from friend list
|
||||
const friendData: any = await page.evaluate(`
|
||||
async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 15000;
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({}); } };
|
||||
xhr.onerror = () => resolve({});
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
// Count chats per job
|
||||
const jobChatCounts: Record<string, number> = {};
|
||||
if (friendData.code === 0) {
|
||||
for (const f of (friendData.zpData?.friendList || [])) {
|
||||
const jobName = f.jobName || 'unknown';
|
||||
jobChatCounts[jobName] = (jobChatCounts[jobName] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let jobs = jobData.zpData || [];
|
||||
if (filterJobId) {
|
||||
jobs = jobs.filter((j: any) => j.encryptJobId === filterJobId);
|
||||
}
|
||||
|
||||
const results = jobs.map((j: any) => ({
|
||||
job_name: j.jobName || '',
|
||||
salary: j.salaryDesc || '',
|
||||
city: j.address || '',
|
||||
status: j.jobOnlineStatus === 1 ? '在线' : '已关闭',
|
||||
total_chats: String(jobChatCounts[j.jobName] || 0),
|
||||
encrypt_job_id: j.encryptJobId || '',
|
||||
}));
|
||||
|
||||
// Add summary row
|
||||
if (!filterJobId && results.length > 0) {
|
||||
results.push({
|
||||
job_name: '--- 总计 ---',
|
||||
salary: '',
|
||||
city: '',
|
||||
status: `${jobs.length} 个职位`,
|
||||
total_chats: String(totalFriends),
|
||||
encrypt_job_id: '',
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user