Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,8 @@ If you are an employer on BOSS直聘, these commands let you manage candidates f
```bash
# ─── Search & Discover (搜索 & 发现) ─────────────
boss recruiter search "golang" --city 深圳 --exp 3-5年 # Search candidates
boss recruiter recommend # Recommended candidates
boss recruiter recommend --job <encryptJobId> # Switch to different 岗位
boss recruiter recommend -p 2 # Next page
boss recruiter recommend --job <encryptJobId> # 推荐牛人 (paginated, 15/page)
boss recruiter recommend --job <encryptJobId> -p 2 # Next page (use hasMore flag)

# ─── Greet & Communicate (沟通) ──────────────────
boss recruiter greet <encryptGeekId> # Initiate chat with candidate
Expand Down Expand Up @@ -371,8 +370,8 @@ boss -v search "Python" # 详细日志
```bash
# 搜索 & 推荐
boss recruiter search "golang" --city 深圳 --exp 3-5年
boss recruiter recommend --job <encryptJobId> # 按岗位查看推荐牛人
boss recruiter recommend -p 2 # 翻页
boss recruiter recommend --job <encryptJobId> # 推荐牛人 (15/页, 真分页)
boss recruiter recommend --job <encryptJobId> -p 2 # 翻页

# 沟通
boss recruiter greet <encryptGeekId> # 向候选人打招呼
Expand Down
37 changes: 32 additions & 5 deletions boss_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
BOSS_FRIEND_LIST_URL,
BOSS_FRIEND_NOTE_URL,
BOSS_GREET_REC_SORT_URL,
BOSS_REC_GEEK_LIST_URL,
BOSS_GREET_SORT_LIST_URL,
BOSS_HISTORY_MSG_URL,
BOSS_INTERVIEW_INVITE_URL,
Expand Down Expand Up @@ -201,6 +202,8 @@ def _headers_for_request(self, url: str, params: dict[str, Any] | None = None) -
# Recruiter (boss) endpoints
elif url == BOSS_SEARCH_GEEK_URL:
headers["Referer"] = f"{BASE_URL}/web/chat/search"
elif url == BOSS_REC_GEEK_LIST_URL:
headers["Referer"] = f"{BASE_URL}/web/frame/recommend/"
elif url in (BOSS_VIEW_GEEK_URL, BOSS_SEND_MSG_URL):
headers["Referer"] = WEB_BOSS_CHAT_URL
elif url in (BOSS_FRIEND_LIST_URL, BOSS_FRIEND_DETAIL_URL, BOSS_LAST_MSG_URL,
Expand Down Expand Up @@ -553,11 +556,35 @@ def search_geeks(
return self._get(BOSS_SEARCH_GEEK_URL, params=params, action="搜索候选人")

def get_boss_recommend_geeks(self, page: int = 1, enc_job_id: str = "") -> dict[str, Any]:
"""Get recommended candidates (new greetings sorted by recommendation)."""
params: dict[str, Any] = {"page": page}
if enc_job_id:
params["encJobId"] = enc_job_id
return self._get(BOSS_GREET_REC_SORT_URL, params=params, action="推荐候选人")
"""Get the "推荐牛人" feed — the candidate-discovery list shown on the recruiter
recommend page. Truly paginated (15/page), with `hasMore` flag.

This used to hit ``greetRecSortList`` (a re-sort of already-greeted candidates,
~10 per job and `page` was a no-op). Now it hits the real ``/wapi/zpjob/rec/geek/list``
endpoint that the BOSS recruiter web page calls on infinite scroll.
"""
if not enc_job_id:
raise ValueError("enc_job_id (encryptJobId) is required for 推荐牛人 — pass --job")
params: dict[str, Any] = {
"age": "16,-1",
"school": 0,
"activation": 0,
"gender": 0,
"recentNotView": 0,
"exchangeResumeWithColleague": 0,
"major": 0,
"keyword1": -1,
"switchJobFrequency": 0,
"experience": 0,
"degree": 0,
"intention": 0,
"salary": 0,
"jobId": enc_job_id,
"page": page,
"coverScreenMemory": 0,
"cardType": 0,
}
return self._get(BOSS_REC_GEEK_LIST_URL, params=params, action="推荐牛人")

def get_boss_view_geek(
self, encrypt_geek_id: str, encrypt_job_id: str, security_id: str = "",
Expand Down
57 changes: 35 additions & 22 deletions boss_cli/commands/recruiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,56 +137,69 @@ def _render(data: dict) -> None:

@recruiter.command("recommend")
@click.option("-n", "--limit", "display_limit", default=0, type=int, help="显示数量 (0=全部)")
@click.option("-p", "--page", default=1, type=int, help="页码 (默认: 1)")
@click.option("--job", "enc_job_id", default="", help="关联职位 encryptJobId (切换岗位)")
@click.option("-p", "--page", default=1, type=int, help="页码 (默认: 1, 真分页, 15/页)")
@click.option("--job", "enc_job_id", required=True, help="目标职位 encryptJobId (必填)")
@structured_output_options
def recruiter_recommend(
display_limit: int, page: int, enc_job_id: str,
as_json: bool, as_yaml: bool,
) -> None:
"""推荐候选人列表 (支持 --job 切换岗位, -p 翻页)"""
"""推荐牛人 (候选人发现池) — 真分页, 每页 15 人, 有 hasMore 标志。

与 BOSS 直聘网页端 "推荐牛人" 一致, 支持滚动翻页继续拉取候选人。
"""
cred = require_auth()

def _action(c: BossClient) -> dict:
return c.get_boss_recommend_geeks(page=page, enc_job_id=enc_job_id)

def _render(data: dict) -> None:
friend_list = data.get("friendList", [])
total = len(friend_list)
geek_list = data.get("geekList", [])
has_more = data.get("hasMore", False)
total = len(geek_list)

if not friend_list:
console.print("[yellow]暂无推荐候选人[/yellow]")
if not geek_list:
console.print("[yellow]暂无推荐候选人 (该岗位推荐池可能已耗尽, 或换岗位试试)[/yellow]")
return

if display_limit > 0:
friend_list = friend_list[:display_limit]
geek_list = geek_list[:display_limit]

more_hint = " · 有更多 (翻页 -p)" if has_more else " · 已到末页"
table = Table(
title=f"推荐候选人 (显示 {len(friend_list)}/{total} 人)",
title=f"推荐牛人 第 {page} 页 (显示 {len(geek_list)}/{total} 人){more_hint}",
show_lines=True,
)
table.add_column("#", style="dim", width=3)
table.add_column("姓名", style="bold cyan", max_width=10)
table.add_column("职位", style="green", max_width=20)
table.add_column("encJobId", style="dim", max_width=28)
table.add_column("新牛人", max_width=4)
table.add_column("时间", style="dim", max_width=10)
table.add_column("年龄", max_width=5)
table.add_column("经验", max_width=10)
table.add_column("学历", max_width=6)
table.add_column("城市", style="green", max_width=10)
table.add_column("期望薪资", max_width=10)
table.add_column("活跃", style="dim", max_width=10)
table.add_column("encryptGeekId", style="dim", max_width=30)

for i, f in enumerate(friend_list, 1):
new_flag = "NEW" if f.get("newGeek") else ""
for i, g in enumerate(geek_list, 1):
card = g.get("geekCard", {}) if isinstance(g.get("geekCard"), dict) else g
table.add_row(
str(i),
f.get("name", "-"),
f.get("jobName", "-"),
f.get("encryptJobId", "-"),
new_flag,
f.get("lastTime", "-"),
card.get("geekName", "-"),
str(card.get("ageDesc", "-")),
str(card.get("workExpDesc") or card.get("geekWorkYear") or "-"),
str(card.get("degreeCategory") or card.get("geekDegree") or "-"),
str(card.get("cityName") or card.get("expectLocationName") or "-"),
str(card.get("salary") or card.get("salaryDesc") or "-"),
str(card.get("activeTimeDesc") or card.get("actionDateDesc") or "-"),
card.get("encryptGeekId") or g.get("encryptGeekId", "-"),
)

console.print(table)

console.print(" [dim]💡 切换岗位: boss recruiter recommend --job <encryptJobId>[/dim]")
console.print(" [dim] 限制显示: boss recruiter recommend -n 10[/dim]")
next_page = page + 1
if has_more:
console.print(f" [dim]→ 下一页: boss recruiter recommend --job {enc_job_id} -p {next_page}[/dim]")
console.print(f" [dim] 导出: boss recruiter recommend --job {enc_job_id} -p {page} --json[/dim]")
console.print(" [dim] 查看职位: boss recruiter jobs[/dim]")

handle_command(cred, action=_action, render=_render, as_json=as_json, as_yaml=as_yaml)
Expand Down
1 change: 1 addition & 0 deletions boss_cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
BOSS_FRIEND_NOTE_URL = "/wapi/zprelation/friend/getNoteAndLabels"
BOSS_GREET_SORT_LIST_URL = "/wapi/zprelation/friend/greetSort/getList"
BOSS_GREET_REC_SORT_URL = "/wapi/zprelation/friend/greetRecSortList"
BOSS_REC_GEEK_LIST_URL = "/wapi/zpjob/rec/geek/list"
BOSS_INTERVIEW_LIST_URL = "/wapi/zpinterview/boss/interview/valid/list"
BOSS_INTERVIEW_DETAIL_URL = "/wapi/zpinterview/boss/interview/detail"
BOSS_GREET_NEW_LIST_URL = "/wapi/zpchat/boss/newgreeting/getHistoryList"
Expand Down
Loading