tzzb 0.2.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.
- tzzb/__init__.py +18 -0
- tzzb/auth.py +474 -0
- tzzb/cli.py +1107 -0
- tzzb/client.py +632 -0
- tzzb/config.py +90 -0
- tzzb/mcp.py +210 -0
- tzzb/models.py +86 -0
- tzzb-0.2.0.dist-info/METADATA +167 -0
- tzzb-0.2.0.dist-info/RECORD +12 -0
- tzzb-0.2.0.dist-info/WHEEL +5 -0
- tzzb-0.2.0.dist-info/entry_points.txt +3 -0
- tzzb-0.2.0.dist-info/top_level.txt +1 -0
tzzb/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""tzzb — Python SDK, CLI, and MCP Server for Tonghuashun Investment Ledger (同花顺投资账本)."""
|
|
2
|
+
|
|
3
|
+
from .client import TzzbClient, TzzbError
|
|
4
|
+
from .config import TzzbConfig, config
|
|
5
|
+
from .models import Account, AssetTrendPoint, LoginStatus, Position, TradeRecord
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"TzzbClient",
|
|
9
|
+
"TzzbError",
|
|
10
|
+
"TzzbConfig",
|
|
11
|
+
"config",
|
|
12
|
+
"Account",
|
|
13
|
+
"AssetTrendPoint",
|
|
14
|
+
"LoginStatus",
|
|
15
|
+
"Position",
|
|
16
|
+
"TradeRecord",
|
|
17
|
+
]
|
|
18
|
+
__version__ = "0.1.0"
|
tzzb/auth.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""Authentication and Chrome DevTools Protocol (CDP) bridge for tzzb.
|
|
2
|
+
|
|
3
|
+
Provides session management, Chrome process management, and CDP request execution
|
|
4
|
+
to bypass Tonghuashun dynamic anti-bot signatures (hexin-v).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import socket
|
|
11
|
+
import subprocess
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import urllib.request
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import websocket
|
|
19
|
+
|
|
20
|
+
from .models import LoginStatus
|
|
21
|
+
|
|
22
|
+
COOKIE_FILE = Path.home() / ".tzzb_cookies.json"
|
|
23
|
+
REQUIRED_COOKIES = {"userid", "ticket", "user"}
|
|
24
|
+
TZZB_URL = "https://tzzb.10jqka.com.cn/pc/index.html#/unlogin"
|
|
25
|
+
CDP_PORT = 9222
|
|
26
|
+
CHROME_PROFILE = Path.home() / ".tzzb_chrome_profile"
|
|
27
|
+
|
|
28
|
+
_ws: websocket.WebSocket | None = None
|
|
29
|
+
_msg_id: int = 0
|
|
30
|
+
_cdp_lock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_port(port: int = CDP_PORT) -> bool:
|
|
34
|
+
"""Check if the given port is open."""
|
|
35
|
+
try:
|
|
36
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
37
|
+
sock.settimeout(1)
|
|
38
|
+
result = sock.connect_ex(("127.0.0.1", port))
|
|
39
|
+
sock.close()
|
|
40
|
+
return result == 0
|
|
41
|
+
except Exception:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def find_chrome() -> str | None:
|
|
46
|
+
"""Find system Chrome/Chromium executable."""
|
|
47
|
+
candidates = [
|
|
48
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
49
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
50
|
+
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
51
|
+
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
52
|
+
str(Path.home() / "AppData/Local/Google/Chrome/Application/chrome.exe"),
|
|
53
|
+
"/usr/bin/google-chrome",
|
|
54
|
+
"/usr/bin/chromium",
|
|
55
|
+
"/usr/bin/chromium-browser",
|
|
56
|
+
]
|
|
57
|
+
for path in candidates:
|
|
58
|
+
if Path(path).exists():
|
|
59
|
+
return path
|
|
60
|
+
|
|
61
|
+
for cmd in [["which", "google-chrome"], ["which", "chromium"], ["which", "chrome"], ["where", "chrome"]]:
|
|
62
|
+
try:
|
|
63
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
|
64
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
65
|
+
return result.stdout.strip().split("\n")[0]
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def launch_chrome() -> bool:
|
|
72
|
+
"""Launch isolated Chrome debug instance with visible GUI window."""
|
|
73
|
+
import sys
|
|
74
|
+
chrome_path = find_chrome()
|
|
75
|
+
if not chrome_path:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
CHROME_PROFILE.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
|
|
80
|
+
if sys.platform == "darwin":
|
|
81
|
+
subprocess.Popen(
|
|
82
|
+
[
|
|
83
|
+
"open",
|
|
84
|
+
"-n",
|
|
85
|
+
"-a",
|
|
86
|
+
"Google Chrome",
|
|
87
|
+
"--args",
|
|
88
|
+
f"--remote-debugging-port={CDP_PORT}",
|
|
89
|
+
f"--user-data-dir={CHROME_PROFILE}",
|
|
90
|
+
"--remote-allow-origins=*",
|
|
91
|
+
"--no-first-run",
|
|
92
|
+
"--no-default-browser-check",
|
|
93
|
+
TZZB_URL,
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
creationflags = 0
|
|
98
|
+
if hasattr(subprocess, "DETACHED_PROCESS"):
|
|
99
|
+
creationflags |= subprocess.DETACHED_PROCESS
|
|
100
|
+
if hasattr(subprocess, "CREATE_NO_WINDOW"):
|
|
101
|
+
creationflags |= subprocess.CREATE_NO_WINDOW
|
|
102
|
+
|
|
103
|
+
subprocess.Popen(
|
|
104
|
+
[
|
|
105
|
+
chrome_path,
|
|
106
|
+
f"--remote-debugging-port={CDP_PORT}",
|
|
107
|
+
f"--user-data-dir={CHROME_PROFILE}",
|
|
108
|
+
"--remote-allow-origins=*",
|
|
109
|
+
"--no-first-run",
|
|
110
|
+
"--no-default-browser-check",
|
|
111
|
+
TZZB_URL,
|
|
112
|
+
],
|
|
113
|
+
stdout=subprocess.DEVNULL,
|
|
114
|
+
stderr=subprocess.DEVNULL,
|
|
115
|
+
creationflags=creationflags,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
for _ in range(30):
|
|
119
|
+
time.sleep(1)
|
|
120
|
+
if check_port(CDP_PORT):
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def reset_connection() -> None:
|
|
126
|
+
"""Close existing WebSocket connection and reset state."""
|
|
127
|
+
global _ws
|
|
128
|
+
if _ws:
|
|
129
|
+
try:
|
|
130
|
+
_ws.close()
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
_ws = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_page_ws_url() -> str:
|
|
137
|
+
"""Retrieve debugger WebSocket URL for an active page target."""
|
|
138
|
+
try:
|
|
139
|
+
resp = urllib.request.urlopen(f"http://127.0.0.1:{CDP_PORT}/json")
|
|
140
|
+
targets = json.loads(resp.read())
|
|
141
|
+
except Exception:
|
|
142
|
+
targets = []
|
|
143
|
+
|
|
144
|
+
for t in targets:
|
|
145
|
+
if t.get("type") == "page":
|
|
146
|
+
return t["webSocketDebuggerUrl"]
|
|
147
|
+
|
|
148
|
+
# Try creating a new target page if none exist
|
|
149
|
+
try:
|
|
150
|
+
req = urllib.request.Request(f"http://127.0.0.1:{CDP_PORT}/json/new?{TZZB_URL}", method="PUT")
|
|
151
|
+
resp = urllib.request.urlopen(req)
|
|
152
|
+
new_target = json.loads(resp.read())
|
|
153
|
+
if "webSocketDebuggerUrl" in new_target:
|
|
154
|
+
return new_target["webSocketDebuggerUrl"]
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
raise RuntimeError("No available page targets in Chrome CDP")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def wait_for_page_load(timeout: int = 15) -> None:
|
|
162
|
+
"""Wait until page state is ready for requests."""
|
|
163
|
+
global _msg_id
|
|
164
|
+
|
|
165
|
+
_msg_id += 1
|
|
166
|
+
nav_msg = {
|
|
167
|
+
"id": _msg_id,
|
|
168
|
+
"method": "Page.navigate",
|
|
169
|
+
"params": {"url": "https://tzzb.10jqka.com.cn/pc/index.html"},
|
|
170
|
+
}
|
|
171
|
+
_ws.send(json.dumps(nav_msg))
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
_ws.settimeout(5)
|
|
175
|
+
while True:
|
|
176
|
+
raw = _ws.recv()
|
|
177
|
+
parsed = json.loads(raw)
|
|
178
|
+
if parsed.get("id") == _msg_id:
|
|
179
|
+
break
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
check_expr = "(function(){return document.readyState==='complete'?1:0})()"
|
|
184
|
+
deadline = time.time() + timeout
|
|
185
|
+
while time.time() < deadline:
|
|
186
|
+
_msg_id += 1
|
|
187
|
+
eval_msg = {
|
|
188
|
+
"id": _msg_id,
|
|
189
|
+
"method": "Runtime.evaluate",
|
|
190
|
+
"params": {
|
|
191
|
+
"expression": check_expr,
|
|
192
|
+
"returnByValue": True,
|
|
193
|
+
"awaitPromise": False,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
_ws.send(json.dumps(eval_msg))
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
_ws.settimeout(3)
|
|
200
|
+
while True:
|
|
201
|
+
raw = _ws.recv()
|
|
202
|
+
parsed = json.loads(raw)
|
|
203
|
+
if parsed.get("id") == _msg_id:
|
|
204
|
+
value = parsed.get("result", {}).get("result", {}).get("value", 0)
|
|
205
|
+
if value == 1:
|
|
206
|
+
time.sleep(1)
|
|
207
|
+
return
|
|
208
|
+
break
|
|
209
|
+
except websocket.WebSocketTimeoutException:
|
|
210
|
+
pass
|
|
211
|
+
time.sleep(0.5)
|
|
212
|
+
|
|
213
|
+
time.sleep(2)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def cdp_connect() -> None:
|
|
217
|
+
"""Connect to Chrome CDP target."""
|
|
218
|
+
global _ws, _msg_id
|
|
219
|
+
|
|
220
|
+
reset_connection()
|
|
221
|
+
ws_url = get_page_ws_url()
|
|
222
|
+
_ws = websocket.create_connection(ws_url, timeout=10)
|
|
223
|
+
_msg_id = 0
|
|
224
|
+
|
|
225
|
+
cdp_send("Page.enable")
|
|
226
|
+
wait_for_page_load(timeout=15)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def cdp_send(method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
230
|
+
"""Send CDP command and wait for response."""
|
|
231
|
+
global _msg_id
|
|
232
|
+
|
|
233
|
+
_msg_id += 1
|
|
234
|
+
msg = {"id": _msg_id, "method": method, "params": params or {}}
|
|
235
|
+
_ws.send(json.dumps(msg))
|
|
236
|
+
|
|
237
|
+
while True:
|
|
238
|
+
raw = _ws.recv()
|
|
239
|
+
parsed = json.loads(raw)
|
|
240
|
+
if parsed.get("id") == _msg_id:
|
|
241
|
+
return parsed
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def ensure_connection() -> None:
|
|
245
|
+
"""Ensure CDP connection is established and healthy."""
|
|
246
|
+
global _ws
|
|
247
|
+
|
|
248
|
+
if _ws is not None:
|
|
249
|
+
try:
|
|
250
|
+
_ws.ping()
|
|
251
|
+
return
|
|
252
|
+
except Exception:
|
|
253
|
+
reset_connection()
|
|
254
|
+
|
|
255
|
+
if not check_port(CDP_PORT):
|
|
256
|
+
if not launch_chrome():
|
|
257
|
+
raise RuntimeError("Failed to launch or connect to Chrome on CDP port 9222")
|
|
258
|
+
|
|
259
|
+
cdp_connect()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def cdp_send_safe(method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
263
|
+
"""Thread-safe CDP call with automatic reconnection."""
|
|
264
|
+
with _cdp_lock:
|
|
265
|
+
ensure_connection()
|
|
266
|
+
try:
|
|
267
|
+
return cdp_send(method, params)
|
|
268
|
+
except (websocket.WebSocketException, ConnectionError, OSError):
|
|
269
|
+
reset_connection()
|
|
270
|
+
ensure_connection()
|
|
271
|
+
try:
|
|
272
|
+
return cdp_send(method, params)
|
|
273
|
+
except Exception:
|
|
274
|
+
reset_connection()
|
|
275
|
+
raise
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def cdp_evaluate(expression: str) -> dict[str, Any]:
|
|
279
|
+
"""Execute JavaScript in browser via Runtime.evaluate."""
|
|
280
|
+
return cdp_send_safe(
|
|
281
|
+
"Runtime.evaluate",
|
|
282
|
+
{
|
|
283
|
+
"expression": expression,
|
|
284
|
+
"returnByValue": True,
|
|
285
|
+
"awaitPromise": True,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def load_cookies() -> dict[str, str] | None:
|
|
291
|
+
"""Load cached cookies from disk."""
|
|
292
|
+
if not COOKIE_FILE.exists():
|
|
293
|
+
return None
|
|
294
|
+
try:
|
|
295
|
+
data = json.loads(COOKIE_FILE.read_text(encoding="utf-8"))
|
|
296
|
+
if REQUIRED_COOKIES.issubset(data.keys()):
|
|
297
|
+
return {k: str(data[k]) for k in REQUIRED_COOKIES}
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def save_cookies(cookies: dict[str, str]) -> None:
|
|
304
|
+
"""Persist cookies to disk."""
|
|
305
|
+
data = {
|
|
306
|
+
**cookies,
|
|
307
|
+
"extracted_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
308
|
+
"source": "cdp",
|
|
309
|
+
}
|
|
310
|
+
COOKIE_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def clear_cookies() -> None:
|
|
314
|
+
"""Clear cached cookies."""
|
|
315
|
+
COOKIE_FILE.unlink(missing_ok=True)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def cdp_get_cookies() -> dict[str, str]:
|
|
319
|
+
"""Extract cookies from browser via Network.getCookies."""
|
|
320
|
+
result = cdp_send_safe("Network.getCookies", {"urls": ["https://tzzb.10jqka.com.cn"]})
|
|
321
|
+
cookies = result.get("result", {}).get("cookies", [])
|
|
322
|
+
extracted: dict[str, str] = {}
|
|
323
|
+
for c in cookies:
|
|
324
|
+
if "10jqka.com.cn" in c.get("domain", "") and c["name"] in REQUIRED_COOKIES:
|
|
325
|
+
extracted[c["name"]] = c["value"]
|
|
326
|
+
return extracted if REQUIRED_COOKIES.issubset(extracted.keys()) else {}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def get_login_status() -> LoginStatus:
|
|
330
|
+
"""Get current authentication status."""
|
|
331
|
+
cookies = load_cookies()
|
|
332
|
+
if cookies:
|
|
333
|
+
try:
|
|
334
|
+
raw = json.loads(COOKIE_FILE.read_text(encoding="utf-8"))
|
|
335
|
+
extracted_at = raw.get("extracted_at", "")
|
|
336
|
+
except Exception:
|
|
337
|
+
extracted_at = ""
|
|
338
|
+
return LoginStatus(
|
|
339
|
+
logged_in=True,
|
|
340
|
+
userid=cookies.get("userid", ""),
|
|
341
|
+
cookie_count=len(cookies),
|
|
342
|
+
extracted_at=extracted_at,
|
|
343
|
+
message="Authenticated",
|
|
344
|
+
)
|
|
345
|
+
return LoginStatus(logged_in=False, message="Not authenticated")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def logout() -> dict[str, Any]:
|
|
349
|
+
"""Log out by clearing both file and browser cookies."""
|
|
350
|
+
clear_cookies()
|
|
351
|
+
try:
|
|
352
|
+
if check_port(CDP_PORT):
|
|
353
|
+
cdp_send_safe("Network.clearBrowserCookies")
|
|
354
|
+
cdp_send_safe("Page.navigate", {"url": TZZB_URL})
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
return {"status": "success", "message": "Logged out successfully"}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def focus_chrome() -> None:
|
|
361
|
+
"""Bring Chrome window to the foreground on macOS."""
|
|
362
|
+
import sys
|
|
363
|
+
if sys.platform == "darwin":
|
|
364
|
+
try:
|
|
365
|
+
subprocess.run(
|
|
366
|
+
["osascript", "-e", 'tell application "Google Chrome" to activate'],
|
|
367
|
+
capture_output=True,
|
|
368
|
+
timeout=2,
|
|
369
|
+
)
|
|
370
|
+
except Exception:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def ensure_qr_login_active(timeout: float = 15.0) -> bool:
|
|
375
|
+
"""Ensure the login dialog is open and switched to QR code scan mode."""
|
|
376
|
+
deadline = time.time() + timeout
|
|
377
|
+
while time.time() < deadline:
|
|
378
|
+
# 1. Trigger modal on main page if not open
|
|
379
|
+
trigger_modal_js = """
|
|
380
|
+
(() => {
|
|
381
|
+
const popup = document.getElementById('ths_iframe_login_popup');
|
|
382
|
+
if (!popup || popup.style.display === 'none') {
|
|
383
|
+
const btn = document.querySelector('.UnloginBar_login') || document.querySelector('.Unlogin_top_box_btn');
|
|
384
|
+
if (btn) btn.click();
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
})()
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
cdp_evaluate(trigger_modal_js)
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
# 2. Check child iframe and ensure qrcode_pannel is active
|
|
396
|
+
try:
|
|
397
|
+
ft = cdp_send_safe("Page.getFrameTree")
|
|
398
|
+
child_frames = ft.get("result", {}).get("frameTree", {}).get("childFrames", [])
|
|
399
|
+
for cf in child_frames:
|
|
400
|
+
frame_id = cf.get("frame", {}).get("id")
|
|
401
|
+
if not frame_id:
|
|
402
|
+
continue
|
|
403
|
+
world = cdp_send_safe("Page.createIsolatedWorld", {"frameId": frame_id, "worldName": "tzzb_login"})
|
|
404
|
+
ctx_id = world.get("result", {}).get("executionContextId")
|
|
405
|
+
if ctx_id:
|
|
406
|
+
res = cdp_send_safe(
|
|
407
|
+
"Runtime.evaluate",
|
|
408
|
+
{
|
|
409
|
+
"contextId": ctx_id,
|
|
410
|
+
"expression": """
|
|
411
|
+
(() => {
|
|
412
|
+
const qrPannel = document.getElementById('qrcode_pannel');
|
|
413
|
+
if (qrPannel && window.getComputedStyle(qrPannel).display === 'block') {
|
|
414
|
+
return 'ready';
|
|
415
|
+
}
|
|
416
|
+
const toQr = document.getElementById('to_qrcode_login');
|
|
417
|
+
if (toQr) {
|
|
418
|
+
toQr.click();
|
|
419
|
+
return 'clicked';
|
|
420
|
+
}
|
|
421
|
+
return 'waiting';
|
|
422
|
+
})()
|
|
423
|
+
""",
|
|
424
|
+
"returnByValue": True,
|
|
425
|
+
},
|
|
426
|
+
)
|
|
427
|
+
status = res.get("result", {}).get("result", {}).get("value")
|
|
428
|
+
if status == "ready":
|
|
429
|
+
return True
|
|
430
|
+
except Exception:
|
|
431
|
+
pass
|
|
432
|
+
time.sleep(0.5)
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def login(timeout: int = 120, force: bool = False) -> dict[str, Any]:
|
|
437
|
+
"""Interactive login flow via Chrome CDP."""
|
|
438
|
+
try:
|
|
439
|
+
if not check_port(CDP_PORT):
|
|
440
|
+
if not launch_chrome():
|
|
441
|
+
return {"status": "error", "message": "Failed to launch Chrome. Please launch Chrome manually."}
|
|
442
|
+
|
|
443
|
+
if force:
|
|
444
|
+
logout()
|
|
445
|
+
|
|
446
|
+
existing = cdp_get_cookies()
|
|
447
|
+
if existing and not force:
|
|
448
|
+
save_cookies(existing)
|
|
449
|
+
return {
|
|
450
|
+
"status": "success",
|
|
451
|
+
"message": f"Cookie valid, userid={existing['userid'][:6]}***",
|
|
452
|
+
"cookies": {k: v[:6] + "***" for k, v in existing.items()},
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
cdp_send_safe("Page.navigate", {"url": TZZB_URL})
|
|
456
|
+
focus_chrome()
|
|
457
|
+
time.sleep(1)
|
|
458
|
+
ensure_qr_login_active(timeout=15.0)
|
|
459
|
+
|
|
460
|
+
start_time = time.time()
|
|
461
|
+
while time.time() - start_time < timeout:
|
|
462
|
+
time.sleep(1)
|
|
463
|
+
cookies = cdp_get_cookies()
|
|
464
|
+
if cookies:
|
|
465
|
+
save_cookies(cookies)
|
|
466
|
+
return {
|
|
467
|
+
"status": "success",
|
|
468
|
+
"message": f"Login successful, userid={cookies['userid'][:6]}***",
|
|
469
|
+
"cookies": {k: v[:6] + "***" for k, v in cookies.items()},
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {"status": "timeout", "message": f"Login timed out after {timeout} seconds"}
|
|
473
|
+
except Exception as e:
|
|
474
|
+
return {"status": "error", "message": str(e)}
|