本文档沉淀 Issue #129 Week 1 的全量经验。未来接入新平台(Lagou / Liepin / 其他)或做类似重构时的可复用 playbook。
底层逻辑:原始代码 30+ 个命令都直接 with BossClient(...) as client:。这条隐式依赖让后续接入第二个招聘平台的成本失控——每增加一家平台需要把 30+ 命令的请求构造重新来一遍。
抽象的颗粒度选择:把"平台"作为第一公民抽象(不是"API endpoint"或"认证流程"),让平台内部的协议差异(响应包络、错误码、加密等)可以完全封闭在平台实现里。
name: str # "zhipin" / "zhilian" / ...
display_name: str # "BOSS 直聘" / "智联招聘" / ...
base_url: strdef is_success(self, response: dict) -> bool
def unwrap_data(self, response: dict) -> Any
def parse_error(self, response: dict) -> tuple[str, str] # (统一错误码, 原始消息)不同平台的差异举例:
- BOSS 直聘:
code == 0表示成功,数据在zpDatakey - 智联招聘:
code == 200表示成功,数据在datakey - 错误码映射到统一枚举:
AUTH_EXPIRED / RATE_LIMITED / ACCOUNT_RISK / TOKEN_REFRESH_FAILED / UNKNOWN
@abstractmethod
def search_jobs(self, query: str, **filters: Any) -> dict
@abstractmethod
def job_detail(self, job_id: str) -> dict
@abstractmethod
def recommend_jobs(self, page: int = 1) -> dict
@abstractmethod
def user_info(self) -> dict# P0+:简历 / 投递 / 历史 / 面试 / 聊天(基类默认 NotImplementedError)
def resume_baseinfo(self) -> dict
def resume_expect(self) -> dict
def deliver_list(self, page: int = 1) -> dict
def job_card(self, security_id: str, lid: str = "") -> dict
def interview_data(self) -> dict
def job_history(self, page: int = 1) -> dict
def chat_history(self, gid: str, security_id: str, page: int = 1, count: int = 20) -> dict
def friend_label(self, friend_id: str, label_id: int, friend_source: int = 0, remove: bool = False) -> dict
def exchange_contact(self, security_id: str, uid: str, friend_name: str, exchange_type: int = 1) -> dict
# P1:写操作
def greet(self, security_id: str, job_id: str, message: str = "") -> dict
def apply(self, security_id: str, job_id: str, lid: str = "") -> dict
# P2:沟通
def friend_list(self, page: int = 1) -> dictdef close(self) -> None # 委托给底层 client.close()
def __enter__(self) -> "Platform": ... # with 上下文管理器支持
def __exit__(self, ...) -> None: ...底层逻辑:命令层从"直用 BossClient"升级到"经 Platform 抽象调用",让命令代码对平台无感。
from boss_agent_cli.api.client import BossClient
def cmd(ctx):
data_dir = ctx.obj["data_dir"]
logger = ctx.obj["logger"]
delay = ctx.obj["delay"]
cdp_url = ctx.obj.get("cdp_url")
auth = AuthManager(data_dir, logger=logger)
with BossClient(auth, delay=delay, cdp_url=cdp_url) as client:
result = client.search_jobs("Python", city="广州")from boss_agent_cli.commands._platform import get_platform_instance
def cmd(ctx):
data_dir = ctx.obj["data_dir"]
logger = ctx.obj["logger"]
auth = AuthManager(data_dir, logger=logger)
with get_platform_instance(ctx, auth) as platform:
result = platform.search_jobs("Python", city="广州")净收益:减少 3 行样板代码(delay / cdp_url / BossClient 构造),调用面不变。
@patch("boss_agent_cli.commands.X.BossClient")
def test_something(mock_client_cls):
mock_client = _ctx_mock(mock_client_cls)
mock_client.search_jobs.return_value = {...}@patch("boss_agent_cli.commands.X.get_platform_instance")
def test_something(mock_get_platform):
mock_platform = _ctx_mock(mock_get_platform)
mock_platform.search_jobs.return_value = {...}底层逻辑:patch 的是命令模块里的 import 别名,不是源模块。所以只需跟随命令文件的 import 方式变化。
for cmd in greet apply detail me recommend ...; do
sed -i '' "s/boss_agent_cli.commands.$cmd.BossClient/boss_agent_cli.commands.$cmd.get_platform_instance/g" tests/*.py
done先按 多平台适配器研究模板 完成平台准入研究。 研究文档必须覆盖平台范围、认证方式、只读能力、受限能力、禁止能力、 端点/字段证据、风险评级、测试样本和验收命令。已有历史调研可保留 7 项清单(样板见 lagou.md),但必须补齐 “统一适配器评估”章节。
第三方 scraper、stealth、response interception、自动滚动抓取和批量触达
示例只能作为风险观察材料,不能直接复制到 Platform 主线实现。若研究
结论不能证明 P0 只读能力、字段映射、脱敏样本和低风险边界都清晰,应停在
风险占位,不进入 stub。
# src/boss_agent_cli/platforms/<name>.py
class MyPlatform(Platform):
name = "myname"
display_name = "平台中文名"
base_url = "https://..."
def is_success(self, r): ... # 按研究报告的响应结构
def unwrap_data(self, r): ...
def parse_error(self, r): ...
# P0/P1/P2 方法全部抛 NotImplementedError("Week 2 待实现")_REGISTRY: dict[str, type[Platform]] = {
"zhipin": BossPlatform,
"zhilian": ZhilianPlatform,
"myname": MyPlatform, # 新增
}- Platform 注册表验证(4 条)
- 基础元信息(3 条)
- 包络适配(10+ 条)
- Stub 行为(每个抽象方法抛 NotImplementedError)
- CLI 集成(
boss --platform <name> schema通)
- 先 P0(search / detail / recommend / user_info)
- 再 P1(greet / apply,如需要)
- 最后 P2(友好度可选)
以下是 Platform 抽象的 breaking change 红线:
- 基类
__init__(client: Any)签名不变 — 子类可窄化但不能加必填参数 - 抽象方法签名不变 — 只能添加新方法,不能改现有方法签名
- 错误码映射枚举 — 对齐 CLAUDE.md 错误码枚举的范围
with上下文语义 —__exit__必须调用close()- Python 嵌入 API 导出 —
Platform/BossPlatform/ZhilianPlatform/get_platform/list_platforms通过from boss_agent_cli import ...始终可达
- Issue #129 — Week 1 设计 + 实施
- Zhilian 候选者侧真实现已并入主线(PR #157 / #158 及后续修复);招聘者侧自动化通过
agent的 browser/CDP adapter V1 接入,带 selector health 与安全熔断 - Issue #90 — 多平台 API 调研
- PR #131 / #132 / #133 / #134 / #135 / #136 / #137 / #138 / #139 / #141 — Week 1 全部 PR