boss-agent-cli 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. boss_agent_cli/__init__.py +1 -0
  2. boss_agent_cli/api/__init__.py +0 -0
  3. boss_agent_cli/api/boss.yaml +209 -0
  4. boss_agent_cli/api/browser_client.py +325 -0
  5. boss_agent_cli/api/client.py +301 -0
  6. boss_agent_cli/api/endpoints.py +60 -0
  7. boss_agent_cli/api/endpoints_loader.py +76 -0
  8. boss_agent_cli/api/models.py +128 -0
  9. boss_agent_cli/api/throttle.py +53 -0
  10. boss_agent_cli/auth/__init__.py +0 -0
  11. boss_agent_cli/auth/browser.py +200 -0
  12. boss_agent_cli/auth/cookie_extract.py +56 -0
  13. boss_agent_cli/auth/manager.py +139 -0
  14. boss_agent_cli/auth/token_store.py +131 -0
  15. boss_agent_cli/bridge/__init__.py +0 -0
  16. boss_agent_cli/bridge/client.py +173 -0
  17. boss_agent_cli/bridge/daemon.py +269 -0
  18. boss_agent_cli/bridge/protocol.py +70 -0
  19. boss_agent_cli/cache/__init__.py +0 -0
  20. boss_agent_cli/cache/store.py +288 -0
  21. boss_agent_cli/chat_summary.py +43 -0
  22. boss_agent_cli/commands/__init__.py +0 -0
  23. boss_agent_cli/commands/apply.py +60 -0
  24. boss_agent_cli/commands/chat.py +210 -0
  25. boss_agent_cli/commands/chat_export.py +337 -0
  26. boss_agent_cli/commands/chat_snapshot.py +102 -0
  27. boss_agent_cli/commands/chat_summary.py +57 -0
  28. boss_agent_cli/commands/chat_utils.py +26 -0
  29. boss_agent_cli/commands/chatmsg.py +96 -0
  30. boss_agent_cli/commands/cities.py +23 -0
  31. boss_agent_cli/commands/detail.py +115 -0
  32. boss_agent_cli/commands/digest.py +51 -0
  33. boss_agent_cli/commands/doctor.py +259 -0
  34. boss_agent_cli/commands/exchange.py +59 -0
  35. boss_agent_cli/commands/export.py +193 -0
  36. boss_agent_cli/commands/greet.py +215 -0
  37. boss_agent_cli/commands/history.py +49 -0
  38. boss_agent_cli/commands/interviews.py +61 -0
  39. boss_agent_cli/commands/login.py +57 -0
  40. boss_agent_cli/commands/logout.py +28 -0
  41. boss_agent_cli/commands/mark.py +84 -0
  42. boss_agent_cli/commands/me.py +77 -0
  43. boss_agent_cli/commands/pipeline.py +78 -0
  44. boss_agent_cli/commands/preset.py +82 -0
  45. boss_agent_cli/commands/recommend.py +60 -0
  46. boss_agent_cli/commands/schema.py +487 -0
  47. boss_agent_cli/commands/search.py +179 -0
  48. boss_agent_cli/commands/shortlist.py +87 -0
  49. boss_agent_cli/commands/show.py +91 -0
  50. boss_agent_cli/commands/status.py +37 -0
  51. boss_agent_cli/commands/watch.py +151 -0
  52. boss_agent_cli/config.py +23 -0
  53. boss_agent_cli/digest.py +10 -0
  54. boss_agent_cli/display.py +325 -0
  55. boss_agent_cli/hooks.py +61 -0
  56. boss_agent_cli/index_cache.py +88 -0
  57. boss_agent_cli/main.py +68 -0
  58. boss_agent_cli/match_score.py +106 -0
  59. boss_agent_cli/output.py +85 -0
  60. boss_agent_cli/pipeline_state.py +71 -0
  61. boss_agent_cli/search_filters.py +340 -0
  62. boss_agent_cli-1.3.0.dist-info/METADATA +412 -0
  63. boss_agent_cli-1.3.0.dist-info/RECORD +66 -0
  64. boss_agent_cli-1.3.0.dist-info/WHEEL +4 -0
  65. boss_agent_cli-1.3.0.dist-info/entry_points.txt +2 -0
  66. boss_agent_cli-1.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+ __version__ = "1.3.0"
File without changes
@@ -0,0 +1,209 @@
1
+ base_url: "https://www.zhipin.com"
2
+
3
+ web_pages:
4
+ geek_base: "/web/geek"
5
+ geek_job: "/web/geek/job"
6
+ geek_recommend: "/web/geek/recommend"
7
+ geek_chat: "/web/geek/chat"
8
+ geek_resume: "/web/geek/resume"
9
+
10
+ default_headers:
11
+ User-Agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
12
+ sec-ch-ua: '"Chromium";v="145", "Not(A:Brand";v="99", "Google Chrome";v="145"'
13
+ sec-ch-ua-mobile: "?0"
14
+ sec-ch-ua-platform: '"macOS"'
15
+ Sec-Fetch-Dest: "empty"
16
+ Sec-Fetch-Mode: "cors"
17
+ Sec-Fetch-Site: "same-origin"
18
+ Accept: "application/json, text/plain, */*"
19
+ Accept-Language: "zh-CN,zh;q=0.9,en;q=0.8"
20
+ DNT: "1"
21
+
22
+ response_codes:
23
+ success: 0
24
+ stoken_expired: 37
25
+ rate_limited: 9
26
+ account_risk: 36
27
+
28
+ endpoints:
29
+ search:
30
+ method: GET
31
+ path: "/wapi/zpgeek/search/joblist.json"
32
+ referer: "/web/geek/job"
33
+ recommend:
34
+ method: GET
35
+ path: "/wapi/zpgeek/pc/recommend/job/list.json"
36
+ referer: "/web/geek/recommend"
37
+ detail:
38
+ method: GET
39
+ path: "/wapi/zpgeek/job/detail.json"
40
+ referer: "/web/geek/job"
41
+ greet:
42
+ method: POST
43
+ path: "/wapi/zpgeek/friend/add.json"
44
+ referer: "/web/geek/chat"
45
+ job_card:
46
+ method: GET
47
+ path: "/wapi/zpgeek/job/card.json"
48
+ referer: "/web/geek/job"
49
+ user_info:
50
+ method: GET
51
+ path: "/wapi/zpuser/wap/getUserInfo.json"
52
+ referer: "/"
53
+ resume_baseinfo:
54
+ method: GET
55
+ path: "/wapi/zpgeek/resume/baseinfo/query.json"
56
+ referer: "/web/geek/resume"
57
+ resume_expect:
58
+ method: GET
59
+ path: "/wapi/zpgeek/resume/expect/query.json"
60
+ referer: "/web/geek/resume"
61
+ deliver_list:
62
+ method: GET
63
+ path: "/wapi/zprelation/resume/geekDeliverList"
64
+ referer: "/web/geek/chat"
65
+ friend_list:
66
+ method: GET
67
+ path: "/wapi/zprelation/friend/getGeekFriendList.json"
68
+ referer: "/web/geek/chat"
69
+ interview_data:
70
+ method: GET
71
+ path: "/wapi/zpinterview/geek/interview/data.json"
72
+ referer: "/web/geek/job"
73
+ job_history:
74
+ method: GET
75
+ path: "/wapi/zpgeek/history/joblist.json"
76
+ referer: "/web/geek/job"
77
+ chat_history:
78
+ method: GET
79
+ path: "/wapi/zpchat/geek/historyMsg"
80
+ referer: "/web/geek/chat"
81
+ friend_label_add:
82
+ method: GET
83
+ path: "/wapi/zprelation/friend/label/addMark"
84
+ referer: "/web/geek/chat"
85
+ friend_label_delete:
86
+ method: GET
87
+ path: "/wapi/zprelation/friend/label/deleteMark"
88
+ referer: "/web/geek/chat"
89
+ exchange_request:
90
+ method: POST
91
+ path: "/wapi/zpchat/exchange/request"
92
+ referer: "/web/geek/chat"
93
+
94
+ lookups:
95
+ city:
96
+ 北京: "101010100"
97
+ 上海: "101020100"
98
+ 广州: "101280100"
99
+ 深圳: "101280600"
100
+ 杭州: "101210100"
101
+ 成都: "101270100"
102
+ 南京: "101190100"
103
+ 武汉: "101200100"
104
+ 西安: "101110100"
105
+ 苏州: "101190400"
106
+ 长沙: "101250100"
107
+ 郑州: "101180100"
108
+ 重庆: "101040100"
109
+ 天津: "101030100"
110
+ 合肥: "101220100"
111
+ 厦门: "101230200"
112
+ 济南: "101120100"
113
+ 青岛: "101120200"
114
+ 大连: "101070200"
115
+ 宁波: "101210400"
116
+ 福州: "101230100"
117
+ 东莞: "101281600"
118
+ 珠海: "101280700"
119
+ 佛山: "101280800"
120
+ 昆明: "101290100"
121
+ 贵阳: "101260100"
122
+ 太原: "101100100"
123
+ 南昌: "101240100"
124
+ 南宁: "101300100"
125
+ 石家庄: "101090100"
126
+ 哈尔滨: "101050100"
127
+ 长春: "101060100"
128
+ 沈阳: "101070100"
129
+ 海口: "101310100"
130
+ 兰州: "101160100"
131
+ 乌鲁木齐: "101130100"
132
+ 无锡: "101190200"
133
+ 常州: "101191100"
134
+ 温州: "101210700"
135
+ 惠州: "101280300"
136
+
137
+ salary:
138
+ 3K以下: "401"
139
+ 3-5K: "402"
140
+ 5-10K: "403"
141
+ 10-15K: "404"
142
+ 10-20K: "405"
143
+ 20-50K: "406"
144
+ 50K以上: "407"
145
+
146
+ experience:
147
+ 应届: "108"
148
+ 1年以内: "101"
149
+ 1-3年: "103"
150
+ 3-5年: "104"
151
+ 5-10年: "105"
152
+ 10年以上: "106"
153
+
154
+ education:
155
+ 大专: "202"
156
+ 本科: "203"
157
+ 硕士: "204"
158
+ 博士: "205"
159
+
160
+ scale:
161
+ 0-20人: "301"
162
+ 20-99人: "302"
163
+ 100-499人: "303"
164
+ 500-999人: "304"
165
+ 1000-9999人: "305"
166
+ 10000人以上: "306"
167
+
168
+ industry:
169
+ 不限: "0"
170
+ 互联网: "100020"
171
+ 电子商务: "100021"
172
+ 游戏: "100024"
173
+ 软件/信息服务: "100032"
174
+ 人工智能: "100901"
175
+ 大数据: "100902"
176
+ 云计算: "100903"
177
+ 区块链: "100904"
178
+ 物联网: "100905"
179
+ 金融: "100101"
180
+ 银行: "100102"
181
+ 保险: "100103"
182
+ 证券/基金: "100104"
183
+ 教育培训: "100200"
184
+ 医疗健康: "100300"
185
+ 房地产: "100400"
186
+ 汽车: "100500"
187
+ 物流/运输: "100600"
188
+ 广告/传媒: "100700"
189
+ 消费品: "100800"
190
+ 制造业: "101000"
191
+ 能源/环保: "101100"
192
+ 政府/非营利: "101200"
193
+ 农业: "101300"
194
+
195
+ stage:
196
+ 不限: "0"
197
+ 未融资: "801"
198
+ 天使轮: "802"
199
+ A轮: "803"
200
+ B轮: "804"
201
+ C轮: "805"
202
+ D轮及以上: "806"
203
+ 已上市: "807"
204
+ 不需要融资: "808"
205
+
206
+ job_type:
207
+ 全职: "1901"
208
+ 兼职: "1903"
209
+ 实习: "1903"
@@ -0,0 +1,325 @@
1
+ """Browser-based API client — uses patchright to make requests from a real browser context.
2
+
3
+ stoken is generated by the browser's JS environment automatically, eliminating expiration issues.
4
+ Used for high-risk operations: search, recommend, greet, batch-greet.
5
+
6
+ Supports two modes:
7
+ 1. CDP mode (preferred): connects to user's existing Chrome via DevTools Protocol
8
+ 2. Patchright mode (fallback): launches a headless Chromium instance
9
+ """
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from patchright.sync_api import sync_playwright
14
+
15
+ from boss_agent_cli.api import endpoints
16
+ from boss_agent_cli.api.throttle import RequestThrottle
17
+
18
+ HOME_URL = "https://www.zhipin.com/"
19
+ CDP_DEFAULT_URL = "http://localhost:9222"
20
+
21
+ # 超时常量
22
+ _CDP_PROBE_TIMEOUT = 3 # CDP 探测 HTTP 超时(秒)
23
+ _NAV_TIMEOUT_MS = 15000 # 页面导航超时(毫秒)
24
+
25
+ # macOS / Linux / Windows Chrome user data 默认路径
26
+ _CHROME_USER_DATA_CANDIDATES = [
27
+ Path.home() / "Library/Application Support/Google/Chrome", # macOS
28
+ Path.home() / ".config/google-chrome", # Linux
29
+ Path.home() / "AppData/Local/Google/Chrome/User Data", # Windows
30
+ ]
31
+
32
+
33
+ class BrowserSession:
34
+ """Persistent browser session that makes API calls via page.evaluate(fetch).
35
+
36
+ Tries CDP connection to user's Chrome first; falls back to headless patchright.
37
+ """
38
+
39
+ def __init__(self, cookies: dict, user_agent: str, *, delay: tuple[float, float] = (1.5, 3.0), cdp_url: str | None = None, logger=None):
40
+ self._throttle = RequestThrottle(delay)
41
+ self._pw = None
42
+ self._browser = None
43
+ self._context = None
44
+ self._page = None
45
+ self._cookies = cookies
46
+ self._user_agent = user_agent
47
+ self._started = False
48
+ self._cdp_url = cdp_url
49
+ self._is_cdp = False
50
+ self._own_context = False # 是否由我们创建的 context(需要在 close 时清理)
51
+ self._logger = logger
52
+ self._bridge_client = None
53
+ self._is_bridge = False
54
+
55
+ def _log(self, message: str) -> None:
56
+ """通过注入的 logger 输出,受 --log-level 控制。"""
57
+ if self._logger:
58
+ self._logger.info(message)
59
+ else:
60
+ print(message, file=sys.stderr)
61
+
62
+ def _ensure_started(self):
63
+ if self._started:
64
+ return
65
+
66
+ # 优先尝试 Bridge 模式(Chrome 扩展 + daemon,零配置)
67
+ if self._try_bridge():
68
+ return
69
+
70
+ self._pw = sync_playwright().start()
71
+
72
+ # 第二优先:CDP 连接用户 Chrome(真实指纹 + 天然登录态)
73
+ if self._try_cdp():
74
+ return
75
+
76
+ # 兜底:启动 headless patchright
77
+ self._start_headless()
78
+
79
+ def _try_bridge(self) -> bool:
80
+ """尝试通过 Browser Bridge(Chrome 扩展 + daemon)连接。"""
81
+ try:
82
+ from boss_agent_cli.bridge.client import BridgeClient
83
+ client = BridgeClient()
84
+ if not client.is_running():
85
+ return False
86
+ if not client.is_extension_connected():
87
+ self._log("[boss] Bridge daemon 运行中但扩展未连接")
88
+ return False
89
+ self._bridge_client = client
90
+ self._started = True
91
+ self._is_cdp = False
92
+ self._is_bridge = True
93
+ self._log("[boss] Bridge 模式连接成功(Chrome 扩展 + daemon)")
94
+ return True
95
+ except Exception:
96
+ return False
97
+
98
+ def _try_cdp(self) -> bool:
99
+ """Try connecting to user's Chrome via CDP.
100
+
101
+ Attempts in order:
102
+ 1. Explicit cdp_url if provided
103
+ 2. Default http://localhost:9222 (+ auto WS fallback)
104
+ 3. WebSocket URL from Chrome's DevToolsActivePort file
105
+ """
106
+ urls_to_try = []
107
+ if self._cdp_url:
108
+ urls_to_try.append(self._cdp_url)
109
+ urls_to_try.append(CDP_DEFAULT_URL)
110
+
111
+ # 从 DevToolsActivePort 文件读取 WebSocket URL
112
+ ws_url = self._read_devtools_active_port()
113
+ if ws_url:
114
+ urls_to_try.append(ws_url)
115
+
116
+ for url in urls_to_try:
117
+ if self._try_connect(url):
118
+ return True
119
+ # HTTP URL 连接失败时,尝试从 /json/version 获取 WS URL
120
+ if url.startswith("http"):
121
+ ws = self._fetch_ws_url(url)
122
+ if ws and self._try_connect(ws):
123
+ return True
124
+ return False
125
+
126
+ def _try_connect(self, url: str) -> bool:
127
+ """Attempt a single CDP connection, reusing user's existing context.
128
+
129
+ Reuses the first existing browser context (preserves login state and
130
+ avoids Chrome's automation detection on new contexts). Only creates
131
+ a new context when none exists.
132
+ """
133
+ try:
134
+ self._browser = self._pw.chromium.connect_over_cdp(url)
135
+ contexts = self._browser.contexts
136
+
137
+ if contexts:
138
+ # 复用用户现有 context(保留登录态 + 规避 automation 检测)
139
+ self._context = contexts[0]
140
+ self._own_context = False # 非我们创建,close 时不关闭
141
+ else:
142
+ # 没有已存在 context,创建新的并注入 cookies
143
+ self._context = self._browser.new_context()
144
+ if self._cookies:
145
+ self._context.add_cookies([
146
+ {"name": name, "value": value, "domain": ".zhipin.com", "path": "/"}
147
+ for name, value in self._cookies.items()
148
+ ])
149
+ self._own_context = True
150
+
151
+ self._page = self._context.new_page()
152
+ # CDP 模式下用较长超时 + commit 级等待(避免 networkidle 卡住)
153
+ try:
154
+ self._page.goto(HOME_URL, wait_until="commit", timeout=_NAV_TIMEOUT_MS)
155
+ except Exception:
156
+ pass # 即使导航超时,页面 JS 环境已可用
157
+ self._started = True
158
+ self._is_cdp = True
159
+ reuse_label = "复用用户 context" if not self._own_context else "新建 context"
160
+ self._log(f"[boss] CDP 连接成功 ({url}),{reuse_label}")
161
+ return True
162
+ except Exception:
163
+ if self._browser:
164
+ try:
165
+ self._browser.close()
166
+ except Exception:
167
+ pass
168
+ self._browser = None
169
+ return False
170
+
171
+ @staticmethod
172
+ def _fetch_ws_url(http_url: str) -> str | None:
173
+ """Fetch WebSocket debugger URL from Chrome's /json/version endpoint."""
174
+ import httpx
175
+ try:
176
+ resp = httpx.get(f"{http_url}/json/version", timeout=_CDP_PROBE_TIMEOUT)
177
+ return resp.json().get("webSocketDebuggerUrl")
178
+ except Exception:
179
+ return None
180
+
181
+ @staticmethod
182
+ def _read_devtools_active_port() -> str | None:
183
+ """Read Chrome's DevToolsActivePort file to get WS endpoint."""
184
+ for user_data_dir in _CHROME_USER_DATA_CANDIDATES:
185
+ port_file = user_data_dir / "DevToolsActivePort"
186
+ if port_file.exists():
187
+ try:
188
+ lines = port_file.read_text().strip().splitlines()
189
+ if len(lines) >= 2:
190
+ port = lines[0].strip()
191
+ ws_path = lines[1].strip()
192
+ return f"ws://127.0.0.1:{port}{ws_path}"
193
+ except Exception:
194
+ continue
195
+ return None
196
+
197
+ def _start_headless(self):
198
+ """Fallback: launch headless patchright Chromium."""
199
+ self._browser = self._pw.chromium.launch(headless=True)
200
+ self._context = self._browser.new_context(
201
+ user_agent=self._user_agent,
202
+ viewport={"width": 1280, "height": 800},
203
+ locale="zh-CN",
204
+ timezone_id="Asia/Shanghai",
205
+ )
206
+ self._own_context = True
207
+ self._context.add_cookies([
208
+ {"name": name, "value": value, "domain": ".zhipin.com", "path": "/"}
209
+ for name, value in self._cookies.items()
210
+ ])
211
+ self._page = self._context.new_page()
212
+ self._page.goto(HOME_URL, wait_until="domcontentloaded")
213
+ self._page.wait_for_load_state("networkidle")
214
+ self._started = True
215
+ self._is_cdp = False
216
+ self._log("[boss] CDP 不可用(提示:需以 --remote-debugging-port=9222 启动 Chrome),降级到 headless patchright")
217
+
218
+ # ── Core request via browser fetch() ─────────────────────────────
219
+
220
+ def request(self, method: str, url: str, *, params: dict | None = None, data: dict | None = None) -> dict:
221
+ self._ensure_started()
222
+ self._throttle.wait()
223
+
224
+ referer = endpoints.REFERER_MAP.get(url, f"{endpoints.BASE_URL}/")
225
+
226
+ # Bridge 模式:通过扩展 fetch
227
+ if self._is_bridge and self._bridge_client:
228
+ import urllib.parse
229
+ full_url = url
230
+ if params:
231
+ full_url = f"{url}?{urllib.parse.urlencode(params)}"
232
+ result = self._bridge_client.fetch_json(
233
+ full_url,
234
+ method=method,
235
+ data=data,
236
+ referer=referer,
237
+ )
238
+ self._throttle.mark()
239
+ return result
240
+
241
+ # Playwright 模式(CDP 或 headless)
242
+ result = self._page.evaluate("""
243
+ async ({method, url, params, data, referer}) => {
244
+ try {
245
+ let fetchUrl = url;
246
+ if (params && Object.keys(params).length > 0) {
247
+ const sp = new URLSearchParams();
248
+ for (const [k, v] of Object.entries(params)) {
249
+ if (v !== null && v !== undefined) sp.append(k, String(v));
250
+ }
251
+ fetchUrl = url + '?' + sp.toString();
252
+ }
253
+
254
+ const options = {
255
+ method: method,
256
+ credentials: 'include',
257
+ headers: {
258
+ 'Accept': 'application/json, text/plain, */*',
259
+ 'Referer': referer,
260
+ 'X-Requested-With': 'XMLHttpRequest',
261
+ },
262
+ };
263
+
264
+ if (method === 'POST' && data) {
265
+ options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
266
+ const formData = new URLSearchParams();
267
+ for (const [k, v] of Object.entries(data)) {
268
+ formData.append(k, String(v));
269
+ }
270
+ options.body = formData.toString();
271
+ }
272
+
273
+ const resp = await fetch(fetchUrl, options);
274
+ return await resp.json();
275
+ } catch (e) {
276
+ return {code: -1, message: e.message, zpData: {}};
277
+ }
278
+ }
279
+ """, {"method": method, "url": url, "params": params or {}, "data": data, "referer": referer})
280
+
281
+ self._throttle.mark()
282
+ return result
283
+
284
+ # ── Lifecycle ────────────────────────────────────────────────────
285
+
286
+ @property
287
+ def is_cdp(self) -> bool:
288
+ """Whether this session is using CDP connection to user's Chrome."""
289
+ return self._is_cdp
290
+
291
+ def close(self):
292
+ if self._is_bridge and self._bridge_client:
293
+ try:
294
+ self._bridge_client.close_window()
295
+ except Exception:
296
+ pass
297
+ self._bridge_client = None
298
+ self._started = False
299
+ return
300
+
301
+ if self._is_cdp:
302
+ # CDP 模式:关闭我们创建的 page 和 context,不关闭用户浏览器
303
+ if self._page:
304
+ try:
305
+ self._page.close()
306
+ except Exception:
307
+ pass
308
+ if self._own_context and self._context:
309
+ try:
310
+ self._context.close()
311
+ except Exception:
312
+ pass
313
+ else:
314
+ # Headless 模式:关闭整个浏览器实例
315
+ if self._browser:
316
+ self._browser.close()
317
+ if self._pw:
318
+ self._pw.stop()
319
+ self._started = False
320
+
321
+ def __enter__(self):
322
+ return self
323
+
324
+ def __exit__(self, *args):
325
+ self.close()