diff --git a/README.md b/README.md index 7221e90..3a0a678 100644 --- a/README.md +++ b/README.md @@ -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 # Switch to different 岗位 -boss recruiter recommend -p 2 # Next page +boss recruiter recommend --job # 推荐牛人 (paginated, 15/page) +boss recruiter recommend --job -p 2 # Next page (use hasMore flag) # ─── Greet & Communicate (沟通) ────────────────── boss recruiter greet # Initiate chat with candidate @@ -371,8 +370,8 @@ boss -v search "Python" # 详细日志 ```bash # 搜索 & 推荐 boss recruiter search "golang" --city 深圳 --exp 3-5年 -boss recruiter recommend --job # 按岗位查看推荐牛人 -boss recruiter recommend -p 2 # 翻页 +boss recruiter recommend --job # 推荐牛人 (15/页, 真分页) +boss recruiter recommend --job -p 2 # 翻页 # 沟通 boss recruiter greet # 向候选人打招呼 diff --git a/boss_cli/client.py b/boss_cli/client.py index ee801cb..3b81ff9 100644 --- a/boss_cli/client.py +++ b/boss_cli/client.py @@ -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, @@ -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, @@ -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 = "", diff --git a/boss_cli/commands/recruiter.py b/boss_cli/commands/recruiter.py index 09ca7dc..6de216d 100644 --- a/boss_cli/commands/recruiter.py +++ b/boss_cli/commands/recruiter.py @@ -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 [/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) diff --git a/boss_cli/constants.py b/boss_cli/constants.py index 4100d5c..efc1e57 100644 --- a/boss_cli/constants.py +++ b/boss_cli/constants.py @@ -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"