agentremote-cli 1.0.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.
- agentremote_cli-1.0.0.dist-info/METADATA +7 -0
- agentremote_cli-1.0.0.dist-info/RECORD +25 -0
- agentremote_cli-1.0.0.dist-info/WHEEL +5 -0
- agentremote_cli-1.0.0.dist-info/entry_points.txt +2 -0
- agentremote_cli-1.0.0.dist-info/top_level.txt +1 -0
- remotecli/__init__.py +1 -0
- remotecli/agent.py +673 -0
- remotecli/agent_cache.py +274 -0
- remotecli/agent_modes.py +5 -0
- remotecli/agent_open.py +323 -0
- remotecli/agent_projection.py +308 -0
- remotecli/agent_result.py +81 -0
- remotecli/agent_utils.py +113 -0
- remotecli/agent_window.py +249 -0
- remotecli/cli.py +635 -0
- remotecli/client.py +120 -0
- remotecli/config/agentremote.cli.toml +6 -0
- remotecli/cua.py +213 -0
- remotecli/guides/GUIDE.md +397 -0
- remotecli/guides/agent.md +60 -0
- remotecli/guides/cua.md +60 -0
- remotecli/guides/rpa.md +45 -0
- remotecli/guides/shell.md +49 -0
- remotecli/rpa.py +57 -0
- remotecli/shell.py +71 -0
remotecli/client.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentRemote HTTP Client — 与 Middleware 通信.
|
|
3
|
+
|
|
4
|
+
供 remotecli CLI 和 AI Agent 使用。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
_RUNTIME_ROOT = Path(__file__).resolve().parents[1]
|
|
17
|
+
_SHARED_ROOT = _RUNTIME_ROOT / "shared"
|
|
18
|
+
if str(_SHARED_ROOT) not in sys.path:
|
|
19
|
+
sys.path.insert(0, str(_SHARED_ROOT))
|
|
20
|
+
|
|
21
|
+
from agentremote_common.config import load_cli_defaults
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RemoteCliClient:
|
|
25
|
+
"""Middleware HTTP API 客户端。
|
|
26
|
+
|
|
27
|
+
handle 由 CLI --handle 传入,每次 HTTP 请求自动带 Authorization: Bearer <handle>。
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, middleware_url: str = "",
|
|
31
|
+
tenant_id: str = "", node_id: str = "",
|
|
32
|
+
agent_id: str = "", handle: str = "") -> None:
|
|
33
|
+
defaults = load_cli_defaults()
|
|
34
|
+
self.middleware_url = (middleware_url or defaults.middleware_url).rstrip("/")
|
|
35
|
+
self.tenant_id = tenant_id or defaults.tenant
|
|
36
|
+
self.node_id = node_id or defaults.node
|
|
37
|
+
self.agent_id = agent_id or defaults.agent
|
|
38
|
+
self.handle = handle or defaults.handle
|
|
39
|
+
self._http = httpx.Client(timeout=httpx.Timeout(10.0))
|
|
40
|
+
|
|
41
|
+
def _auth_headers(self) -> Dict[str, str]:
|
|
42
|
+
"""构建 Authorization header (handle 为空时不发)。"""
|
|
43
|
+
if self.handle:
|
|
44
|
+
return {"Authorization": f"Bearer {self.handle}"}
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
# ── Capabilities ──────────────────────
|
|
48
|
+
|
|
49
|
+
def list_capabilities(self) -> Dict[str, Any]:
|
|
50
|
+
r = self._http.get(
|
|
51
|
+
f"{self.middleware_url}/api/v1/capabilities",
|
|
52
|
+
params={"tenant_id": self.tenant_id, "node_id": self.node_id},
|
|
53
|
+
headers=self._auth_headers(),
|
|
54
|
+
)
|
|
55
|
+
r.raise_for_status()
|
|
56
|
+
return r.json()
|
|
57
|
+
|
|
58
|
+
def get_guide(self, capability_id: str) -> Dict[str, Any]:
|
|
59
|
+
r = self._http.get(
|
|
60
|
+
f"{self.middleware_url}/api/v1/capabilities/{capability_id}/guide",
|
|
61
|
+
params={"tenant_id": self.tenant_id, "node_id": self.node_id},
|
|
62
|
+
headers=self._auth_headers(),
|
|
63
|
+
)
|
|
64
|
+
r.raise_for_status()
|
|
65
|
+
return r.json()
|
|
66
|
+
|
|
67
|
+
# ── Tasks ─────────────────────────────
|
|
68
|
+
|
|
69
|
+
def submit(self, capability_id: str, tool: str, args: Dict[str, Any] = None,
|
|
70
|
+
timeout_sec: int = 300) -> Dict[str, Any]:
|
|
71
|
+
body = {
|
|
72
|
+
"tenant_id": self.tenant_id,
|
|
73
|
+
"node_id": self.node_id,
|
|
74
|
+
"agent_id": self.agent_id,
|
|
75
|
+
"capability_id": capability_id,
|
|
76
|
+
"tool": tool,
|
|
77
|
+
"args": args or {},
|
|
78
|
+
"timeout_sec": timeout_sec,
|
|
79
|
+
}
|
|
80
|
+
try:
|
|
81
|
+
r = self._http.post(
|
|
82
|
+
f"{self.middleware_url}/api/v1/tasks",
|
|
83
|
+
json=body,
|
|
84
|
+
headers=self._auth_headers(),
|
|
85
|
+
)
|
|
86
|
+
r.raise_for_status()
|
|
87
|
+
except httpx.HTTPStatusError as e:
|
|
88
|
+
# 把服务端返回的 JSON 错误详情带出来
|
|
89
|
+
detail = ""
|
|
90
|
+
try:
|
|
91
|
+
detail = e.response.json()
|
|
92
|
+
except Exception:
|
|
93
|
+
detail = e.response.text
|
|
94
|
+
return {
|
|
95
|
+
"status": "rejected",
|
|
96
|
+
"http_status": e.response.status_code,
|
|
97
|
+
"detail": detail,
|
|
98
|
+
}
|
|
99
|
+
except httpx.RequestError as e:
|
|
100
|
+
return {
|
|
101
|
+
"status": "rejected",
|
|
102
|
+
"reason": "connection_error",
|
|
103
|
+
"error": str(e),
|
|
104
|
+
}
|
|
105
|
+
return r.json()
|
|
106
|
+
|
|
107
|
+
def poll_result(self, trace_id: str, timeout_sec: float = 30.0,
|
|
108
|
+
interval: float = 0.1) -> Dict[str, Any]:
|
|
109
|
+
deadline = time.time() + timeout_sec
|
|
110
|
+
while time.time() < deadline:
|
|
111
|
+
r = self._http.get(
|
|
112
|
+
f"{self.middleware_url}/api/v1/tasks/{trace_id}/result",
|
|
113
|
+
headers=self._auth_headers(),
|
|
114
|
+
)
|
|
115
|
+
if r.status_code == 200:
|
|
116
|
+
data = r.json()
|
|
117
|
+
if data.get("status") != "pending" and data.get("result") is not None:
|
|
118
|
+
return data
|
|
119
|
+
time.sleep(interval)
|
|
120
|
+
return {"trace_id": trace_id, "status": "timeout", "result": None}
|
remotecli/cua.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
remotecli cua — CUA 桌面控制子命令.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
from .client import RemoteCliClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CuaCommands:
|
|
14
|
+
"""CUA 桌面控制命令构建器。"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: RemoteCliClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
self._capability_id = "desktop.cua"
|
|
19
|
+
|
|
20
|
+
def _submit(self, tool: str, args: Dict[str, Any], timeout_sec: int = 30) -> Dict[str, Any]:
|
|
21
|
+
return self._client.submit(self._capability_id, tool, args, timeout_sec=timeout_sec)
|
|
22
|
+
|
|
23
|
+
def click(self, pid: int, window_id: int, target: str = "",
|
|
24
|
+
x: int = 0, y: int = 0, element_index: int = 0,
|
|
25
|
+
locate_mode: str = "") -> Dict[str, Any]:
|
|
26
|
+
args: Dict[str, Any] = {"pid": pid, "window_id": window_id}
|
|
27
|
+
if target:
|
|
28
|
+
args["target"] = target
|
|
29
|
+
if locate_mode:
|
|
30
|
+
args["locate_mode"] = locate_mode
|
|
31
|
+
elif element_index:
|
|
32
|
+
args["element_index"] = element_index
|
|
33
|
+
elif x or y:
|
|
34
|
+
args["x"] = x
|
|
35
|
+
args["y"] = y
|
|
36
|
+
return self._submit("click", args)
|
|
37
|
+
|
|
38
|
+
def double_click(self, pid: int, window_id: int, target: str = "",
|
|
39
|
+
x: int = 0, y: int = 0, element_index: int = 0,
|
|
40
|
+
locate_mode: str = "") -> Dict[str, Any]:
|
|
41
|
+
args: Dict[str, Any] = {"pid": pid, "window_id": window_id}
|
|
42
|
+
if target:
|
|
43
|
+
args["target"] = target
|
|
44
|
+
if locate_mode:
|
|
45
|
+
args["locate_mode"] = locate_mode
|
|
46
|
+
elif element_index:
|
|
47
|
+
args["element_index"] = element_index
|
|
48
|
+
elif x or y:
|
|
49
|
+
args["x"] = x
|
|
50
|
+
args["y"] = y
|
|
51
|
+
return self._submit("double_click", args)
|
|
52
|
+
|
|
53
|
+
def right_click(self, pid: int, window_id: int, target: str = "",
|
|
54
|
+
x: int = 0, y: int = 0, element_index: int = 0,
|
|
55
|
+
locate_mode: str = "") -> Dict[str, Any]:
|
|
56
|
+
args: Dict[str, Any] = {"pid": pid, "window_id": window_id}
|
|
57
|
+
if target:
|
|
58
|
+
args["target"] = target
|
|
59
|
+
if locate_mode:
|
|
60
|
+
args["locate_mode"] = locate_mode
|
|
61
|
+
elif element_index:
|
|
62
|
+
args["element_index"] = element_index
|
|
63
|
+
elif x or y:
|
|
64
|
+
args["x"] = x
|
|
65
|
+
args["y"] = y
|
|
66
|
+
return self._submit("right_click", args)
|
|
67
|
+
|
|
68
|
+
def type_text(self, pid: int, target: str, text: str, element_index: int = 0,
|
|
69
|
+
window_id: int = 0, locate_mode: str = "") -> Dict[str, Any]:
|
|
70
|
+
args: Dict[str, Any] = {"pid": pid, "text": text}
|
|
71
|
+
if window_id:
|
|
72
|
+
args["window_id"] = window_id
|
|
73
|
+
if target:
|
|
74
|
+
args["target"] = target
|
|
75
|
+
if locate_mode:
|
|
76
|
+
args["locate_mode"] = locate_mode
|
|
77
|
+
elif element_index:
|
|
78
|
+
args["element_index"] = element_index
|
|
79
|
+
return self._submit("type_text", args)
|
|
80
|
+
|
|
81
|
+
def hotkey(self, pid: int, keys: str | Sequence[str], window_id: int = 0) -> Dict[str, Any]:
|
|
82
|
+
args: Dict[str, Any] = {"pid": pid, "keys": self._parse_hotkey_keys(keys)}
|
|
83
|
+
if window_id:
|
|
84
|
+
args["window_id"] = window_id
|
|
85
|
+
return self._submit("hotkey", args)
|
|
86
|
+
|
|
87
|
+
def press_key(self, pid: int, key: str, window_id: int = 0) -> Dict[str, Any]:
|
|
88
|
+
args: Dict[str, Any] = {"pid": pid, "key": key}
|
|
89
|
+
if window_id:
|
|
90
|
+
args["window_id"] = window_id
|
|
91
|
+
return self._submit("press_key", args)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _parse_hotkey_keys(keys: str | Sequence[str]) -> list[str]:
|
|
95
|
+
if isinstance(keys, str):
|
|
96
|
+
separator = "," if "," in keys else "+"
|
|
97
|
+
parsed = [part.strip() for part in keys.split(separator)]
|
|
98
|
+
else:
|
|
99
|
+
parsed = [str(part).strip() for part in keys]
|
|
100
|
+
|
|
101
|
+
parsed = [part for part in parsed if part]
|
|
102
|
+
if not parsed:
|
|
103
|
+
raise ValueError("hotkey keys must include at least one key")
|
|
104
|
+
return parsed
|
|
105
|
+
|
|
106
|
+
def launch_app(self, name: str = "", path: str = "", args_str: str = "",
|
|
107
|
+
wait: bool = False) -> Dict[str, Any]:
|
|
108
|
+
app_args: Dict[str, Any] = {}
|
|
109
|
+
if name:
|
|
110
|
+
app_args["name"] = name
|
|
111
|
+
if path:
|
|
112
|
+
app_args["launch_path"] = path
|
|
113
|
+
if args_str:
|
|
114
|
+
app_args["args"] = args_str
|
|
115
|
+
if wait:
|
|
116
|
+
app_args["wait"] = True
|
|
117
|
+
return self._submit("launch_app", app_args)
|
|
118
|
+
|
|
119
|
+
def list_apps(self, query: str = "", max_results: int = 0) -> Dict[str, Any]:
|
|
120
|
+
args: Dict[str, Any] = {}
|
|
121
|
+
if query:
|
|
122
|
+
args["query"] = query
|
|
123
|
+
if max_results:
|
|
124
|
+
args["max_results"] = max_results
|
|
125
|
+
return self._submit("list_apps", args)
|
|
126
|
+
|
|
127
|
+
def list_windows(self, pid: int = 0) -> Dict[str, Any]:
|
|
128
|
+
args: Dict[str, Any] = {}
|
|
129
|
+
if pid:
|
|
130
|
+
args["pid"] = pid
|
|
131
|
+
return self._submit("list_windows", args)
|
|
132
|
+
|
|
133
|
+
def bring_to_front(self, pid: int, window_id: int) -> Dict[str, Any]:
|
|
134
|
+
return self._submit("bring_to_front", {"pid": pid, "window_id": window_id})
|
|
135
|
+
|
|
136
|
+
def get_window_state(self, pid: int, window_id: int, mode: str = "vision") -> Dict[str, Any]:
|
|
137
|
+
args = {"pid": pid, "window_id": window_id}
|
|
138
|
+
if mode and mode != "uia":
|
|
139
|
+
args["capture_mode"] = mode
|
|
140
|
+
return self._submit("get_window_state", args)
|
|
141
|
+
|
|
142
|
+
def probe_window(
|
|
143
|
+
self,
|
|
144
|
+
pid: int,
|
|
145
|
+
window_id: int,
|
|
146
|
+
targets: Sequence[str],
|
|
147
|
+
mode: str = "smart",
|
|
148
|
+
) -> Dict[str, Any]:
|
|
149
|
+
return self._submit(
|
|
150
|
+
"probe_window",
|
|
151
|
+
{
|
|
152
|
+
"pid": pid,
|
|
153
|
+
"window_id": window_id,
|
|
154
|
+
"targets": [str(target) for target in targets if str(target).strip()],
|
|
155
|
+
"mode": mode,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def get_cursor_position(self) -> Dict[str, Any]:
|
|
160
|
+
return self._submit("get_cursor_position", {})
|
|
161
|
+
|
|
162
|
+
def move_mouse(self, x: int, y: int) -> Dict[str, Any]:
|
|
163
|
+
return self._submit("move_mouse", {"x": x, "y": y})
|
|
164
|
+
|
|
165
|
+
def move_mouse_relative(self, dx: int, dy: int) -> Dict[str, Any]:
|
|
166
|
+
return self._submit("move_mouse_relative", {"dx": dx, "dy": dy})
|
|
167
|
+
|
|
168
|
+
def scroll(
|
|
169
|
+
self,
|
|
170
|
+
pid: int,
|
|
171
|
+
window_id: int,
|
|
172
|
+
dx: int = 0,
|
|
173
|
+
dy: int = 0,
|
|
174
|
+
direction: str = "",
|
|
175
|
+
amount: int = 0,
|
|
176
|
+
by: str = "",
|
|
177
|
+
dispatch: str = "",
|
|
178
|
+
) -> Dict[str, Any]:
|
|
179
|
+
args: Dict[str, Any] = {"pid": pid, "window_id": window_id}
|
|
180
|
+
if direction:
|
|
181
|
+
args["direction"] = direction
|
|
182
|
+
if amount:
|
|
183
|
+
args["amount"] = amount
|
|
184
|
+
if by:
|
|
185
|
+
args["by"] = by
|
|
186
|
+
if dispatch:
|
|
187
|
+
args["dispatch"] = dispatch
|
|
188
|
+
else:
|
|
189
|
+
args["dx"] = dx
|
|
190
|
+
args["dy"] = dy
|
|
191
|
+
return self._submit("scroll", args)
|
|
192
|
+
|
|
193
|
+
def drag(self, pid: int, window_id: int, from_x: int, from_y: int,
|
|
194
|
+
to_x: int, to_y: int) -> Dict[str, Any]:
|
|
195
|
+
return self._submit("drag", {
|
|
196
|
+
"pid": pid, "window_id": window_id,
|
|
197
|
+
"from_x": from_x, "from_y": from_y,
|
|
198
|
+
"to_x": to_x, "to_y": to_y,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
def set_value(self, pid: int, target: str = "", value: str = "",
|
|
202
|
+
element_index: int = 0, window_id: int = 0,
|
|
203
|
+
locate_mode: str = "") -> Dict[str, Any]:
|
|
204
|
+
args: Dict[str, Any] = {"pid": pid, "value": value}
|
|
205
|
+
if window_id:
|
|
206
|
+
args["window_id"] = window_id
|
|
207
|
+
if target:
|
|
208
|
+
args["target"] = target
|
|
209
|
+
if locate_mode:
|
|
210
|
+
args["locate_mode"] = locate_mode
|
|
211
|
+
elif element_index:
|
|
212
|
+
args["element_index"] = element_index
|
|
213
|
+
return self._submit("set_value", args)
|