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.
- boss_agent_cli/__init__.py +1 -0
- boss_agent_cli/api/__init__.py +0 -0
- boss_agent_cli/api/boss.yaml +209 -0
- boss_agent_cli/api/browser_client.py +325 -0
- boss_agent_cli/api/client.py +301 -0
- boss_agent_cli/api/endpoints.py +60 -0
- boss_agent_cli/api/endpoints_loader.py +76 -0
- boss_agent_cli/api/models.py +128 -0
- boss_agent_cli/api/throttle.py +53 -0
- boss_agent_cli/auth/__init__.py +0 -0
- boss_agent_cli/auth/browser.py +200 -0
- boss_agent_cli/auth/cookie_extract.py +56 -0
- boss_agent_cli/auth/manager.py +139 -0
- boss_agent_cli/auth/token_store.py +131 -0
- boss_agent_cli/bridge/__init__.py +0 -0
- boss_agent_cli/bridge/client.py +173 -0
- boss_agent_cli/bridge/daemon.py +269 -0
- boss_agent_cli/bridge/protocol.py +70 -0
- boss_agent_cli/cache/__init__.py +0 -0
- boss_agent_cli/cache/store.py +288 -0
- boss_agent_cli/chat_summary.py +43 -0
- boss_agent_cli/commands/__init__.py +0 -0
- boss_agent_cli/commands/apply.py +60 -0
- boss_agent_cli/commands/chat.py +210 -0
- boss_agent_cli/commands/chat_export.py +337 -0
- boss_agent_cli/commands/chat_snapshot.py +102 -0
- boss_agent_cli/commands/chat_summary.py +57 -0
- boss_agent_cli/commands/chat_utils.py +26 -0
- boss_agent_cli/commands/chatmsg.py +96 -0
- boss_agent_cli/commands/cities.py +23 -0
- boss_agent_cli/commands/detail.py +115 -0
- boss_agent_cli/commands/digest.py +51 -0
- boss_agent_cli/commands/doctor.py +259 -0
- boss_agent_cli/commands/exchange.py +59 -0
- boss_agent_cli/commands/export.py +193 -0
- boss_agent_cli/commands/greet.py +215 -0
- boss_agent_cli/commands/history.py +49 -0
- boss_agent_cli/commands/interviews.py +61 -0
- boss_agent_cli/commands/login.py +57 -0
- boss_agent_cli/commands/logout.py +28 -0
- boss_agent_cli/commands/mark.py +84 -0
- boss_agent_cli/commands/me.py +77 -0
- boss_agent_cli/commands/pipeline.py +78 -0
- boss_agent_cli/commands/preset.py +82 -0
- boss_agent_cli/commands/recommend.py +60 -0
- boss_agent_cli/commands/schema.py +487 -0
- boss_agent_cli/commands/search.py +179 -0
- boss_agent_cli/commands/shortlist.py +87 -0
- boss_agent_cli/commands/show.py +91 -0
- boss_agent_cli/commands/status.py +37 -0
- boss_agent_cli/commands/watch.py +151 -0
- boss_agent_cli/config.py +23 -0
- boss_agent_cli/digest.py +10 -0
- boss_agent_cli/display.py +325 -0
- boss_agent_cli/hooks.py +61 -0
- boss_agent_cli/index_cache.py +88 -0
- boss_agent_cli/main.py +68 -0
- boss_agent_cli/match_score.py +106 -0
- boss_agent_cli/output.py +85 -0
- boss_agent_cli/pipeline_state.py +71 -0
- boss_agent_cli/search_filters.py +340 -0
- boss_agent_cli-1.3.0.dist-info/METADATA +412 -0
- boss_agent_cli-1.3.0.dist-info/RECORD +66 -0
- boss_agent_cli-1.3.0.dist-info/WHEEL +4 -0
- boss_agent_cli-1.3.0.dist-info/entry_points.txt +2 -0
- 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()
|