dp-cli 0.6.3__tar.gz → 0.8.0__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 (91) hide show
  1. dp_cli-0.8.0/LICENSE +29 -0
  2. dp_cli-0.8.0/MANIFEST.in +1 -0
  3. {dp_cli-0.6.3 → dp_cli-0.8.0}/PKG-INFO +23 -3
  4. {dp_cli-0.6.3 → dp_cli-0.8.0}/README.md +15 -2
  5. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/bridge.py +16 -16
  6. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/bridge_manager.py +18 -16
  7. dp_cli-0.8.0/dp_cli/catalog.py +109 -0
  8. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/__init__.py +5 -1
  9. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/_utils.py +267 -5
  10. dp_cli-0.8.0/dp_cli/commands/assert_cmd.py +454 -0
  11. dp_cli-0.8.0/dp_cli/commands/batch.py +178 -0
  12. dp_cli-0.8.0/dp_cli/commands/browser.py +866 -0
  13. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/element.py +72 -24
  14. dp_cli-0.8.0/dp_cli/commands/emulate.py +431 -0
  15. dp_cli-0.8.0/dp_cli/commands/extract.py +124 -0
  16. dp_cli-0.8.0/dp_cli/commands/hook.py +437 -0
  17. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/keyboard.py +19 -5
  18. dp_cli-0.8.0/dp_cli/commands/meta.py +111 -0
  19. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/misc.py +84 -18
  20. dp_cli-0.8.0/dp_cli/commands/network.py +476 -0
  21. dp_cli-0.8.0/dp_cli/commands/observe.py +167 -0
  22. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/page.py +184 -10
  23. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/record.py +24 -8
  24. dp_cli-0.8.0/dp_cli/commands/rpc.py +204 -0
  25. dp_cli-0.8.0/dp_cli/commands/serve.py +162 -0
  26. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/snapshot_cmd.py +144 -36
  27. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/storage.py +121 -8
  28. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/tab.py +4 -4
  29. dp_cli-0.8.0/dp_cli/human.py +116 -0
  30. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/locators/__init__.py +2 -1
  31. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/locators/pw_js.py +26 -6
  32. dp_cli-0.8.0/dp_cli/locators/suggest.py +214 -0
  33. dp_cli-0.8.0/dp_cli/logs.py +38 -0
  34. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/main.py +19 -2
  35. dp_cli-0.8.0/dp_cli/output.py +205 -0
  36. dp_cli-0.8.0/dp_cli/pw_ts.py +105 -0
  37. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/recorder.py +189 -16
  38. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/session.py +114 -17
  39. dp_cli-0.8.0/dp_cli/skill_dist.py +217 -0
  40. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/a11y.py +108 -39
  41. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/clickable.py +4 -2
  42. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/stealth.py +212 -2
  43. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/PKG-INFO +23 -3
  44. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/SOURCES.txt +38 -1
  45. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/requires.txt +7 -0
  46. {dp_cli-0.6.3 → dp_cli-0.8.0}/pyproject.toml +8 -1
  47. dp_cli-0.8.0/setup.py +24 -0
  48. dp_cli-0.8.0/skills/dp-cli/SKILL.md +553 -0
  49. dp_cli-0.8.0/skills/dp-cli/references/auto-connect-gnome.sh +165 -0
  50. dp_cli-0.8.0/skills/dp-cli/references/commands.md +443 -0
  51. dp_cli-0.8.0/skills/dp-cli/references/extraction.md +82 -0
  52. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_a11y.py +141 -0
  53. dp_cli-0.8.0/tests/test_assert_cmd.py +314 -0
  54. dp_cli-0.8.0/tests/test_batch.py +218 -0
  55. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_bridge_integration.py +1 -0
  56. dp_cli-0.8.0/tests/test_browser_cdp.py +563 -0
  57. dp_cli-0.8.0/tests/test_browser_smoke.py +59 -0
  58. dp_cli-0.8.0/tests/test_commands.py +2913 -0
  59. dp_cli-0.8.0/tests/test_compact.py +136 -0
  60. dp_cli-0.8.0/tests/test_docs.py +224 -0
  61. dp_cli-0.8.0/tests/test_event_wait.py +263 -0
  62. dp_cli-0.8.0/tests/test_expect_gen.py +345 -0
  63. dp_cli-0.8.0/tests/test_hook.py +384 -0
  64. dp_cli-0.8.0/tests/test_human.py +324 -0
  65. dp_cli-0.8.0/tests/test_pw_ts.py +194 -0
  66. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_recorder.py +193 -1
  67. dp_cli-0.8.0/tests/test_rpc.py +443 -0
  68. dp_cli-0.8.0/tests/test_serve.py +173 -0
  69. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_session.py +124 -1
  70. dp_cli-0.8.0/tests/test_skill_dist.py +314 -0
  71. dp_cli-0.8.0/tests/test_trim.py +215 -0
  72. dp_cli-0.6.3/dp_cli/commands/browser.py +0 -336
  73. dp_cli-0.6.3/dp_cli/commands/network.py +0 -175
  74. dp_cli-0.6.3/dp_cli/output.py +0 -64
  75. dp_cli-0.6.3/tests/test_commands.py +0 -889
  76. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/__init__.py +0 -0
  77. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/locators/playwright.py +0 -0
  78. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/__init__.py +0 -0
  79. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/clickable_js.py +0 -0
  80. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/extract.py +0 -0
  81. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/js_scripts.py +0 -0
  82. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/utils.py +0 -0
  83. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/dependency_links.txt +0 -0
  84. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/entry_points.txt +0 -0
  85. {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/top_level.txt +0 -0
  86. {dp_cli-0.6.3 → dp_cli-0.8.0}/setup.cfg +0 -0
  87. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_bridge_manager.py +0 -0
  88. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_clickable.py +0 -0
  89. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_pw_locator.py +0 -0
  90. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_resolve_locator.py +0 -0
  91. {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_snapshot_small.py +0 -0
dp_cli-0.8.0/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, mofanx
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ graft skills
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dp-cli
3
- Version: 0.6.3
3
+ Version: 0.8.0
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
@@ -13,16 +13,23 @@ Classifier: Topic :: Utilities
13
13
  Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
14
14
  Requires-Python: >=3.8
15
15
  Description-Content-Type: text/markdown
16
+ License-File: LICENSE
16
17
  Requires-Dist: DrissionPage>=4.0
17
18
  Requires-Dist: click>=8.0
18
19
  Requires-Dist: aiohttp>=3.9
19
20
  Requires-Dist: websockets>=12
20
21
  Requires-Dist: requests>=2.28
22
+ Provides-Extra: tls
23
+ Requires-Dist: curl_cffi>=0.7.0; extra == "tls"
24
+ Provides-Extra: extract
25
+ Requires-Dist: trafilatura>=1.6.0; extra == "extract"
26
+ Requires-Dist: readability-lxml>=0.8.1; extra == "extract"
21
27
  Provides-Extra: test
22
28
  Requires-Dist: pytest>=7.4.0; extra == "test"
23
29
  Requires-Dist: pytest-cov>=4.1.0; extra == "test"
24
30
  Requires-Dist: pytest-timeout>=2.1.0; extra == "test"
25
31
  Requires-Dist: pytest-mock>=3.11.0; extra == "test"
32
+ Dynamic: license-file
26
33
 
27
34
  # dp-cli
28
35
 
@@ -52,6 +59,18 @@ pip install dp-cli
52
59
  dp --help
53
60
  ```
54
61
 
62
+ ### Agent skill(可选,推荐)
63
+
64
+ 为 AI agent 安装完整使用指南(全部命令参考 + 工作流 playbook):
65
+
66
+ ```bash
67
+ dp install --skills # 装到 agent 的 skill 目录
68
+ npx skills add mofanx/dp-cli # 或走 skills CLI 渠道
69
+ claude plugin marketplace add mofanx/dp-cli # 或 Claude Code 插件市场
70
+ ```
71
+
72
+ 未安装也能用——skill 只是文档层;agent 可直接读 `dp --help` 或 `python -m dp_cli.catalog` 自举。
73
+
55
74
  ## Quick Start
56
75
 
57
76
  ```bash
@@ -278,7 +297,8 @@ dp_cli/
278
297
  ├── snapshot_cmd.py # snapshot / extract / query / find / inspect
279
298
  ├── element.py # click / fill / select / hover / drag / check / upload / count
280
299
  ├── keyboard.py # press / type / scroll / scroll-to / autoscroll
281
- ├── page.py # screenshot / pdf / eval / wait (idle/loaded/url/title) / dialog
300
+ ├── page.py # screenshot / pdf / eval / wait / console / dialog
301
+ ├── assert_cmd.py # assert-text / assert-url / assert-visible / assert-count
282
302
  ├── tab.py # tab-list / tab-new / tab-select / tab-close
283
303
  ├── storage.py # cookie-* / localstorage-* / sessionstorage-*
284
304
  ├── network.py # listen / listen-stop / http-get / http-post
@@ -287,7 +307,7 @@ dp_cli/
287
307
 
288
308
  ## Documentation
289
309
 
290
- See [`skills/SKILL.md`](skills/SKILL.md) for full workflow guide and [`skills/references/commands.md`](skills/references/commands.md) for complete command reference.
310
+ See [`skills/dp-cli/SKILL.md`](skills/dp-cli/SKILL.md) for full workflow guide and [`skills/dp-cli/references/commands.md`](skills/dp-cli/references/commands.md) for complete command reference. Exit-code contract: [`ERROR_CODES.md`](ERROR_CODES.md).
291
311
 
292
312
  ## License
293
313
 
@@ -26,6 +26,18 @@ pip install dp-cli
26
26
  dp --help
27
27
  ```
28
28
 
29
+ ### Agent skill(可选,推荐)
30
+
31
+ 为 AI agent 安装完整使用指南(全部命令参考 + 工作流 playbook):
32
+
33
+ ```bash
34
+ dp install --skills # 装到 agent 的 skill 目录
35
+ npx skills add mofanx/dp-cli # 或走 skills CLI 渠道
36
+ claude plugin marketplace add mofanx/dp-cli # 或 Claude Code 插件市场
37
+ ```
38
+
39
+ 未安装也能用——skill 只是文档层;agent 可直接读 `dp --help` 或 `python -m dp_cli.catalog` 自举。
40
+
29
41
  ## Quick Start
30
42
 
31
43
  ```bash
@@ -252,7 +264,8 @@ dp_cli/
252
264
  ├── snapshot_cmd.py # snapshot / extract / query / find / inspect
253
265
  ├── element.py # click / fill / select / hover / drag / check / upload / count
254
266
  ├── keyboard.py # press / type / scroll / scroll-to / autoscroll
255
- ├── page.py # screenshot / pdf / eval / wait (idle/loaded/url/title) / dialog
267
+ ├── page.py # screenshot / pdf / eval / wait / console / dialog
268
+ ├── assert_cmd.py # assert-text / assert-url / assert-visible / assert-count
256
269
  ├── tab.py # tab-list / tab-new / tab-select / tab-close
257
270
  ├── storage.py # cookie-* / localstorage-* / sessionstorage-*
258
271
  ├── network.py # listen / listen-stop / http-get / http-post
@@ -261,7 +274,7 @@ dp_cli/
261
274
 
262
275
  ## Documentation
263
276
 
264
- See [`skills/SKILL.md`](skills/SKILL.md) for full workflow guide and [`skills/references/commands.md`](skills/references/commands.md) for complete command reference.
277
+ See [`skills/dp-cli/SKILL.md`](skills/dp-cli/SKILL.md) for full workflow guide and [`skills/dp-cli/references/commands.md`](skills/dp-cli/references/commands.md) for complete command reference. Exit-code contract: [`ERROR_CODES.md`](ERROR_CODES.md).
265
278
 
266
279
  ## License
267
280
 
@@ -97,14 +97,14 @@ class CDPBridge:
97
97
  for ws in list(self._browser_clients):
98
98
  try:
99
99
  await ws.close(code=1011, message=b'upstream closed')
100
- except Exception:
101
- pass
100
+ except Exception as e:
101
+ log.debug('close downstream ws failed: %s', e)
102
102
  self._browser_clients.clear()
103
103
  for sid, ws in list(self._session_to_client.items()):
104
104
  try:
105
105
  await ws.close(code=1011, message=b'upstream closed')
106
- except Exception:
107
- pass
106
+ except Exception as e:
107
+ log.debug('close session ws failed: %s', e)
108
108
  self._session_to_client.clear()
109
109
  self.ws = None
110
110
  self._connected_evt.clear()
@@ -144,8 +144,8 @@ class CDPBridge:
144
144
  try:
145
145
  if not client_ws.closed:
146
146
  await client_ws.send_str(json.dumps(ev))
147
- except Exception:
148
- pass
147
+ except Exception as e:
148
+ log.debug('forward event to client failed: %s', e)
149
149
  return
150
150
 
151
151
  if not sess_id:
@@ -154,8 +154,8 @@ class CDPBridge:
154
154
  try:
155
155
  if not c.closed:
156
156
  await c.send_str(ev_str)
157
- except Exception:
158
- pass
157
+ except Exception as e:
158
+ log.debug('broadcast event failed: %s', e)
159
159
 
160
160
  async def call(self, method: str, params: dict | None = None,
161
161
  session_id: str | None = None) -> dict:
@@ -200,8 +200,8 @@ class CDPBridge:
200
200
  err = {'id': client_id, 'error': {'code': -32000, 'message': str(e)}}
201
201
  try:
202
202
  await client_ws.send_str(json.dumps(err))
203
- except Exception:
204
- pass
203
+ except Exception as e2:
204
+ log.debug('send error reply failed: %s', e2)
205
205
 
206
206
  async def attach_page_client(self, target_id: str,
207
207
  client_ws: web.WebSocketResponse) -> str | None:
@@ -386,8 +386,8 @@ async def handle_ws(request: web.Request) -> web.StreamResponse:
386
386
  try:
387
387
  payload = json.loads(msg.data)
388
388
  await bridge.forward_from_client(payload, client_ws, session_id)
389
- except Exception:
390
- pass
389
+ except Exception as e:
390
+ log.debug('forward client message failed: %s', e)
391
391
  elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED,
392
392
  aiohttp.WSMsgType.ERROR):
393
393
  break
@@ -479,14 +479,14 @@ async def main_async(user_data_dir: Path, host: str, port: int) -> None:
479
479
  continue
480
480
  try:
481
481
  signal.signal(sig, _win_signal_handler)
482
- except (ValueError, OSError):
483
- pass
482
+ except (ValueError, OSError) as e:
483
+ log.debug('register signal handler failed: %s', e)
484
484
  else:
485
485
  for sig in (signal.SIGINT, signal.SIGTERM):
486
486
  try:
487
487
  loop.add_signal_handler(sig, stop_evt.set)
488
- except NotImplementedError:
489
- pass
488
+ except NotImplementedError as e:
489
+ log.debug('add_signal_handler unsupported: %s', e)
490
490
  try:
491
491
  await stop_evt.wait()
492
492
  finally:
@@ -24,6 +24,8 @@ import sys
24
24
  import time
25
25
  from pathlib import Path
26
26
 
27
+ from dp_cli.logs import debug
28
+
27
29
  IS_WINDOWS = sys.platform == 'win32'
28
30
 
29
31
 
@@ -74,8 +76,8 @@ def _win_terminate(pid: int) -> None:
74
76
  check=False,
75
77
  timeout=5,
76
78
  )
77
- except Exception:
78
- pass
79
+ except Exception as e:
80
+ debug(f'taskkill pid={pid} 失败', e)
79
81
 
80
82
 
81
83
  _READY_RE = re.compile(r'^BRIDGE_READY host=(?P<host>\S+) port=(?P<port>\d+)\s*$')
@@ -113,8 +115,8 @@ def detect_inspect_mode(port: int, timeout: float = 2.0) -> bool:
113
115
  # 经典模式 /json/version 返回 dict,含 Browser 字段
114
116
  if isinstance(data, dict) and 'Browser' in data:
115
117
  return False
116
- except Exception:
117
- pass
118
+ except Exception as e:
119
+ debug(f'/json/version 响应解析失败,按 inspect 模式处理: port={port}', e)
118
120
  return True
119
121
 
120
122
 
@@ -166,8 +168,8 @@ def start_bridge(user_data_dir: str | os.PathLike,
166
168
  for line in proc.stderr:
167
169
  with stderr_lock:
168
170
  stderr_buf.append(line)
169
- except Exception:
170
- pass
171
+ except Exception as e:
172
+ debug('bridge stderr 读取线程结束异常', e)
171
173
 
172
174
  stderr_thread = threading.Thread(target=_read_stderr, daemon=True)
173
175
  stderr_thread.start()
@@ -207,8 +209,8 @@ def start_bridge(user_data_dir: str | os.PathLike,
207
209
  except Exception:
208
210
  try:
209
211
  proc.terminate()
210
- except Exception:
211
- pass
212
+ except Exception as e:
213
+ debug('bridge 启动失败后 terminate 失败', e)
212
214
  raise
213
215
 
214
216
  # ready 后让 bridge 的 stdout 继续被 drain,避免管道阻塞。
@@ -217,8 +219,8 @@ def start_bridge(user_data_dir: str | os.PathLike,
217
219
  try:
218
220
  for _ in stream:
219
221
  pass
220
- except Exception:
221
- pass
222
+ except Exception as e:
223
+ debug('stdout drain 线程结束异常', e)
222
224
 
223
225
  if proc.stdout:
224
226
  threading.Thread(target=_drain, args=(proc.stdout,), daemon=True).start()
@@ -256,8 +258,8 @@ def stop_bridge(pid: int, timeout: float = 2.0) -> bool:
256
258
  # 1) 优雅: 给整个进程组发 CTRL_BREAK_EVENT
257
259
  try:
258
260
  os.kill(pid, signal.CTRL_BREAK_EVENT)
259
- except Exception:
260
- pass
261
+ except Exception as e:
262
+ debug(f'CTRL_BREAK_EVENT pid={pid} 失败', e)
261
263
 
262
264
  deadline = time.monotonic() + timeout
263
265
  while time.monotonic() < deadline:
@@ -276,8 +278,8 @@ def stop_bridge(pid: int, timeout: float = 2.0) -> bool:
276
278
  except (ProcessLookupError, PermissionError, OSError):
277
279
  try:
278
280
  os.kill(pid, signal.SIGTERM)
279
- except Exception:
280
- pass
281
+ except Exception as e:
282
+ debug(f'SIGTERM pid={pid} 失败', e)
281
283
 
282
284
  deadline = time.monotonic() + timeout
283
285
  while time.monotonic() < deadline:
@@ -291,7 +293,7 @@ def stop_bridge(pid: int, timeout: float = 2.0) -> bool:
291
293
  except Exception:
292
294
  try:
293
295
  os.kill(pid, signal.SIGKILL)
294
- except Exception:
295
- pass
296
+ except Exception as e:
297
+ debug(f'SIGKILL pid={pid} 失败', e)
296
298
  time.sleep(0.1)
297
299
  return not is_bridge_alive(pid)
@@ -0,0 +1,109 @@
1
+ # -*- coding:utf-8 -*-
2
+ """从 click 命令树机械导出命令元数据。
3
+
4
+ 两类消费方:
5
+ - ``python -m dp_cli.catalog --md`` → 生成 commands.md 风格的命令表
6
+ - ``python -m dp_cli.catalog`` → 输出 MCP 风格 tool schema JSON
7
+ (未来 mcp-serve 薄壳的上游;schema 由代码生成,不手写,不腐化)
8
+
9
+ 文档腐化守卫见 tests/test_docs.py:commands.md 必须覆盖全部注册命令。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+
17
+ import click
18
+
19
+ from dp_cli.main import cli
20
+
21
+ # 模块名 → 命令表类别(按模块机械归类,新增模块时在此补一行)
22
+ CATEGORY = {
23
+ 'browser': '浏览器', 'misc': '导航/页面', 'snapshot_cmd': '快照',
24
+ 'element': '提取/交互', 'keyboard': '键盘/滚动', 'network': '网络',
25
+ 'assert_cmd': '断言', 'emulate': '模拟/下载/拦截', 'observe': '观测',
26
+ 'tab': '标签页', 'storage': '存储', 'record': '录制',
27
+ 'page': '页面', 'batch': '批量', 'rpc': '逆向',
28
+ 'extract': '提取', 'hook': '逆向', 'serve': '批量', 'sh': '批量',
29
+ 'meta': '元信息',
30
+ }
31
+
32
+
33
+ def _param_schema(p):
34
+ """click.Param → JSON Schema 片段。"""
35
+ if isinstance(p, click.Option) and p.is_flag:
36
+ t = {'type': 'boolean'}
37
+ elif isinstance(p.type, click.types.IntParamType):
38
+ t = {'type': 'integer'}
39
+ elif isinstance(p.type, click.types.FloatParamType):
40
+ t = {'type': 'number'}
41
+ elif isinstance(p.type, click.types.BoolParamType):
42
+ t = {'type': 'boolean'}
43
+ elif isinstance(p.type, click.types.Choice):
44
+ t = {'type': 'string', 'enum': list(p.type.choices)}
45
+ else:
46
+ t = {'type': 'string'}
47
+ if getattr(p, 'multiple', False):
48
+ t = {'type': 'array', 'items': t}
49
+ if getattr(p, 'help', None):
50
+ t['description'] = p.help
51
+ return t
52
+
53
+
54
+ def iter_commands():
55
+ """yield (name, help, input_schema) —— MCP tool 风格。"""
56
+ for name in sorted(cli.commands):
57
+ cmd = cli.commands[name]
58
+ props, required = {}, []
59
+ for p in cmd.params:
60
+ props[p.name] = _param_schema(p)
61
+ if p.required:
62
+ required.append(p.name)
63
+ yield {
64
+ 'name': name,
65
+ 'description': (cmd.help or '').strip().splitlines()[0] if cmd.help else '',
66
+ 'inputSchema': {'type': 'object', 'properties': props,
67
+ 'required': required},
68
+ '_category': CATEGORY.get(
69
+ getattr(cmd.callback, '__module__', '').rsplit('.', 1)[-1],
70
+ '其他'),
71
+ }
72
+
73
+
74
+ def to_markdown(cmds):
75
+ """生成按类别分组的 markdown 表(含 options 摘要)。"""
76
+ by_cat = {}
77
+ for c in cmds:
78
+ by_cat.setdefault(c['_category'], []).append(c)
79
+ lines = ['<!-- GENERATED by `python -m dp_cli.catalog --md` —— 勿手改 -->',
80
+ '', '| 类别 | 命令 | 参数 |', '|------|------|------|']
81
+ for cat in CATEGORY.values():
82
+ for c in by_cat.pop(cat, []):
83
+ params = ', '.join(
84
+ (f'<{k}>' if k in c['inputSchema']['required'] else f'[{k}]')
85
+ for k in c['inputSchema']['properties'])
86
+ lines.append(f"| {cat} | `{c['name']}` | {params} |")
87
+ for cat, cs in sorted(by_cat.items()):
88
+ for c in cs:
89
+ params = ', '.join(c['inputSchema']['properties'])
90
+ lines.append(f"| {cat} | `{c['name']}` | {params} |")
91
+ return '\n'.join(lines) + '\n'
92
+
93
+
94
+ def main(argv=None):
95
+ ap = argparse.ArgumentParser(prog='dp-cli-catalog')
96
+ ap.add_argument('--md', action='store_true', help='输出 markdown 命令表')
97
+ args = ap.parse_args(argv)
98
+ cmds = list(iter_commands())
99
+ if args.md:
100
+ sys.stdout.write(to_markdown(cmds))
101
+ else:
102
+ tools = [{k: v for k, v in c.items() if not k.startswith('_')}
103
+ for c in cmds]
104
+ json.dump({'tools': tools}, sys.stdout, ensure_ascii=False, indent=2)
105
+ sys.stdout.write('\n')
106
+
107
+
108
+ if __name__ == '__main__':
109
+ main()
@@ -2,9 +2,13 @@
2
2
  from dp_cli.commands import (
3
3
  browser, snapshot_cmd, element, keyboard,
4
4
  page, tab, storage, network, record, misc,
5
+ assert_cmd, emulate, observe, batch, rpc, extract, hook, serve,
6
+ meta,
5
7
  )
6
8
 
7
- _MODULES = [browser, snapshot_cmd, element, keyboard, page, tab, storage, network, record, misc]
9
+ _MODULES = [browser, snapshot_cmd, element, keyboard, page, tab, storage, network, record, misc,
10
+ assert_cmd, emulate, observe, batch, rpc, extract, hook, serve,
11
+ meta]
8
12
 
9
13
 
10
14
  def register_all(cli):