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.
Files changed (40) hide show
  1. {dp_cli-0.3.0 → dp_cli-0.3.2}/PKG-INFO +8 -3
  2. {dp_cli-0.3.0 → dp_cli-0.3.2}/README.md +7 -2
  3. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/_utils.py +64 -4
  4. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/snapshot_cmd.py +12 -24
  5. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/a11y.py +10 -25
  6. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/clickable.py +28 -15
  7. dp_cli-0.3.2/dp_cli/snapshot/clickable_js.py +381 -0
  8. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/PKG-INFO +8 -3
  9. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/SOURCES.txt +2 -1
  10. {dp_cli-0.3.0 → dp_cli-0.3.2}/pyproject.toml +1 -1
  11. {dp_cli-0.3.0 → dp_cli-0.3.2}/tests/test_clickable.py +60 -16
  12. dp_cli-0.3.2/tests/test_resolve_locator.py +141 -0
  13. dp_cli-0.3.0/dp_cli/snapshot/clickable_js.py +0 -273
  14. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/__init__.py +0 -0
  15. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/bridge.py +0 -0
  16. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/bridge_manager.py +0 -0
  17. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/__init__.py +0 -0
  18. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/browser.py +0 -0
  19. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/element.py +0 -0
  20. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/keyboard.py +0 -0
  21. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/misc.py +0 -0
  22. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/network.py +0 -0
  23. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/page.py +0 -0
  24. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/storage.py +0 -0
  25. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/commands/tab.py +0 -0
  26. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/main.py +0 -0
  27. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/output.py +0 -0
  28. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/session.py +0 -0
  29. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/__init__.py +0 -0
  30. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/extract.py +0 -0
  31. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/js_scripts.py +0 -0
  32. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/snapshot/utils.py +0 -0
  33. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli/stealth.py +0 -0
  34. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/dependency_links.txt +0 -0
  35. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/entry_points.txt +0 -0
  36. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/requires.txt +0 -0
  37. {dp_cli-0.3.0 → dp_cli-0.3.2}/dp_cli.egg-info/top_level.txt +0 -0
  38. {dp_cli-0.3.0 → dp_cli-0.3.2}/setup.cfg +0 -0
  39. {dp_cli-0.3.0 → dp_cli-0.3.2}/tests/test_bridge_integration.py +0 -0
  40. {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.0
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 match (`btn` / `click` / `toggle` / …) |
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 match (`btn` / `click` / `toggle` / …) |
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 resolve_locator(locator: str, session: str = 'default') -> str:
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
- 否则尝试智能补全 css:/xpath: 前缀。
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
- # locator 不可用时(如 t:p),尝试用 name 作为 text 定位器
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
- def scan(session, viewport_only, confidence, max_elements, fmt, filename):
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('- 用 ref:N 引用元素(⚡ = medium, ? = low)')
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
- marker = marker_map.get(rec.get('confidence'), '')
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 CONFIDENCE_MARKER
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' ref:N 引用')
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
- marker = CONFIDENCE_MARKER.get(rec.get('confidence'), '')
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': loc,
262
- 'role': f'clickable/{tag}',
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
- [42] button "Sign in" (role=button, pos=120,340, 80x32) → #signin
175
- [43] ? span "•••" (cursor:pointer, pos=200,150, 24x24) → .more-icon
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
- text = rec.get('text') or ''
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 text:
186
- parts.append(f'"{text[:80]}"')
187
- rect_str = ''
188
- if rect:
189
- rect_str = f'{rect.get("w", 0)}x{rect.get("h", 0)}@{rect.get("x", 0)},{rect.get("y", 0)}'
190
- meta = f'({reason}'
191
- if rect_str:
192
- meta += f', {rect_str}'
193
- meta += ')'
194
- parts.append(meta)
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)