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.
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}
@@ -0,0 +1,6 @@
1
+ [cli]
2
+ middleware_url = "http://127.0.0.1:9100"
3
+ tenant = "acme_shanghai"
4
+ node = "sh_win_01"
5
+ agent = "agent_ai_01"
6
+ handle = ""
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)