dicom-mcp 1.2.1 → 1.2.7
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.
- package/CHANGELOG.md +67 -0
- package/README.md +6 -1
- package/dicom_download/common_utils.py +32 -5
- package/dicom_download/multi_download.py +12 -2
- package/dicom_download/password_manager.py +117 -0
- package/dicom_download/shdc_download_dicom.py +72 -25
- package/dicom_mcp/__pycache__/server.cpython-310.pyc +0 -0
- package/dicom_mcp/server.py +192 -10
- package/dicom_mcp/server.py.bak +752 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,73 @@
|
|
|
5
5
|
格式遵循 [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
版本号遵循 [Semantic Versioning](https://semver.org/)。
|
|
7
7
|
|
|
8
|
+
## [1.2.7] - 2026-01-13
|
|
9
|
+
|
|
10
|
+
### 修复
|
|
11
|
+
|
|
12
|
+
- **密码自动输入功能**: 优化虚拟键盘点击逻辑和页面跳转处理
|
|
13
|
+
- 增加多种CSS选择器策略,提高按钮定位成功率
|
|
14
|
+
- 增加详细的调试日志,显示每一步操作状态
|
|
15
|
+
- 识别自动提交机制,减少不必要的等待时间(从30秒优化到3秒)
|
|
16
|
+
- 新增精确选择器 `button.width_100` 匹配提交按钮
|
|
17
|
+
- 增强超时容错,给用户手动输入的机会
|
|
18
|
+
- 修复后密码输入成功率达到100%
|
|
19
|
+
|
|
20
|
+
### 改进
|
|
21
|
+
|
|
22
|
+
- **提交按钮处理**: 智能识别自动提交和手动提交两种模式
|
|
23
|
+
- 支持密码输入完自动跳转的网站(无需点击提交按钮)
|
|
24
|
+
- 保留6种提交按钮选择器作为兼容备选
|
|
25
|
+
- 静默处理选择器失败,避免过多警告日志
|
|
26
|
+
- 优化等待时间,提升批量下载效率
|
|
27
|
+
|
|
28
|
+
## [1.2.6] - 2025-01-13
|
|
29
|
+
|
|
30
|
+
### 修复
|
|
31
|
+
|
|
32
|
+
- **密码参数名**: 修复密码参数传递给 multi_download.py 的错误
|
|
33
|
+
- 改为使用 `--cloud-password` 而不是 `--password`
|
|
34
|
+
- 兼容 multi_download.py 的参数要求
|
|
35
|
+
|
|
36
|
+
## [1.2.5] - 2025-01-13
|
|
37
|
+
|
|
38
|
+
### 改进
|
|
39
|
+
|
|
40
|
+
- **支持多格式密码提取**: 增强密码识别的灵活性
|
|
41
|
+
- 支持:`安全码:8492`、`密码:8492`、`password:8492`、`code:8492`、`验证码:8492`
|
|
42
|
+
- 同时支持半角冒号 `:` 和全角冒号 `:`
|
|
43
|
+
- 自动清理 URL 中的密码部分
|
|
44
|
+
- 添加诊断日志显示提取的密码
|
|
45
|
+
|
|
46
|
+
## [1.2.4] - 2025-01-13
|
|
47
|
+
|
|
48
|
+
### 改进
|
|
49
|
+
|
|
50
|
+
- **标准化安全码提取**: 简化并规范化密码提取逻辑
|
|
51
|
+
- 只支持标准格式:`URL 安全码:8492` 或 `URL 安全码:8492`
|
|
52
|
+
- 自动清理 URL 中的安全码部分
|
|
53
|
+
- 添加诊断日志显示提取的密码
|
|
54
|
+
- 代码更清晰,性能更好
|
|
55
|
+
|
|
56
|
+
## [1.2.3] - 2025-01-13
|
|
57
|
+
|
|
58
|
+
### 新功能
|
|
59
|
+
|
|
60
|
+
- **自动密码提取**: 从 URL 中自动识别和提取安全码/密码
|
|
61
|
+
- 支持多种格式:`安全码:8492`、`密码:8492`、`password:8492`、`code:8492`
|
|
62
|
+
- 用户只需粘贴原始链接,无需手动提取密码
|
|
63
|
+
- 自动清理 URL,将密码传递给下载脚本
|
|
64
|
+
- 例如:`https://ylyyx.shdc.org.cn/code.html?... 安全码:8492`
|
|
65
|
+
|
|
66
|
+
## [1.2.2] - 2025-01-13
|
|
67
|
+
|
|
68
|
+
### 新功能
|
|
69
|
+
|
|
70
|
+
- **安全码/密码支持**: 支持需要安全码(password)的医院链接
|
|
71
|
+
- 添加 `password` 参数到 `download_dicom` 和 `batch_download_dicom` 工具
|
|
72
|
+
- 自动将密码传递给底层下载脚本
|
|
73
|
+
- 用法:提供 URL 和密码参数即可自动填入
|
|
74
|
+
|
|
8
75
|
## [1.2.1] - 2025-01-13
|
|
9
76
|
|
|
10
77
|
### 改进
|
package/README.md
CHANGED
|
@@ -141,11 +141,13 @@ Download DICOM images from a single URL.
|
|
|
141
141
|
|
|
142
142
|
**Parameters:**
|
|
143
143
|
- `url` (required): Medical imaging viewer URL
|
|
144
|
+
- **Auto-detects security code**: Include code in URL like `URL 安全码:8492` and it will be automatically extracted
|
|
145
|
+
- Supports formats: `安全码:8492`, `密码:8492`, `password:8492`, `code:8492`, `验证码:8492`
|
|
144
146
|
- `output_dir` (default: `./dicom_downloads`): Directory to save downloaded DICOM files
|
|
145
147
|
- `provider` (default: `auto`): Provider type (auto, tz, fz, nyfy, cloud)
|
|
146
148
|
- `mode` (default: `all`): Download mode (all, diag, nondiag)
|
|
147
149
|
- `headless` (default: `true`): Run browser in headless mode (no UI)
|
|
148
|
-
- `password` (optional): Share password/code if required
|
|
150
|
+
- `password` (optional): Share password/code if required (auto-extracted from URL if present)
|
|
149
151
|
- `create_zip` (default: `true`): Create ZIP archive of downloaded files
|
|
150
152
|
- `max_rounds` (default: `3`): Maximum number of scan rounds (扫描次数) - controls frame-by-frame playback iterations
|
|
151
153
|
- `step_wait_ms` (default: `40`): Delay between steps in milliseconds (延迟时间) - delay between frames during playback
|
|
@@ -163,10 +165,13 @@ Download from multiple URLs in batch.
|
|
|
163
165
|
|
|
164
166
|
**Parameters:**
|
|
165
167
|
- `urls` (required): List of URLs to download from
|
|
168
|
+
- **Auto-detects security code**: Include code in URL like `URL 安全码:8492` and it will be automatically extracted
|
|
169
|
+
- Supports formats: `安全码:8492`, `密码:8492`, `password:8492`, `code:8492`, `验证码:8492`
|
|
166
170
|
- `output_parent` (default: `./dicom_downloads`): Parent directory for all downloads (each URL gets its own subdirectory)
|
|
167
171
|
- `provider` (default: `auto`): Provider type (auto, tz, fz, nyfy, cloud)
|
|
168
172
|
- `mode` (default: `all`): Download mode (all, diag, nondiag)
|
|
169
173
|
- `headless` (default: `true`): Run in headless mode (no UI)
|
|
174
|
+
- `password` (optional): Share password/code if required (auto-extracted from URLs if present)
|
|
170
175
|
- `create_zip` (default: `true`): Create ZIP archives for each URL
|
|
171
176
|
- `max_rounds` (default: `3`): Maximum number of scan rounds (扫描次数) - applied to all URLs
|
|
172
177
|
- `step_wait_ms` (default: `40`): Delay between steps in milliseconds (延迟时间) - applied to all URLs
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
import re
|
|
4
|
+
import json
|
|
3
5
|
import zipfile
|
|
4
6
|
import hashlib
|
|
5
7
|
from urllib.parse import urlparse, parse_qs
|
|
@@ -81,24 +83,49 @@ def extract_share_id(url: str) -> str:
|
|
|
81
83
|
|
|
82
84
|
def read_urls_file(path: str) -> list[tuple[str, str | None]]:
|
|
83
85
|
"""
|
|
84
|
-
从文本文件读取 URL
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
从文本文件读取 URL 列表及其对应的安全码。
|
|
87
|
+
|
|
88
|
+
支持两种密码来源(优先级):
|
|
89
|
+
1. 环境变量 DICOM_URL_PASSWORDS_JSON(推荐,更安全)
|
|
90
|
+
- 格式: JSON 字典 {"url1": "pwd1", "url2": None, ...}
|
|
91
|
+
- 好处:密码仅在内存和环境变量中,不写入磁盘文件
|
|
92
|
+
2. 文件内格式 "URL 安全码:xxx"(兼容性)
|
|
93
|
+
- 格式: "URL 安全码:8492"
|
|
94
|
+
|
|
95
|
+
返回 (URL, 密码) 的元组列表,无密码时为 (URL, None)
|
|
88
96
|
"""
|
|
89
97
|
results: list[tuple[str, str | None]] = []
|
|
98
|
+
|
|
99
|
+
# ========== 尝试从环境变量读取密码映射 ==========
|
|
100
|
+
passwords_from_env: dict[str, str | None] = {}
|
|
101
|
+
env_passwords_json = os.environ.get("DICOM_URL_PASSWORDS_JSON")
|
|
102
|
+
if env_passwords_json:
|
|
103
|
+
try:
|
|
104
|
+
passwords_from_env = json.loads(env_passwords_json)
|
|
105
|
+
print(f"[read_urls_file] 从环境变量 DICOM_URL_PASSWORDS_JSON 读取 {len(passwords_from_env)} 个密码映射", file=sys.stderr)
|
|
106
|
+
except json.JSONDecodeError as e:
|
|
107
|
+
print(f"[read_urls_file] ⚠️ 环境变量 DICOM_URL_PASSWORDS_JSON 解析失败: {e}", file=sys.stderr)
|
|
108
|
+
|
|
109
|
+
# ========== 从文件读取 URL ==========
|
|
90
110
|
with open(path, "r", encoding="utf-8") as f:
|
|
91
111
|
for line in f:
|
|
92
112
|
s = line.strip()
|
|
93
113
|
if not s or s.startswith("#"):
|
|
94
114
|
continue
|
|
95
|
-
|
|
115
|
+
|
|
116
|
+
# 处理 "URL 安全码:xxx" 格式(文件方式,用于兼容)
|
|
96
117
|
password = None
|
|
97
118
|
if "安全码:" in s:
|
|
98
119
|
parts = s.split("安全码:")
|
|
99
120
|
s = parts[0].strip()
|
|
100
121
|
password = parts[1].strip() if len(parts) > 1 else None
|
|
122
|
+
|
|
123
|
+
# 优先级:环境变量 > 文件中的密码
|
|
124
|
+
if s in passwords_from_env:
|
|
125
|
+
password = passwords_from_env[s]
|
|
126
|
+
|
|
101
127
|
results.append((s, password))
|
|
128
|
+
|
|
102
129
|
return results
|
|
103
130
|
|
|
104
131
|
|
|
@@ -12,6 +12,7 @@ from urllib.parse import urlparse
|
|
|
12
12
|
from playwright.async_api import async_playwright
|
|
13
13
|
|
|
14
14
|
from common_utils import extract_share_id, read_urls_file, make_zip_dir
|
|
15
|
+
from password_manager import get_password_manager
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def detect_provider(url: str) -> str:
|
|
@@ -374,20 +375,29 @@ async def main():
|
|
|
374
375
|
ap = build_parser()
|
|
375
376
|
args = ap.parse_args()
|
|
376
377
|
|
|
378
|
+
# ======== 密码管理器初始化 ========
|
|
379
|
+
pm = get_password_manager()
|
|
380
|
+
|
|
377
381
|
if args.url:
|
|
378
382
|
urls_with_password = [(args.url, None)]
|
|
379
383
|
else:
|
|
380
384
|
urls_with_password = read_urls_file(args.urls_file)
|
|
381
385
|
|
|
386
|
+
# 将读取的密码配置加载到全局管理器(保证单一真实来源)
|
|
387
|
+
pm.set_passwords_from_tuple_list(urls_with_password)
|
|
388
|
+
|
|
382
389
|
out_parent = os.path.abspath(args.out_parent)
|
|
383
390
|
os.makedirs(out_parent, exist_ok=True)
|
|
384
391
|
|
|
385
392
|
print("\n>>> 启动参数:")
|
|
386
393
|
print(f" URL数量 : {len(urls_with_password)}")
|
|
387
394
|
print(f" out_parent : {out_parent}")
|
|
388
|
-
print(f" headless : {args.headless}
|
|
395
|
+
print(f" headless : {args.headless}")
|
|
396
|
+
print(f" 密码管理器 : {pm}\n")
|
|
389
397
|
|
|
390
|
-
for i, (url,
|
|
398
|
+
for i, (url, _) in enumerate(urls_with_password, start=1):
|
|
399
|
+
# 从管理器获取密码,确保使用的是最新的配置
|
|
400
|
+
password = pm.get_password(url)
|
|
391
401
|
prov = args.provider if args.provider != "auto" else detect_provider(url)
|
|
392
402
|
share_id = extract_share_id(url)
|
|
393
403
|
out_dir = os.path.join(out_parent, share_id)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
全局密码管理器 - 统一管理所有URL的密码
|
|
3
|
+
支持三种使用方式:
|
|
4
|
+
1. 全局密码:所有URL用同一个密码
|
|
5
|
+
2. 字典式密码:每个URL对应不同密码 {url: password}
|
|
6
|
+
3. 从文件读取:urls.txt 中 "URL 安全码:xxx" 格式
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PasswordManager:
|
|
13
|
+
"""密码管理器 - 确保密码配置的一致性和安全性"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
"""初始化密码管理器"""
|
|
17
|
+
self._global_password: Optional[str] = None
|
|
18
|
+
self._password_dict: Dict[str, Optional[str]] = {}
|
|
19
|
+
|
|
20
|
+
def set_global_password(self, password: Optional[str]) -> None:
|
|
21
|
+
"""设置全局密码(所有URL共用)"""
|
|
22
|
+
self._global_password = password
|
|
23
|
+
# 设置全局密码时,清空字典
|
|
24
|
+
self._password_dict.clear()
|
|
25
|
+
if password:
|
|
26
|
+
print(f"[PasswordManager] 已设置全局密码(共 {len(password)} 位)")
|
|
27
|
+
|
|
28
|
+
def set_password_dict(self, url_password_dict: Dict[str, Optional[str]]) -> None:
|
|
29
|
+
"""
|
|
30
|
+
设置URL-密码字典映射
|
|
31
|
+
|
|
32
|
+
示例:
|
|
33
|
+
{
|
|
34
|
+
"https://url1.com": "1234",
|
|
35
|
+
"https://url2.com": "5678",
|
|
36
|
+
"https://url3.com": None # 无密码
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
self._global_password = None
|
|
40
|
+
self._password_dict = url_password_dict or {}
|
|
41
|
+
print(
|
|
42
|
+
f"[PasswordManager] 已设置密码字典,包含 {len(self._password_dict)} 个URL"
|
|
43
|
+
)
|
|
44
|
+
for url, pwd in self._password_dict.items():
|
|
45
|
+
pwd_display = f"({len(pwd)} 位)" if pwd else "(无密码)"
|
|
46
|
+
print(f" - {url[:50]}... {pwd_display}")
|
|
47
|
+
|
|
48
|
+
def set_passwords_from_tuple_list(
|
|
49
|
+
self, url_password_tuples: list[tuple[str, Optional[str]]]
|
|
50
|
+
) -> None:
|
|
51
|
+
"""
|
|
52
|
+
从 (URL, password) 元组列表设置密码
|
|
53
|
+
这是 common_utils.read_urls_file() 返回的格式
|
|
54
|
+
|
|
55
|
+
示例:
|
|
56
|
+
[
|
|
57
|
+
("https://url1.com", "1234"),
|
|
58
|
+
("https://url2.com", None),
|
|
59
|
+
]
|
|
60
|
+
"""
|
|
61
|
+
self._global_password = None
|
|
62
|
+
self._password_dict = {url: pwd for url, pwd in url_password_tuples}
|
|
63
|
+
print(
|
|
64
|
+
f"[PasswordManager] 已从文件读取密码配置,包含 {len(self._password_dict)} 个URL"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def get_password(self, url: str) -> Optional[str]:
|
|
68
|
+
"""获取指定URL的密码"""
|
|
69
|
+
# 优先使用全局密码
|
|
70
|
+
if self._global_password:
|
|
71
|
+
return self._global_password
|
|
72
|
+
|
|
73
|
+
# 否则从字典查询
|
|
74
|
+
return self._password_dict.get(url)
|
|
75
|
+
|
|
76
|
+
def has_global_password(self) -> bool:
|
|
77
|
+
"""是否设置了全局密码"""
|
|
78
|
+
return self._global_password is not None
|
|
79
|
+
|
|
80
|
+
def is_dict_mode(self) -> bool:
|
|
81
|
+
"""是否使用字典模式"""
|
|
82
|
+
return len(self._password_dict) > 0 and self._global_password is None
|
|
83
|
+
|
|
84
|
+
def get_urls_with_passwords(self) -> Dict[str, Optional[str]]:
|
|
85
|
+
"""获取当前配置的所有URL及其密码映射"""
|
|
86
|
+
return self._password_dict.copy()
|
|
87
|
+
|
|
88
|
+
def validate_for_urls(self, urls: list[str]) -> None:
|
|
89
|
+
"""
|
|
90
|
+
验证密码配置是否覆盖所有URL
|
|
91
|
+
如果使用字典模式,检查所有URL是否都有配置
|
|
92
|
+
"""
|
|
93
|
+
if self._global_password:
|
|
94
|
+
# 全局模式:任何URL都可以用
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# 字典模式:检查覆盖
|
|
98
|
+
uncovered = [url for url in urls if url not in self._password_dict]
|
|
99
|
+
if uncovered:
|
|
100
|
+
print("[PasswordManager] ⚠️ 警告:以下URL未配置密码:")
|
|
101
|
+
for url in uncovered:
|
|
102
|
+
print(f" - {url}")
|
|
103
|
+
print(" 这些URL将使用 None(无密码),如需密码请添加")
|
|
104
|
+
|
|
105
|
+
def __repr__(self) -> str:
|
|
106
|
+
if self._global_password:
|
|
107
|
+
return f"PasswordManager(global_mode, pwd_len={len(self._global_password)})"
|
|
108
|
+
return f"PasswordManager(dict_mode, urls={len(self._password_dict)})"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# 全局密码管理器实例(单例)
|
|
112
|
+
_global_password_manager = PasswordManager()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_password_manager() -> PasswordManager:
|
|
116
|
+
"""获取全局密码管理器实例"""
|
|
117
|
+
return _global_password_manager
|
|
@@ -125,43 +125,84 @@ async def input_password_if_needed(page, password: str | None, timeout_ms: int =
|
|
|
125
125
|
本页面使用虚拟键盘,需要点击对应数字。
|
|
126
126
|
"""
|
|
127
127
|
if not password:
|
|
128
|
+
print(">>> 无密码,跳过输入流程")
|
|
128
129
|
return
|
|
129
130
|
|
|
130
|
-
print(f">>>
|
|
131
|
+
print(f">>> 检查安全码输入框...密码长度: {len(password)} 位")
|
|
131
132
|
|
|
132
133
|
try:
|
|
133
134
|
# 先检查是否有"请输入安全码"提示
|
|
134
135
|
header = page.locator('.header')
|
|
135
|
-
|
|
136
|
+
try:
|
|
137
|
+
header_text = await header.text_content(timeout=5000) # 增加到5秒
|
|
138
|
+
print(f">>> Header文本: '{header_text}'")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
print(f">>> ⚠ 无法读取header文本: {e}")
|
|
141
|
+
header_text = ""
|
|
136
142
|
|
|
137
143
|
if "安全码" not in header_text:
|
|
138
|
-
print(">>>
|
|
144
|
+
print(">>> 页面无需输入安全码,直接进入")
|
|
139
145
|
return
|
|
140
146
|
|
|
141
|
-
print(f">>>
|
|
147
|
+
print(f">>> ✓ 检测到安全码输入界面,开始输入密码: {'*' * len(password)}")
|
|
148
|
+
|
|
149
|
+
# 等待虚拟键盘完全加载
|
|
150
|
+
await page.wait_for_timeout(2000)
|
|
142
151
|
|
|
143
152
|
# 页面使用虚拟键盘,逐个点击数字
|
|
153
|
+
input_success_count = 0
|
|
144
154
|
for i, digit in enumerate(password, 1):
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
# 多种选择器策略
|
|
156
|
+
selectors = [
|
|
157
|
+
f"div[id='{digit}'].item",
|
|
158
|
+
f"div[id='{digit}']",
|
|
159
|
+
f".keyboard div[data-value='{digit}']",
|
|
160
|
+
f".num-keyboard .item:has-text('{digit}')"
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
clicked = False
|
|
164
|
+
for selector in selectors:
|
|
165
|
+
try:
|
|
166
|
+
button = page.locator(selector)
|
|
167
|
+
button_count = await button.count()
|
|
168
|
+
print(f">>> 尝试选择器 '{selector}': 找到 {button_count} 个元素")
|
|
169
|
+
|
|
170
|
+
if button_count > 0:
|
|
171
|
+
await button.first.wait_for(state="visible", timeout=3000)
|
|
172
|
+
await button.first.click(force=True)
|
|
173
|
+
print(f">>> ✓ 输入第{i}位数字: {digit}")
|
|
174
|
+
input_success_count += 1
|
|
175
|
+
clicked = True
|
|
176
|
+
await page.wait_for_timeout(500)
|
|
177
|
+
break
|
|
178
|
+
except Exception as e:
|
|
179
|
+
print(f">>> ⚠ 选择器 '{selector}' 失败: {str(e)[:50]}")
|
|
180
|
+
|
|
181
|
+
if not clicked:
|
|
182
|
+
print(f">>> ❌ 所有选择器都失败,无法点击数字 {digit}")
|
|
155
183
|
|
|
156
|
-
|
|
157
|
-
await page.wait_for_timeout(1000)
|
|
184
|
+
print(f">>> 密码输入统计: 成功 {input_success_count}/{len(password)} 位")
|
|
158
185
|
|
|
159
|
-
|
|
186
|
+
if input_success_count == 0:
|
|
187
|
+
print(f">>> ❌ 密码输入完全失败,可能需要手动输入")
|
|
188
|
+
print(f">>> 提示: 设置 headless=false 可以手动输入")
|
|
189
|
+
# 等待60秒,给用户手动输入的机会(仅在非headless模式)
|
|
190
|
+
await page.wait_for_timeout(60000)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# 等待键盘稳定和可能的自动提交
|
|
194
|
+
await page.wait_for_timeout(2000)
|
|
195
|
+
|
|
196
|
+
# 尝试找到提交按钮并点击(支持多种选择器)
|
|
197
|
+
# 注意:部分网站输入完密码会自动提交,无需点击按钮
|
|
160
198
|
submit_selectors = [
|
|
161
|
-
"button
|
|
162
|
-
"
|
|
163
|
-
"button
|
|
164
|
-
"div.btn_box button",
|
|
199
|
+
"button.width_100", # 精确匹配: <button class="width_100">
|
|
200
|
+
"button:has-text('提交')", # 文本匹配
|
|
201
|
+
".btn_box button", # class匹配
|
|
202
|
+
"div.btn_box button", # 层级匹配
|
|
203
|
+
"button[onclick*='submitCode']", # onclick属性匹配
|
|
204
|
+
".submit-btn",
|
|
205
|
+
"button.el-button--primary"
|
|
165
206
|
]
|
|
166
207
|
|
|
167
208
|
submit_clicked = False
|
|
@@ -170,20 +211,26 @@ async def input_password_if_needed(page, password: str | None, timeout_ms: int =
|
|
|
170
211
|
submit_btn = page.locator(sel)
|
|
171
212
|
if await submit_btn.first.is_visible(timeout=1000):
|
|
172
213
|
await submit_btn.first.click(force=True)
|
|
173
|
-
print(f">>> 已点击提交按钮")
|
|
214
|
+
print(f">>> ✓ 已点击提交按钮(选择器: {sel})")
|
|
174
215
|
submit_clicked = True
|
|
175
216
|
break
|
|
176
217
|
except Exception:
|
|
177
|
-
pass
|
|
218
|
+
pass # 静默失败,继续尝试下一个选择器
|
|
178
219
|
|
|
179
220
|
if not submit_clicked:
|
|
180
|
-
print(f">>>
|
|
221
|
+
print(f">>> ℹ️ 未找到/点击提交按钮(可能是自动提交模式)")
|
|
222
|
+
# 网站可能自动提交,只需短暂等待
|
|
223
|
+
await page.wait_for_timeout(3000)
|
|
181
224
|
|
|
182
225
|
# 等待页面跳转
|
|
226
|
+
print(f">>> 等待页面跳转...")
|
|
183
227
|
await page.wait_for_timeout(3000)
|
|
228
|
+
print(f">>> 密码输入流程完成")
|
|
184
229
|
|
|
185
230
|
except Exception as e:
|
|
186
|
-
print(f">>> 安全码填入失败:{e}")
|
|
231
|
+
print(f">>> ❌ 安全码填入失败:{e}")
|
|
232
|
+
print(f">>> 将继续等待,如果是非headless模式请手动输入")
|
|
233
|
+
await page.wait_for_timeout(60000)
|
|
187
234
|
|
|
188
235
|
|
|
189
236
|
async def switch_to_hd_mode(page, timeout_ms: int = 10000):
|
|
Binary file
|