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.
- dp_cli-0.8.0/LICENSE +29 -0
- dp_cli-0.8.0/MANIFEST.in +1 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/PKG-INFO +23 -3
- {dp_cli-0.6.3 → dp_cli-0.8.0}/README.md +15 -2
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/bridge.py +16 -16
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/bridge_manager.py +18 -16
- dp_cli-0.8.0/dp_cli/catalog.py +109 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/__init__.py +5 -1
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/_utils.py +267 -5
- dp_cli-0.8.0/dp_cli/commands/assert_cmd.py +454 -0
- dp_cli-0.8.0/dp_cli/commands/batch.py +178 -0
- dp_cli-0.8.0/dp_cli/commands/browser.py +866 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/element.py +72 -24
- dp_cli-0.8.0/dp_cli/commands/emulate.py +431 -0
- dp_cli-0.8.0/dp_cli/commands/extract.py +124 -0
- dp_cli-0.8.0/dp_cli/commands/hook.py +437 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/keyboard.py +19 -5
- dp_cli-0.8.0/dp_cli/commands/meta.py +111 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/misc.py +84 -18
- dp_cli-0.8.0/dp_cli/commands/network.py +476 -0
- dp_cli-0.8.0/dp_cli/commands/observe.py +167 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/page.py +184 -10
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/record.py +24 -8
- dp_cli-0.8.0/dp_cli/commands/rpc.py +204 -0
- dp_cli-0.8.0/dp_cli/commands/serve.py +162 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/snapshot_cmd.py +144 -36
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/storage.py +121 -8
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/commands/tab.py +4 -4
- dp_cli-0.8.0/dp_cli/human.py +116 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/locators/__init__.py +2 -1
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/locators/pw_js.py +26 -6
- dp_cli-0.8.0/dp_cli/locators/suggest.py +214 -0
- dp_cli-0.8.0/dp_cli/logs.py +38 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/main.py +19 -2
- dp_cli-0.8.0/dp_cli/output.py +205 -0
- dp_cli-0.8.0/dp_cli/pw_ts.py +105 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/recorder.py +189 -16
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/session.py +114 -17
- dp_cli-0.8.0/dp_cli/skill_dist.py +217 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/a11y.py +108 -39
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/clickable.py +4 -2
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/stealth.py +212 -2
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/PKG-INFO +23 -3
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/SOURCES.txt +38 -1
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/requires.txt +7 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/pyproject.toml +8 -1
- dp_cli-0.8.0/setup.py +24 -0
- dp_cli-0.8.0/skills/dp-cli/SKILL.md +553 -0
- dp_cli-0.8.0/skills/dp-cli/references/auto-connect-gnome.sh +165 -0
- dp_cli-0.8.0/skills/dp-cli/references/commands.md +443 -0
- dp_cli-0.8.0/skills/dp-cli/references/extraction.md +82 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_a11y.py +141 -0
- dp_cli-0.8.0/tests/test_assert_cmd.py +314 -0
- dp_cli-0.8.0/tests/test_batch.py +218 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_bridge_integration.py +1 -0
- dp_cli-0.8.0/tests/test_browser_cdp.py +563 -0
- dp_cli-0.8.0/tests/test_browser_smoke.py +59 -0
- dp_cli-0.8.0/tests/test_commands.py +2913 -0
- dp_cli-0.8.0/tests/test_compact.py +136 -0
- dp_cli-0.8.0/tests/test_docs.py +224 -0
- dp_cli-0.8.0/tests/test_event_wait.py +263 -0
- dp_cli-0.8.0/tests/test_expect_gen.py +345 -0
- dp_cli-0.8.0/tests/test_hook.py +384 -0
- dp_cli-0.8.0/tests/test_human.py +324 -0
- dp_cli-0.8.0/tests/test_pw_ts.py +194 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_recorder.py +193 -1
- dp_cli-0.8.0/tests/test_rpc.py +443 -0
- dp_cli-0.8.0/tests/test_serve.py +173 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_session.py +124 -1
- dp_cli-0.8.0/tests/test_skill_dist.py +314 -0
- dp_cli-0.8.0/tests/test_trim.py +215 -0
- dp_cli-0.6.3/dp_cli/commands/browser.py +0 -336
- dp_cli-0.6.3/dp_cli/commands/network.py +0 -175
- dp_cli-0.6.3/dp_cli/output.py +0 -64
- dp_cli-0.6.3/tests/test_commands.py +0 -889
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/__init__.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/locators/playwright.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/__init__.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/clickable_js.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/extract.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/js_scripts.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli/snapshot/utils.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/dependency_links.txt +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/entry_points.txt +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/dp_cli.egg-info/top_level.txt +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/setup.cfg +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_bridge_manager.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_clickable.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_pw_locator.py +0 -0
- {dp_cli-0.6.3 → dp_cli-0.8.0}/tests/test_resolve_locator.py +0 -0
- {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.
|
dp_cli-0.8.0/MANIFEST.in
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
graft skills
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dp-cli
|
|
3
|
-
Version: 0.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|