navcli 0.1.0__py3-none-any.whl
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.
- navcli/cli/__init__.py +5 -0
- navcli/cli/app.py +84 -0
- navcli/cli/client.py +271 -0
- navcli/cli/commands/__init__.py +0 -0
- navcli/cli/commands/base.py +46 -0
- navcli/cli/commands/control.py +119 -0
- navcli/cli/commands/explore.py +125 -0
- navcli/cli/commands/interaction.py +129 -0
- navcli/cli/commands/navigation.py +90 -0
- navcli/cli/commands/query.py +171 -0
- navcli/core/__init__.py +0 -0
- navcli/core/models/__init__.py +15 -0
- navcli/core/models/dom.py +33 -0
- navcli/core/models/element.py +22 -0
- navcli/core/models/feedback.py +24 -0
- navcli/core/models/state.py +19 -0
- navcli/server/__init__.py +86 -0
- navcli/server/app.py +48 -0
- navcli/server/browser.py +373 -0
- navcli/server/middleware/__init__.py +0 -0
- navcli/server/routes/__init__.py +11 -0
- navcli/server/routes/control.py +44 -0
- navcli/server/routes/explore.py +382 -0
- navcli/server/routes/interaction.py +317 -0
- navcli/server/routes/navigation.py +133 -0
- navcli/server/routes/query.py +303 -0
- navcli/server/routes/session.py +177 -0
- navcli/utils/__init__.py +20 -0
- navcli/utils/image.py +30 -0
- navcli/utils/js.py +13 -0
- navcli/utils/selector.py +88 -0
- navcli/utils/text.py +17 -0
- navcli/utils/time.py +46 -0
- navcli/utils/url.py +16 -0
- navcli-0.1.0.dist-info/METADATA +79 -0
- navcli-0.1.0.dist-info/RECORD +39 -0
- navcli-0.1.0.dist-info/WHEEL +5 -0
- navcli-0.1.0.dist-info/entry_points.txt +3 -0
- navcli-0.1.0.dist-info/top_level.txt +1 -0
navcli/utils/selector.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""CSS selector parsing and building utilities."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_selector(selector: str) -> dict:
|
|
8
|
+
"""Parse a CSS selector into components.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
selector: CSS selector string
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Dict with tag, id, classes, attrs
|
|
15
|
+
"""
|
|
16
|
+
result = {
|
|
17
|
+
'tag': None,
|
|
18
|
+
'id': None,
|
|
19
|
+
'classes': [],
|
|
20
|
+
'attrs': {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Match tag
|
|
24
|
+
tag_match = re.match(r'^([a-zA-Z][a-zA-Z0-9-]*)', selector)
|
|
25
|
+
if tag_match:
|
|
26
|
+
result['tag'] = tag_match.group(1)
|
|
27
|
+
selector = selector[tag_match.end():]
|
|
28
|
+
|
|
29
|
+
# Match id
|
|
30
|
+
id_match = re.match(r'#([a-zA-Z0-9_-]+)', selector)
|
|
31
|
+
if id_match:
|
|
32
|
+
result['id'] = id_match.group(1)
|
|
33
|
+
selector = selector[id_match.end():]
|
|
34
|
+
|
|
35
|
+
# Match classes
|
|
36
|
+
class_matches = re.findall(r'\.([a-zA-Z0-9_-]+)', selector)
|
|
37
|
+
result['classes'] = class_matches
|
|
38
|
+
|
|
39
|
+
# Match attributes
|
|
40
|
+
attr_matches = re.findall(r'\[([^\]]+)\]', selector)
|
|
41
|
+
for attr in attr_matches:
|
|
42
|
+
if '=' in attr:
|
|
43
|
+
key, value = attr.split('=', 1)
|
|
44
|
+
result['attrs'][key] = value.strip('"\'')
|
|
45
|
+
else:
|
|
46
|
+
# Attribute without value (e.g., [disabled])
|
|
47
|
+
result['attrs'][attr] = ''
|
|
48
|
+
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_selector(
|
|
53
|
+
tag: Optional[str] = None,
|
|
54
|
+
id: Optional[str] = None,
|
|
55
|
+
classes: Optional[list] = None,
|
|
56
|
+
attrs: Optional[dict] = None,
|
|
57
|
+
text: Optional[str] = None
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Build a CSS selector from components.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
tag: HTML tag name
|
|
63
|
+
id: Element id
|
|
64
|
+
classes: List of class names
|
|
65
|
+
attrs: Dict of attributes
|
|
66
|
+
text: Text content (used for :text pseudo-selector)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
CSS selector string
|
|
70
|
+
"""
|
|
71
|
+
parts = []
|
|
72
|
+
|
|
73
|
+
if tag:
|
|
74
|
+
parts.append(tag)
|
|
75
|
+
if id:
|
|
76
|
+
parts.append(f'#{id}')
|
|
77
|
+
if classes:
|
|
78
|
+
parts.extend(f'.{c}' for c in classes)
|
|
79
|
+
if attrs:
|
|
80
|
+
for k, v in attrs.items():
|
|
81
|
+
if v:
|
|
82
|
+
parts.append(f'[{k}="{v}"]')
|
|
83
|
+
else:
|
|
84
|
+
parts.append(f'[{k}]')
|
|
85
|
+
if text:
|
|
86
|
+
parts.append(f':text-is("{text}")')
|
|
87
|
+
|
|
88
|
+
return ''.join(parts) or '*'
|
navcli/utils/text.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Text processing utilities."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_key_value(s: str, sep: str = '=') -> tuple:
|
|
5
|
+
"""Parse a key-value pair from string.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
s: String in format "key=value"
|
|
9
|
+
sep: Separator character
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
Tuple of (key, value)
|
|
13
|
+
"""
|
|
14
|
+
if sep in s:
|
|
15
|
+
key, value = s.split(sep, 1)
|
|
16
|
+
return key.strip(), value.strip()
|
|
17
|
+
return s.strip(), ''
|
navcli/utils/time.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Time and timeout utilities."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def wait_for_condition(
|
|
8
|
+
condition: Callable[[], bool],
|
|
9
|
+
timeout: float = 10.0,
|
|
10
|
+
interval: float = 0.1,
|
|
11
|
+
error_msg: str = "Condition not met"
|
|
12
|
+
) -> bool:
|
|
13
|
+
"""Wait for a condition to become true.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
condition: Callable that returns True when condition is met
|
|
17
|
+
timeout: Maximum time to wait in seconds
|
|
18
|
+
interval: Polling interval in seconds
|
|
19
|
+
error_msg: Error message for timeout
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
True if condition was met
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
TimeoutError: If condition is not met within timeout
|
|
26
|
+
"""
|
|
27
|
+
start = time.time()
|
|
28
|
+
while time.time() - start < timeout:
|
|
29
|
+
if condition():
|
|
30
|
+
return True
|
|
31
|
+
time.sleep(interval)
|
|
32
|
+
raise TimeoutError(f"{error_msg} within {timeout}s")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def format_timeout(seconds: float) -> str:
|
|
36
|
+
"""Format seconds into human readable timeout.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
seconds: Time in seconds
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Formatted string like "3s" or "500ms"
|
|
43
|
+
"""
|
|
44
|
+
if seconds >= 1:
|
|
45
|
+
return f"{int(seconds)}s" if seconds == int(seconds) else f"{seconds}s"
|
|
46
|
+
return f"{int(seconds * 1000)}ms"
|
navcli/utils/url.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""URL utilities."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def normalize_url(url: str) -> str:
|
|
5
|
+
"""Normalize a URL.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
url: URL to normalize
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
Normalized URL with scheme
|
|
12
|
+
"""
|
|
13
|
+
url = url.strip()
|
|
14
|
+
if not url.startswith(('http://', 'https://')):
|
|
15
|
+
url = 'https://' + url
|
|
16
|
+
return url
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: navcli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 可交互、可探索的浏览器命令行工具,专为 AI Agent 设计
|
|
5
|
+
Author: NavCLI Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/navcli/navcli
|
|
8
|
+
Project-URL: Repository, https://github.com/navcli/navcli
|
|
9
|
+
Project-URL: Issues, https://github.com/navcli/navcli/issues
|
|
10
|
+
Keywords: browser,cli,automation,ai-agent,playwright
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: playwright>=1.40.0
|
|
22
|
+
Requires-Dist: cmd2>=2.4.0
|
|
23
|
+
Requires-Dist: fastapi>=0.109.0
|
|
24
|
+
Requires-Dist: uvicorn<1.0.0,>=0.25.0
|
|
25
|
+
Requires-Dist: pydantic>=2.5.0
|
|
26
|
+
Requires-Dist: cssify>=1.0.0
|
|
27
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# NavCLI - 目标与愿景
|
|
35
|
+
|
|
36
|
+
## 核心目标
|
|
37
|
+
|
|
38
|
+
**让 AI Agent 能够像人类一样浏览网页。**
|
|
39
|
+
|
|
40
|
+
现有方案(HTTP API、无头浏览器脚本、Playwright MCP)存在局限:
|
|
41
|
+
- 不支持 JS 渲染的 SPA
|
|
42
|
+
- 无会话状态持久化
|
|
43
|
+
- 缺乏交互式探索能力
|
|
44
|
+
|
|
45
|
+
NavCLI 的定位:**可交互、可探索的浏览器命令行工具**
|
|
46
|
+
|
|
47
|
+
## 核心价值
|
|
48
|
+
|
|
49
|
+
| 特性 | NavCLI 解决的问题 |
|
|
50
|
+
|------|------------------|
|
|
51
|
+
| JS 渲染支持 | 完整支持 SPA 应用 |
|
|
52
|
+
| 会话持久化 | cookies、session 保持 |
|
|
53
|
+
| 交互式 CLI | Agent 可边探索边操作 |
|
|
54
|
+
| Token 优化 | 轻量 elements + 按需 text/html |
|
|
55
|
+
|
|
56
|
+
## 典型工作流
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
> g https://example.com # 导航
|
|
60
|
+
> elements # 查看可操作元素
|
|
61
|
+
> c .btn-login # 点击登录
|
|
62
|
+
> t #email "test@example.com" # 输入邮箱
|
|
63
|
+
> t #password "123456" # 输入密码
|
|
64
|
+
> c button[type="submit"] # 提交
|
|
65
|
+
> text # 确认结果
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Agent 可以:**导航 → 观察 → 交互 → 反馈 → 继续**
|
|
69
|
+
|
|
70
|
+
## 愿景
|
|
71
|
+
|
|
72
|
+
成为 AI Agent 的**标准浏览器交互层**,让任何 Agent 都能通过命令式界面控制浏览器,完成:
|
|
73
|
+
- 表单填写、登录认证
|
|
74
|
+
- 信息抓取、内容探索
|
|
75
|
+
- 复杂多步业务流程
|
|
76
|
+
|
|
77
|
+
## 相关文档
|
|
78
|
+
|
|
79
|
+
- [PRD 产品需求文档](./NAVCLI_PRD.md)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
navcli/cli/__init__.py,sha256=SE3EeeJ5Hku0o_rbXHVG3cPDxZaLkrLVcH57wFJOtXI,117
|
|
2
|
+
navcli/cli/app.py,sha256=wYEVsUjp4BE_MJDu2v75av1bT5x7EvF4_ssdpH2Q2VQ,3072
|
|
3
|
+
navcli/cli/client.py,sha256=qYdMNMLnXlsMq0bkKJgAiXHpxnqTErehjAF0GRRAG7E,9138
|
|
4
|
+
navcli/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
navcli/cli/commands/base.py,sha256=b8hsB43lUaD0b-5nErtunBGPOFPfKq_sdFEMmynjFRE,1596
|
|
6
|
+
navcli/cli/commands/control.py,sha256=JbAUNXgxZFmEO96TbhtS2CHw2jH2HZUk3EOIU_RwUBY,3253
|
|
7
|
+
navcli/cli/commands/explore.py,sha256=p1p4i2Z-GISmFIikdpQ0mEnpPArSKVA3OIo1Xu42Njg,3664
|
|
8
|
+
navcli/cli/commands/interaction.py,sha256=39CSeWW-Sb_z1w3i2kkmvJ515DL7WLdJPGCwUpri4aQ,3701
|
|
9
|
+
navcli/cli/commands/navigation.py,sha256=ksIyoaCFzkF076C4K8qbl4OI-15UWOjhd47Hm7dGAkI,2265
|
|
10
|
+
navcli/cli/commands/query.py,sha256=zWp-tiCr6xGeIlrsaK1ufoBOTk5L5n8CzGtkIkcnRAs,4630
|
|
11
|
+
navcli/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
navcli/core/models/__init__.py,sha256=-LEFzktT5xBaOMa_UXlNR_c0O9gVTfv5Bp-3IZb43Q4,295
|
|
13
|
+
navcli/core/models/dom.py,sha256=vZzEpzLEOlCnUJWBWx0-gmHj8WBxorsnPsZJgUW3UOE,1160
|
|
14
|
+
navcli/core/models/element.py,sha256=6L8PnY7h_-_MFweq6NU6gdG1g5_r_AHlW1bYTuUBNoM,747
|
|
15
|
+
navcli/core/models/feedback.py,sha256=K67nc7fCiAAfeYKDppNEnHtM41fSdF-Ai7IMhvAv7PY,689
|
|
16
|
+
navcli/core/models/state.py,sha256=iyn6GvGpONGRP6WwiE0PYird4owJleK03Gk-jWExfGg,657
|
|
17
|
+
navcli/server/__init__.py,sha256=l7As1ZOyGRO_BX5agyhirw8XNm8Jc6qDez0ugAwXFoA,1795
|
|
18
|
+
navcli/server/app.py,sha256=zNWYPGRGVWmoyiZX1g9i6LNqSagJirwzEi8eqsYUUqs,1320
|
|
19
|
+
navcli/server/browser.py,sha256=XEqTUFgw7fLQDIjdahYn1dDDu-IlpaggEiUbH5divjk,10604
|
|
20
|
+
navcli/server/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
navcli/server/routes/__init__.py,sha256=LCd1lCcBCqJmFchr9OTvKJvCTQNLETAljdPaou-fd3I,180
|
|
22
|
+
navcli/server/routes/control.py,sha256=ysoszU2OjhVH4BW3oahJ5-28l3yheQ-vhdqI-I8EVEU,1071
|
|
23
|
+
navcli/server/routes/explore.py,sha256=7leV1LgTLurqDnLIOhDJhA5XEEGBxHkUOV8SgHJxYxc,11862
|
|
24
|
+
navcli/server/routes/interaction.py,sha256=Daq-6Rp7FlWjyMK-HHYGKwQLHDBYWzC0GLq1q346Zxw,8562
|
|
25
|
+
navcli/server/routes/navigation.py,sha256=ZDcblrKnOTDn8aeBKwWgZYHQke6Hmrm_NNtTNSC3MQU,3440
|
|
26
|
+
navcli/server/routes/query.py,sha256=Lgp9lA7lUgXsxvI_1G-fymkIkiCwKpTFOW7SB_NWVJk,7699
|
|
27
|
+
navcli/server/routes/session.py,sha256=gzwQKm8CkJTXcm6-5IYfzm44JF50l7Vhk-0SaLAt754,4701
|
|
28
|
+
navcli/utils/__init__.py,sha256=g3CIjq_xybpmanja9A7oyQMtf9emS6qdHJIJ6BJx6t0,521
|
|
29
|
+
navcli/utils/image.py,sha256=e30zNrOd3wi9UWesiQZJ5n62wIOfrGZnJFCMFKnFOD0,681
|
|
30
|
+
navcli/utils/js.py,sha256=-OenTrj4Nh4s-zjUvjJnN3XSncX8b03tNrgY_nhPRxo,334
|
|
31
|
+
navcli/utils/selector.py,sha256=E_8E5bjjL1gDn524l3bH3XzK3egn9IyoYEb7zXsTwTQ,2146
|
|
32
|
+
navcli/utils/text.py,sha256=wCFCKCn6Ibi8GjAeGXBHT5CfS8BCs7X9qVxDq4eR1tE,389
|
|
33
|
+
navcli/utils/time.py,sha256=bAjJ59rp02MkIfoCT1TzmFd_oGaGlOI-5RPiKSqT-kI,1181
|
|
34
|
+
navcli/utils/url.py,sha256=UG71w8ArYJlqjl2XslwAN-Kk9GJOPK3k5fg6CxXE5js,301
|
|
35
|
+
navcli-0.1.0.dist-info/METADATA,sha256=WyNy3vFZJjHwSR5RYa6rQZYbMR5LhYbepmg4rkztBvA,2607
|
|
36
|
+
navcli-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
37
|
+
navcli-0.1.0.dist-info/entry_points.txt,sha256=J26RwbEYUO5UbuhH0aXF_tvQFCOCatAR80blaSn5oWk,72
|
|
38
|
+
navcli-0.1.0.dist-info/top_level.txt,sha256=dwTN5Lw7STNP3zhL2-RDElX3EslzM2sbYYLfuszQSO4,7
|
|
39
|
+
navcli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
navcli
|