dp-cli 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {dp_cli-0.3.0 → dp_cli-0.3.2}/PKG-INFO +8 -3
- {dp_cli-0.3.0 → dp_cli-0.3.2}/README.md +7 -2
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/_utils.py +64 -4
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/snapshot_cmd.py +12 -24
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/a11y.py +10 -25
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/clickable.py +28 -15
- dp_cli-0.3.2/dp_cli/snapshot/clickable_js.py +381 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/PKG-INFO +8 -3
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/SOURCES.txt +2 -1
- {dp_cli-0.3.0 → dp_cli-0.3.2}/pyproject.toml +1 -1
- {dp_cli-0.3.0 → dp_cli-0.3.2}/tests/test_clickable.py +60 -16
- dp_cli-0.3.2/tests/test_resolve_locator.py +141 -0
- dp_cli-0.3.0/dp_cli/snapshot/clickable_js.py +0 -273
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/__init__.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/bridge.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/bridge_manager.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/__init__.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/browser.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/element.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/keyboard.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/misc.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/network.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/page.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/storage.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/tab.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/main.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/output.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/session.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/__init__.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/extract.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/js_scripts.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/utils.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/stealth.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/dependency_links.txt +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/entry_points.txt +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/requires.txt +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/top_level.txt +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/setup.cfg +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/tests/test_bridge_integration.py +0 -0
- {dp_cli-0.3.0 → dp_cli-0.3.2}/tests/test_bridge_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dp-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: A powerful CLI for DrissionPage — browser automation, structured data extraction, network listening and more.
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Project-URL: Homepage, https://github.com/mofanx/dp-cli
|
|
@@ -119,8 +119,13 @@ Results are deduplicated by `backendNodeId` and rendered with confidence markers
|
|
|
119
119
|
| Marker | Confidence | Triggers |
|
|
120
120
|
|--------|-----------|----------|
|
|
121
121
|
| none | **high** | `<a href>`, `<button>`, `<input>`, `role=button/link/...`, `contenteditable` |
|
|
122
|
-
| `⚡` | **medium** | `onclick` / `jsaction` / `tabindex>=0` / `aria-selected` / `<audio>/<video
|
|
123
|
-
| `?` | **low** | `cursor:pointer` / class keyword
|
|
122
|
+
| `⚡` | **medium** | `onclick` / `jsaction` / `tabindex>=0` / `aria-selected` / `<audio>/<video>`, or `cursor:pointer` + heuristic (aria-label / icon child / small square / class keyword) — **catches most React/Vue icon buttons** |
|
|
123
|
+
| `?` | **low** | bare `cursor:pointer` / class keyword only (no other signals); hidden unless `--include-low` |
|
|
124
|
+
|
|
125
|
+
Output includes helpful context:
|
|
126
|
+
- `@top-left`, `@top-right`, `@center`, `@bottom` … — position in the 9-region viewport grid
|
|
127
|
+
- `(icon)` — icon-only button (no visible label, has `<svg>` / `<img>` child)
|
|
128
|
+
- Shadow DOM is traversed automatically (open shadow roots)
|
|
124
129
|
|
|
125
130
|
Every element gets an `[N]` ref usable in any command: `dp click "ref:5"`.
|
|
126
131
|
|
|
@@ -98,8 +98,13 @@ Results are deduplicated by `backendNodeId` and rendered with confidence markers
|
|
|
98
98
|
| Marker | Confidence | Triggers |
|
|
99
99
|
|--------|-----------|----------|
|
|
100
100
|
| none | **high** | `<a href>`, `<button>`, `<input>`, `role=button/link/...`, `contenteditable` |
|
|
101
|
-
| `⚡` | **medium** | `onclick` / `jsaction` / `tabindex>=0` / `aria-selected` / `<audio>/<video
|
|
102
|
-
| `?` | **low** | `cursor:pointer` / class keyword
|
|
101
|
+
| `⚡` | **medium** | `onclick` / `jsaction` / `tabindex>=0` / `aria-selected` / `<audio>/<video>`, or `cursor:pointer` + heuristic (aria-label / icon child / small square / class keyword) — **catches most React/Vue icon buttons** |
|
|
102
|
+
| `?` | **low** | bare `cursor:pointer` / class keyword only (no other signals); hidden unless `--include-low` |
|
|
103
|
+
|
|
104
|
+
Output includes helpful context:
|
|
105
|
+
- `@top-left`, `@top-right`, `@center`, `@bottom` … — position in the 9-region viewport grid
|
|
106
|
+
- `(icon)` — icon-only button (no visible label, has `<svg>` / `<img>` child)
|
|
107
|
+
- Shadow DOM is traversed automatically (open shadow roots)
|
|
103
108
|
|
|
104
109
|
Every element gets an `[N]` ref usable in any command: `dp click "ref:5"`.
|
|
105
110
|
|
|
@@ -106,11 +106,55 @@ def normalize_locator(loc: str) -> str:
|
|
|
106
106
|
return loc
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
def
|
|
109
|
+
def _mark_element_by_backend_id(page, backend_node_id: int) -> str:
|
|
110
|
+
"""通过 CDP 给指定 backendNodeId 的元素打一个临时 data-dp-ref 属性,
|
|
111
|
+
返回 marker 字符串,调用方可用 @data-dp-ref=<marker> 精确定位。
|
|
112
|
+
|
|
113
|
+
这是 ref:N → Element 的最鲁棒通路:
|
|
114
|
+
- 绕开 CSS Modules / React 动态 class 命名
|
|
115
|
+
- 绕开 xpath 结构变化
|
|
116
|
+
- 只要 DOM 节点还在,backendNodeId 稳定
|
|
117
|
+
|
|
118
|
+
失败(节点已不存在、CDP 异常)返回 None。
|
|
119
|
+
"""
|
|
120
|
+
import uuid
|
|
121
|
+
marker = 'dp' + uuid.uuid4().hex[:12]
|
|
122
|
+
try:
|
|
123
|
+
res = page.run_cdp('DOM.resolveNode', backendNodeId=int(backend_node_id))
|
|
124
|
+
except Exception:
|
|
125
|
+
return None
|
|
126
|
+
if not isinstance(res, dict):
|
|
127
|
+
return None
|
|
128
|
+
obj_id = (res.get('object') or {}).get('objectId')
|
|
129
|
+
if not obj_id:
|
|
130
|
+
return None
|
|
131
|
+
try:
|
|
132
|
+
page.run_cdp(
|
|
133
|
+
'Runtime.callFunctionOn',
|
|
134
|
+
objectId=obj_id,
|
|
135
|
+
functionDeclaration=(
|
|
136
|
+
'function(m){'
|
|
137
|
+
'try{this.setAttribute("data-dp-ref", m);}catch(e){}'
|
|
138
|
+
'}'
|
|
139
|
+
),
|
|
140
|
+
arguments=[{'value': marker}],
|
|
141
|
+
returnByValue=True,
|
|
142
|
+
)
|
|
143
|
+
except Exception:
|
|
144
|
+
return None
|
|
145
|
+
return marker
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def resolve_locator(locator: str, session: str = 'default', page=None) -> str:
|
|
110
149
|
"""解析定位器:ref:N 展开 + 智能前缀补全。
|
|
111
150
|
|
|
112
|
-
如果 locator 以 'ref:' 开头,从 session 的 refs
|
|
113
|
-
|
|
151
|
+
如果 locator 以 'ref:' 开头,从 session 的 refs 映射中查找。
|
|
152
|
+
- 有 backendNodeId 时:通过 CDP 现场打临时属性,返回 @data-dp-ref=<marker>
|
|
153
|
+
(最鲁棒,绕开 CSS Modules / 动态 class / xpath 变化)
|
|
154
|
+
- 无 backendNodeId 或打标失败时:回落到保存的 locator 字符串
|
|
155
|
+
- 再失败,用 name 作 text 定位器
|
|
156
|
+
|
|
157
|
+
:param page: 可选,传入避免内部再调用 _get_page;为 None 时按需懒加载。
|
|
114
158
|
"""
|
|
115
159
|
if not locator.startswith('ref:'):
|
|
116
160
|
return normalize_locator(locator)
|
|
@@ -130,11 +174,27 @@ def resolve_locator(locator: str, session: str = 'default') -> str:
|
|
|
130
174
|
code='REF_NOT_FOUND')
|
|
131
175
|
raise SystemExit(1)
|
|
132
176
|
|
|
177
|
+
# 1. 首选:backendNodeId 打标
|
|
178
|
+
bid = ref_data.get('backendNodeId')
|
|
179
|
+
if bid:
|
|
180
|
+
if page is None:
|
|
181
|
+
try:
|
|
182
|
+
page = _get_page(session)
|
|
183
|
+
except SystemExit:
|
|
184
|
+
page = None
|
|
185
|
+
except Exception:
|
|
186
|
+
page = None
|
|
187
|
+
if page is not None:
|
|
188
|
+
marker = _mark_element_by_backend_id(page, bid)
|
|
189
|
+
if marker:
|
|
190
|
+
return f'@data-dp-ref={marker}'
|
|
191
|
+
|
|
192
|
+
# 2. 回退:原保存的 locator 字符串
|
|
133
193
|
real_loc = ref_data.get('locator')
|
|
134
194
|
if real_loc and not real_loc.startswith('t:'):
|
|
135
195
|
return real_loc
|
|
136
196
|
|
|
137
|
-
#
|
|
197
|
+
# 3. 再回退:用 name 作文字定位
|
|
138
198
|
name = ref_data.get('name', '')
|
|
139
199
|
if name and len(name) <= 50:
|
|
140
200
|
return f'text:{name}'
|
|
@@ -105,7 +105,9 @@ def register(cli):
|
|
|
105
105
|
@click.option('--format', 'fmt', type=click.Choice(['text', 'json']),
|
|
106
106
|
default='text', show_default=True, help='输出格式')
|
|
107
107
|
@click.option('--filename', default=None, help='保存到文件路径')
|
|
108
|
-
|
|
108
|
+
@click.option('--verbose', '-v', is_flag=True, default=False,
|
|
109
|
+
help='显示 detection reason 和像素尺寸(调试用)')
|
|
110
|
+
def scan(session, viewport_only, confidence, max_elements, fmt, filename, verbose):
|
|
109
111
|
"""Vimium 风格扫描当前页面的可交互元素(纯 DOM 遍历,不依赖 a11y tree)。
|
|
110
112
|
|
|
111
113
|
\b
|
|
@@ -188,40 +190,26 @@ def register(cli):
|
|
|
188
190
|
f'{len(filtered)} after filter'
|
|
189
191
|
+ (' (truncated)' if data.get('truncated') else '')
|
|
190
192
|
)
|
|
191
|
-
rendered_lines.append(
|
|
193
|
+
rendered_lines.append(
|
|
194
|
+
'- ⚡ = medium, ? = low;@zone = 位置区域(top-left/top-right/… 9 宫格);'
|
|
195
|
+
'(icon) = 仅图标按钮;用 ref:N 引用'
|
|
196
|
+
)
|
|
192
197
|
rendered_lines.append('')
|
|
193
198
|
|
|
194
|
-
|
|
195
|
-
marker_map = {'high': '', 'medium': '⚡ ', 'low': '? '}
|
|
199
|
+
from ..snapshot.clickable import format_clickable_record
|
|
196
200
|
|
|
197
201
|
for i, rec in enumerate(filtered, start=1):
|
|
198
202
|
refs[str(i)] = {
|
|
199
203
|
'locator': rec.get('locator') or '',
|
|
200
204
|
'role': f"clickable/{rec.get('tag', '')}",
|
|
201
|
-
'name': (rec.get('text') or '')[:100],
|
|
205
|
+
'name': (rec.get('label') or rec.get('text') or '')[:100],
|
|
202
206
|
'backendNodeId': rec.get('backendNodeId'),
|
|
203
207
|
'confidence': rec.get('confidence'),
|
|
204
208
|
'reason': rec.get('reason'),
|
|
209
|
+
'zone': rec.get('zone'),
|
|
210
|
+
'iconOnly': bool(rec.get('iconOnly')),
|
|
205
211
|
}
|
|
206
|
-
|
|
207
|
-
tag = rec.get('tag', '')
|
|
208
|
-
text = (rec.get('text') or '').strip()
|
|
209
|
-
reason = rec.get('reason') or ''
|
|
210
|
-
rect = rec.get('rect') or {}
|
|
211
|
-
loc = rec.get('locator') or ''
|
|
212
|
-
|
|
213
|
-
parts = [f'- [{i}] {marker}{tag}']
|
|
214
|
-
if text:
|
|
215
|
-
disp = text[:80] + '…' if len(text) > 80 else text
|
|
216
|
-
parts.append(f'"{disp}"')
|
|
217
|
-
meta_bits = [reason]
|
|
218
|
-
if rect.get('w'):
|
|
219
|
-
meta_bits.append(f'{rect["w"]}x{rect["h"]}')
|
|
220
|
-
meta_bits.append(f'@{rect.get("x", 0)},{rect.get("y", 0)}')
|
|
221
|
-
parts.append(f'({", ".join(meta_bits)})')
|
|
222
|
-
if loc:
|
|
223
|
-
parts.append(f'→ {loc}')
|
|
224
|
-
rendered_lines.append(' '.join(parts))
|
|
212
|
+
rendered_lines.append('- ' + format_clickable_record(rec, i, verbose=verbose))
|
|
225
213
|
|
|
226
214
|
# 保存 refs 到 session
|
|
227
215
|
if refs:
|
|
@@ -219,7 +219,7 @@ def render_a11y_text(snapshot: dict, verbose: bool = False,
|
|
|
219
219
|
# ── 追加 clickable_extras(a11y tree 漏掉的可交互元素)──
|
|
220
220
|
extras = snapshot.get('clickable_extras') or []
|
|
221
221
|
if extras:
|
|
222
|
-
from .clickable import
|
|
222
|
+
from .clickable import format_clickable_record
|
|
223
223
|
lines.append('')
|
|
224
224
|
meta = snapshot.get('clickable_meta') or {}
|
|
225
225
|
header_suffix = []
|
|
@@ -232,38 +232,23 @@ def render_a11y_text(snapshot: dict, verbose: bool = False,
|
|
|
232
232
|
lines.append(f'### Additional Interactive Elements'
|
|
233
233
|
f' (Vimium-style, not in a11y tree){suffix_str}')
|
|
234
234
|
lines.append(f'- 共 {len(extras)} 个;⚡ = medium 置信, ? = low 置信;'
|
|
235
|
-
f'
|
|
235
|
+
f'@zone=位置区域(top-left/top-right/center/… 9 宫格);'
|
|
236
|
+
f'(icon)=仅图标;用 ref:N 引用')
|
|
236
237
|
lines.append('')
|
|
237
238
|
for rec in extras:
|
|
238
239
|
ctx['counter'] += 1
|
|
239
240
|
rid = ctx['counter']
|
|
240
|
-
|
|
241
|
-
tag = rec.get('tag', '')
|
|
242
|
-
text = (rec.get('text') or '').strip()
|
|
243
|
-
reason = rec.get('reason') or ''
|
|
244
|
-
loc = rec.get('locator') or ''
|
|
245
|
-
rect = rec.get('rect') or {}
|
|
246
|
-
|
|
247
|
-
parts = [f'- [{rid}] {marker}{tag}']
|
|
248
|
-
if text:
|
|
249
|
-
display_text = text[:80] + '…' if len(text) > 80 else text
|
|
250
|
-
parts.append(f'"{display_text}"')
|
|
251
|
-
meta_parts = [reason]
|
|
252
|
-
if rect.get('w'):
|
|
253
|
-
meta_parts.append(f'{rect["w"]}x{rect["h"]}')
|
|
254
|
-
parts.append(f'({", ".join(meta_parts)})')
|
|
255
|
-
if loc:
|
|
256
|
-
parts.append(f'→ {loc}')
|
|
257
|
-
lines.append(' '.join(parts))
|
|
258
|
-
|
|
241
|
+
lines.append('- ' + format_clickable_record(rec, rid))
|
|
259
242
|
# 记入 refs 以便 click/fill 引用
|
|
260
243
|
ctx['refs'][str(rid)] = {
|
|
261
|
-
'locator':
|
|
262
|
-
'role': f
|
|
263
|
-
'name': text[:100],
|
|
244
|
+
'locator': rec.get('locator') or '',
|
|
245
|
+
'role': f"clickable/{rec.get('tag', '')}",
|
|
246
|
+
'name': (rec.get('label') or rec.get('text') or '')[:100],
|
|
264
247
|
'backendNodeId': rec.get('backendNodeId'),
|
|
265
248
|
'confidence': rec.get('confidence'),
|
|
266
|
-
'reason': reason,
|
|
249
|
+
'reason': rec.get('reason'),
|
|
250
|
+
'zone': rec.get('zone'),
|
|
251
|
+
'iconOnly': bool(rec.get('iconOnly')),
|
|
267
252
|
}
|
|
268
253
|
|
|
269
254
|
if snapshot.get('clickable_warning'):
|
|
@@ -106,6 +106,9 @@ def detect_clickables(page,
|
|
|
106
106
|
'confidence': e.get('confidence'),
|
|
107
107
|
'reason': e.get('reason'),
|
|
108
108
|
'text': e.get('text'),
|
|
109
|
+
'label': e.get('label'),
|
|
110
|
+
'iconOnly': bool(e.get('iconOnly')),
|
|
111
|
+
'zone': e.get('zone'),
|
|
109
112
|
'rect': e.get('rect'),
|
|
110
113
|
'inViewport': e.get('inViewport'),
|
|
111
114
|
'backendNodeId': backend_node_id,
|
|
@@ -167,31 +170,41 @@ CONFIDENCE_MARKER = {
|
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
|
|
170
|
-
def format_clickable_record(rec: dict, ref_id: int) -> str:
|
|
173
|
+
def format_clickable_record(rec: dict, ref_id: int, verbose: bool = False) -> str:
|
|
171
174
|
"""把一条 ClickableRecord 渲染成一行文本。
|
|
172
175
|
|
|
173
|
-
|
|
174
|
-
[
|
|
175
|
-
[
|
|
176
|
+
默认紧凑格式(给 AI / 用户看,强调有用信息):
|
|
177
|
+
[5] button "Sign in" @top-right → #signin
|
|
178
|
+
[8] ⚡ div "User profile" @top-left (icon) → .user-menu-btn
|
|
179
|
+
[9] ⚡ svg @top-right (icon) → xpath://header//svg[3]
|
|
180
|
+
[12] ? span "see more" @bottom → css:.footer > span:nth-child(3)
|
|
181
|
+
|
|
182
|
+
verbose=True 时追加 reason + 尺寸,用于调试:
|
|
183
|
+
[5] button "Sign in" @top-right (button, 80x32) → #signin
|
|
176
184
|
"""
|
|
177
185
|
marker = CONFIDENCE_MARKER.get(rec.get('confidence'), '')
|
|
178
186
|
tag = rec.get('tag', '')
|
|
179
|
-
|
|
187
|
+
label = (rec.get('label') or rec.get('text') or '').strip()
|
|
188
|
+
zone = rec.get('zone') or ''
|
|
189
|
+
icon_only = bool(rec.get('iconOnly'))
|
|
180
190
|
reason = rec.get('reason') or ''
|
|
181
191
|
rect = rec.get('rect') or {}
|
|
182
192
|
loc = rec.get('locator') or ''
|
|
183
193
|
|
|
184
194
|
parts = [f'[{ref_id}]', f'{marker}{tag}']
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
if label:
|
|
196
|
+
display = label[:80] + '…' if len(label) > 80 else label
|
|
197
|
+
parts.append(f'"{display}"')
|
|
198
|
+
elif icon_only:
|
|
199
|
+
# 无 label 但有 icon:标注 (icon) 提示这是图标按钮
|
|
200
|
+
parts.append('(icon)')
|
|
201
|
+
if zone:
|
|
202
|
+
parts.append(f'@{zone}')
|
|
203
|
+
if verbose:
|
|
204
|
+
meta_bits = [reason]
|
|
205
|
+
if rect.get('w'):
|
|
206
|
+
meta_bits.append(f'{rect["w"]}x{rect["h"]}')
|
|
207
|
+
parts.append(f'({", ".join(meta_bits)})')
|
|
195
208
|
if loc:
|
|
196
209
|
parts.append(f'→ {loc}')
|
|
197
210
|
return ' '.join(parts)
|