pygpt-net 2.7.4__py3-none-any.whl → 2.7.5__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.
Files changed (133) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app_core.py +4 -2
  4. pygpt_net/controller/__init__.py +5 -1
  5. pygpt_net/controller/assistant/assistant.py +1 -4
  6. pygpt_net/controller/assistant/batch.py +5 -504
  7. pygpt_net/controller/assistant/editor.py +5 -5
  8. pygpt_net/controller/assistant/files.py +16 -16
  9. pygpt_net/controller/chat/handler/google_stream.py +307 -1
  10. pygpt_net/controller/chat/handler/worker.py +8 -1
  11. pygpt_net/controller/chat/image.py +2 -2
  12. pygpt_net/controller/dialogs/confirm.py +73 -101
  13. pygpt_net/controller/lang/mapping.py +9 -9
  14. pygpt_net/controller/painter/capture.py +50 -1
  15. pygpt_net/controller/presets/presets.py +2 -1
  16. pygpt_net/controller/remote_store/__init__.py +12 -0
  17. pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
  18. pygpt_net/controller/remote_store/google/batch.py +402 -0
  19. pygpt_net/controller/remote_store/google/store.py +615 -0
  20. pygpt_net/controller/remote_store/openai/__init__.py +12 -0
  21. pygpt_net/controller/remote_store/openai/batch.py +524 -0
  22. pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
  23. pygpt_net/controller/remote_store/remote_store.py +35 -0
  24. pygpt_net/controller/ui/ui.py +20 -1
  25. pygpt_net/core/assistants/assistants.py +3 -15
  26. pygpt_net/core/db/database.py +5 -3
  27. pygpt_net/core/locale/placeholder.py +35 -0
  28. pygpt_net/core/remote_store/__init__.py +12 -0
  29. pygpt_net/core/remote_store/google/__init__.py +11 -0
  30. pygpt_net/core/remote_store/google/files.py +224 -0
  31. pygpt_net/core/remote_store/google/store.py +248 -0
  32. pygpt_net/core/remote_store/openai/__init__.py +11 -0
  33. pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
  34. pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
  35. pygpt_net/core/remote_store/remote_store.py +24 -0
  36. pygpt_net/data/config/config.json +8 -4
  37. pygpt_net/data/config/models.json +77 -3
  38. pygpt_net/data/config/settings.json +45 -0
  39. pygpt_net/data/locale/locale.de.ini +41 -41
  40. pygpt_net/data/locale/locale.en.ini +53 -43
  41. pygpt_net/data/locale/locale.es.ini +41 -41
  42. pygpt_net/data/locale/locale.fr.ini +41 -41
  43. pygpt_net/data/locale/locale.it.ini +41 -41
  44. pygpt_net/data/locale/locale.pl.ini +42 -42
  45. pygpt_net/data/locale/locale.uk.ini +41 -41
  46. pygpt_net/data/locale/locale.zh.ini +41 -41
  47. pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
  48. pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
  49. pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
  50. pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
  51. pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
  52. pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
  53. pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
  54. pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
  55. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
  56. pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
  57. pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
  58. pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
  59. pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
  60. pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
  61. pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
  62. pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
  63. pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
  64. pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
  65. pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
  66. pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
  67. pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
  68. pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
  69. pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
  70. pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
  71. pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
  72. pygpt_net/item/assistant.py +1 -211
  73. pygpt_net/item/ctx.py +3 -1
  74. pygpt_net/item/store.py +238 -0
  75. pygpt_net/migrations/Version20260102190000.py +35 -0
  76. pygpt_net/migrations/__init__.py +3 -1
  77. pygpt_net/plugin/cmd_mouse_control/config.py +470 -1
  78. pygpt_net/plugin/cmd_mouse_control/plugin.py +488 -22
  79. pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
  80. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
  81. pygpt_net/plugin/idx_llama_index/config.py +2 -2
  82. pygpt_net/provider/api/google/__init__.py +16 -54
  83. pygpt_net/provider/api/google/chat.py +546 -129
  84. pygpt_net/provider/api/google/computer.py +190 -0
  85. pygpt_net/provider/api/google/realtime/realtime.py +2 -2
  86. pygpt_net/provider/api/google/remote_tools.py +93 -0
  87. pygpt_net/provider/api/google/store.py +546 -0
  88. pygpt_net/provider/api/google/worker/__init__.py +0 -0
  89. pygpt_net/provider/api/google/worker/importer.py +392 -0
  90. pygpt_net/provider/api/openai/computer.py +10 -1
  91. pygpt_net/provider/api/openai/store.py +6 -6
  92. pygpt_net/provider/api/openai/worker/importer.py +24 -24
  93. pygpt_net/provider/core/config/patch.py +16 -1
  94. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
  95. pygpt_net/provider/core/model/patch.py +17 -3
  96. pygpt_net/provider/core/preset/json_file.py +13 -7
  97. pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
  98. pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
  99. pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
  100. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
  101. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
  102. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
  103. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
  104. pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
  105. pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
  106. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
  107. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
  108. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
  109. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
  110. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
  111. pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
  112. pygpt_net/provider/llms/google.py +2 -2
  113. pygpt_net/ui/base/config_dialog.py +3 -2
  114. pygpt_net/ui/dialog/assistant.py +3 -3
  115. pygpt_net/ui/dialog/plugins.py +3 -1
  116. pygpt_net/ui/dialog/remote_store_google.py +539 -0
  117. pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
  118. pygpt_net/ui/dialogs.py +5 -3
  119. pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
  120. pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
  121. pygpt_net/ui/menu/tools.py +13 -5
  122. pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
  123. pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
  124. pygpt_net/ui/widget/element/button.py +4 -4
  125. pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
  126. pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
  127. pygpt_net/ui/widget/option/checkbox_list.py +47 -9
  128. pygpt_net/ui/widget/option/combo.py +39 -3
  129. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/METADATA +33 -2
  130. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/RECORD +133 -108
  131. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/LICENSE +0 -0
  132. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/WHEEL +0 -0
  133. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,729 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2026.01.02 02:00:00 #
10
+ # ================================================== #
11
+
12
+ import time
13
+ import threading
14
+ from typing import Optional, List
15
+
16
+ from PySide6.QtCore import Slot, Signal
17
+ from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
18
+
19
+
20
+ class WorkerSandboxSignals(BaseSignals):
21
+ screenshot = Signal(dict, object)
22
+ start = Signal()
23
+ call = Signal(str, dict, object, object) # op: str, params: dict, ret: dict (container), done: threading.Event
24
+
25
+
26
+ class Worker(BaseWorker):
27
+ """
28
+ Sandbox worker: executes computer-use actions via Plugin in main thread.
29
+ It mirrors the public API of host worker. Each result includes "url".
30
+ All Playwright operations are delegated to plugin through signals to avoid
31
+ cross-thread greenlet violations.
32
+ """
33
+
34
+ WAIT_TIMEOUT = 60.0 # seconds to wait for a main-thread operation
35
+
36
+ def __init__(self, *args, **kwargs):
37
+ super(Worker, self).__init__()
38
+ self.signals = WorkerSandboxSignals()
39
+ self.window = None
40
+ self.args = args
41
+ self.kwargs = kwargs
42
+ self.plugin = None
43
+ self.cmds = None
44
+ self.ctx = None
45
+ self.msg = None
46
+
47
+ # Viewport and pointer tracking (synced from plugin on ensure)
48
+ self.viewport_w = 1440
49
+ self.viewport_h = 900
50
+ self._mouse_x = 0
51
+ self._mouse_y = 0
52
+ self._last_url = ""
53
+
54
+ # Defaults
55
+ self._default_scroll_px = 800
56
+
57
+ # ========================= Lifecycle ========================= #
58
+
59
+ @Slot()
60
+ def run(self):
61
+ try:
62
+ responses = []
63
+ for item in self.cmds:
64
+ if self.is_stopped():
65
+ break
66
+
67
+ response = None
68
+ try:
69
+ cmd = item.get("cmd")
70
+ if not cmd:
71
+ continue
72
+
73
+ # alias before gating
74
+ if cmd == "screenshot":
75
+ item = dict(item)
76
+ item["cmd"] = "get_screenshot"
77
+ cmd = "get_screenshot"
78
+
79
+ # allow only plugin-declared commands
80
+ allowed = getattr(self.plugin, "allowed_cmds", None)
81
+ if isinstance(allowed, (list, set, tuple)) and cmd not in allowed:
82
+ continue
83
+
84
+ response = self._dispatch(item)
85
+ if response:
86
+ responses.append(response)
87
+
88
+ except Exception as e:
89
+ responses.append(self.make_response(item, self.throw_error(e)))
90
+
91
+ if responses:
92
+ self.reply_more(responses)
93
+
94
+ except Exception as e:
95
+ self.error(e)
96
+ finally:
97
+ self.cleanup()
98
+
99
+ def on_destroy(self):
100
+ """Handle destroyed event."""
101
+ self.cleanup()
102
+
103
+ def cleanup(self):
104
+ """Playwright is owned by the plugin; worker does not close it."""
105
+ return
106
+
107
+ # ========================= Dispatch ========================= #
108
+
109
+ def _dispatch(self, item: dict) -> dict:
110
+ cmd = item["cmd"]
111
+
112
+ handler = None
113
+
114
+ # Legacy-compatible API
115
+ if cmd == "open_web_browser":
116
+ handler = self.cmd_open_web_browser
117
+ elif cmd == "get_mouse_position":
118
+ handler = self.cmd_mouse_get_pos
119
+ elif cmd == "mouse_move":
120
+ handler = self.cmd_mouse_move
121
+ elif cmd == "mouse_drag":
122
+ handler = self.cmd_mouse_drag
123
+ elif cmd == "mouse_click":
124
+ handler = self.cmd_mouse_click
125
+ elif cmd == "mouse_scroll":
126
+ handler = self.cmd_mouse_scroll
127
+ elif cmd == "get_screenshot":
128
+ handler = self.cmd_make_screenshot
129
+ elif cmd == "keyboard_key":
130
+ handler = self.cmd_keyboard_key
131
+ elif cmd == "keyboard_keys":
132
+ handler = self.cmd_keyboard_keys
133
+ elif cmd == "keyboard_type":
134
+ handler = self.cmd_keyboard_type
135
+ elif cmd == "wait":
136
+ handler = self.cmd_wait
137
+
138
+ # Google/OpenAI Computer Use
139
+ elif cmd == "wait_5_seconds":
140
+ handler = self.cmd_wait_5_seconds
141
+ elif cmd == "go_back":
142
+ handler = self.cmd_go_back
143
+ elif cmd == "go_forward":
144
+ handler = self.cmd_go_forward
145
+ elif cmd == "search":
146
+ handler = self.cmd_search
147
+ elif cmd == "navigate":
148
+ handler = self.cmd_navigate
149
+ elif cmd == "click_at":
150
+ handler = self.cmd_click_at
151
+ elif cmd == "hover_at":
152
+ handler = self.cmd_hover_at
153
+ elif cmd == "type_text_at":
154
+ handler = self.cmd_type_text_at
155
+ elif cmd == "key_combination":
156
+ handler = self.cmd_key_combination
157
+ elif cmd == "scroll_document":
158
+ handler = self.cmd_scroll_document
159
+ elif cmd == "scroll_at":
160
+ handler = self.cmd_scroll_at
161
+ elif cmd == "drag_and_drop":
162
+ handler = self.cmd_drag_and_drop
163
+
164
+ # Action-style
165
+ elif cmd == "click":
166
+ handler = self.cmd_click
167
+ elif cmd == "double_click":
168
+ handler = self.cmd_double_click
169
+ elif cmd == "move":
170
+ handler = self.cmd_move
171
+ elif cmd == "type":
172
+ handler = self.cmd_type_text
173
+ elif cmd == "keypress":
174
+ handler = self.cmd_keypress
175
+ elif cmd == "scroll":
176
+ handler = self.cmd_scroll
177
+ elif cmd == "drag":
178
+ handler = self.cmd_drag
179
+
180
+ if not handler:
181
+ return None
182
+
183
+ return handler(item)
184
+
185
+ # ========================= Helpers ========================= #
186
+
187
+ def _call(self, op: str, params: dict) -> dict:
188
+ """
189
+ Call plugin to perform a Playwright operation in the main thread and wait for result.
190
+ """
191
+ ret = {}
192
+ done = threading.Event()
193
+ self.signals.call.emit(op, dict(params or {}), ret, done)
194
+ if not done.wait(self.WAIT_TIMEOUT):
195
+ raise TimeoutError(f"Playwright op timeout: {op}")
196
+ # update cached state if provided
197
+ self._update_state_from_ret(ret)
198
+ return ret
199
+
200
+ def _update_state_from_ret(self, ret: dict):
201
+ url = ret.get("url")
202
+ if isinstance(url, str):
203
+ self._last_url = url
204
+ vw = ret.get("viewport_w")
205
+ vh = ret.get("viewport_h")
206
+ if isinstance(vw, int) and isinstance(vh, int) and vw > 0 and vh > 0:
207
+ self.viewport_w, self.viewport_h = vw, vh
208
+ mx = ret.get("mouse_x")
209
+ my = ret.get("mouse_y")
210
+ if isinstance(mx, int) and isinstance(my, int):
211
+ self._mouse_x, self._mouse_y = mx, my
212
+
213
+ def _ensure_browser(self):
214
+ """Ensure Playwright is started in the main thread and sync viewport."""
215
+ self.signals.start.emit()
216
+ ret = self._call("ensure", {})
217
+ if "viewport_w" in ret and "viewport_h" in ret:
218
+ self.viewport_w = int(ret["viewport_w"] or self.viewport_w)
219
+ self.viewport_h = int(ret["viewport_h"] or self.viewport_h)
220
+
221
+ def _current_url(self) -> str:
222
+ return self.plugin.get_last_url()
223
+ return self._last_url or ""
224
+
225
+ def _result_with_url(self, base: dict) -> dict:
226
+ base = dict(base or {})
227
+ base["url"] = self._current_url()
228
+ return base
229
+
230
+ def _get_screen_and_pointer(self, item: dict = None) -> dict:
231
+ current_step = self.get_param(item, "current_step", "")
232
+ screen_w, screen_h = self.viewport_w, self.viewport_h
233
+ return {
234
+ "current_step": current_step,
235
+ "screen_w": screen_w,
236
+ "screen_h": screen_h,
237
+ "mouse_x": self._mouse_x,
238
+ "mouse_y": self._mouse_y,
239
+ }
240
+
241
+ def _get_current(self, item: dict = None) -> dict:
242
+ base = self._get_screen_and_pointer(item)
243
+ return self._result_with_url(base)
244
+
245
+ def _denorm_x(self, x_norm: int) -> int:
246
+ x_norm = max(0, min(999, int(x_norm)))
247
+ return int(round(x_norm / 1000.0 * self.viewport_w))
248
+
249
+ def _denorm_y(self, y_norm: int) -> int:
250
+ y_norm = max(0, min(999, int(y_norm)))
251
+ return int(round(y_norm / 1000.0 * self.viewport_h))
252
+
253
+ def _button_from_name(self, name: Optional[str]) -> str:
254
+ if not name:
255
+ return "left"
256
+ name = name.lower()
257
+ if name in ("left", "right", "middle"):
258
+ return name
259
+ return "left"
260
+
261
+ def _permit(self, option_name: str) -> bool:
262
+ try:
263
+ if hasattr(self.plugin, "get_option_value"):
264
+ val = self.plugin.get_option_value(option_name)
265
+ if val is None:
266
+ return True
267
+ return bool(val)
268
+ except Exception:
269
+ pass
270
+ return True
271
+
272
+ # ========================= Legacy-compatible commands ========================= #
273
+
274
+ def cmd_open_web_browser(self, item: dict) -> dict:
275
+ try:
276
+ self.msg = "Open web browser (sandbox via plugin)"
277
+ self.log(self.msg)
278
+ self._ensure_browser()
279
+ url = ""
280
+ if self.has_param(item, "url"):
281
+ url = self.get_param(item, "url") or ""
282
+ if url:
283
+ self._call("navigate", {"url": url})
284
+ result = self._get_current(item)
285
+ self.log("Response: {}".format(result))
286
+ except Exception as e:
287
+ result = self.throw_error(e)
288
+
289
+ if self.has_param(item, "no_screenshot"):
290
+ result["no_screenshot"] = True
291
+ return self.make_response(item, result)
292
+
293
+ def cmd_wait(self, item: dict) -> dict:
294
+ wait_time = 5
295
+ try:
296
+ if self.has_param(item, "seconds"):
297
+ wait_time = int(self.get_param(item, "seconds"))
298
+ self.msg = "Wait (sandbox)"
299
+ self.log(self.msg)
300
+ result = self._get_current(item)
301
+ self.log("Response: {}".format(result))
302
+ except Exception as e:
303
+ result = self.throw_error(e)
304
+ if self.has_param(item, "no_screenshot"):
305
+ result["no_screenshot"] = True
306
+ time.sleep(max(0, wait_time))
307
+ return self.make_response(item, result)
308
+
309
+ def cmd_mouse_get_pos(self, item: dict) -> dict:
310
+ try:
311
+ self.msg = "Mouse get position (sandbox)"
312
+ self.log(self.msg)
313
+ result = self._get_current(item)
314
+ self.log("Response: {}".format(result))
315
+ except Exception as e:
316
+ result = self.throw_error(e)
317
+ if self.has_param(item, "no_screenshot"):
318
+ result["no_screenshot"] = True
319
+ return self.make_response(item, result)
320
+
321
+ def cmd_mouse_move(self, item: dict) -> dict:
322
+ error = None
323
+ try:
324
+ if not self._permit("allow_mouse_move"):
325
+ raise RuntimeError("Mouse move not permitted by settings.")
326
+ self._ensure_browser()
327
+ x = int(self.get_param(item, "x", self.get_param(item, "mouse_x", 0)))
328
+ y = int(self.get_param(item, "y", self.get_param(item, "mouse_y", 0)))
329
+ click = self.get_param(item, "click", None)
330
+ num = int(self.get_param(item, "num_clicks", 1))
331
+ if click:
332
+ # Single combined call: click at explicit coordinates
333
+ self._call("click", {"x": x, "y": y, "button": self._button_from_name(click), "count": max(1, num)})
334
+ else:
335
+ self._call("move", {"x": x, "y": y})
336
+ result = self._get_current(item)
337
+ except Exception as e:
338
+ error = str(e)
339
+ result = self._get_current(item)
340
+ result["error"] = error
341
+ self.log("Error: {}".format(e))
342
+
343
+ if self.has_param(item, "no_screenshot"):
344
+ result["no_screenshot"] = True
345
+ return self.make_response(item, result)
346
+
347
+ def cmd_mouse_click(self, item: dict) -> dict:
348
+ try:
349
+ if not self._permit("allow_mouse_click"):
350
+ raise RuntimeError("Mouse click not permitted by settings.")
351
+ self._ensure_browser()
352
+ button = self._button_from_name(self.get_param(item, "button", "left"))
353
+ num = int(self.get_param(item, "num_clicks", 1))
354
+ x = self.get_param(item, "x", None)
355
+ y = self.get_param(item, "y", None)
356
+ payload = {"button": button, "count": max(1, num)}
357
+ if x is not None and y is not None:
358
+ payload["x"] = int(x)
359
+ payload["y"] = int(y)
360
+ self._call("click", payload)
361
+ result = self._get_current(item)
362
+ except Exception as e:
363
+ result = self.throw_error(e)
364
+ if self.has_param(item, "no_screenshot"):
365
+ result["no_screenshot"] = True
366
+ return self.make_response(item, result)
367
+
368
+ def cmd_mouse_scroll(self, item: dict) -> dict:
369
+ try:
370
+ if not self._permit("allow_mouse_scroll"):
371
+ raise RuntimeError("Mouse scroll not permitted by settings.")
372
+ self._ensure_browser()
373
+ x = self.get_param(item, "x", self.get_param(item, "mouse_x", None))
374
+ y = self.get_param(item, "y", self.get_param(item, "mouse_y", None))
375
+ dx = int(self.get_param(item, "dx", 0))
376
+ dy = int(self.get_param(item, "dy", 0))
377
+ unit = self.get_param(item, "unit", "px")
378
+ if unit == "step":
379
+ dx = int(dx) * 30
380
+ dy = int(dy) * 30
381
+ payload = {"dx": dx, "dy": dy}
382
+ if x is not None and y is not None:
383
+ payload["x"] = int(x)
384
+ payload["y"] = int(y)
385
+ self._call("scroll", payload)
386
+ result = self._get_current(item)
387
+ except Exception as e:
388
+ result = self.throw_error(e)
389
+ if self.has_param(item, "no_screenshot"):
390
+ result["no_screenshot"] = True
391
+ return self.make_response(item, result)
392
+
393
+ def cmd_mouse_drag(self, item: dict) -> dict:
394
+ try:
395
+ self._ensure_browser()
396
+ x = int(self.get_param(item, "x"))
397
+ y = int(self.get_param(item, "y"))
398
+ dx = int(self.get_param(item, "dx"))
399
+ dy = int(self.get_param(item, "dy"))
400
+ self._call("drag", {"x": x, "y": y, "dx": dx, "dy": dy})
401
+ result = self._get_current(item)
402
+ except Exception as e:
403
+ result = self.throw_error(e)
404
+ if self.has_param(item, "no_screenshot"):
405
+ result["no_screenshot"] = True
406
+ return self.make_response(item, result)
407
+
408
+ def cmd_keyboard_keys(self, item: dict) -> dict:
409
+ error = None
410
+ try:
411
+ if not self._permit("allow_keyboard"):
412
+ raise RuntimeError("Keyboard not permitted by settings.")
413
+ self._ensure_browser()
414
+ keys = self.get_param(item, "keys", []) or []
415
+ self._call("keypress_combo", {"keys": keys})
416
+ result = self._get_current(item)
417
+ except Exception as e:
418
+ error = str(e)
419
+ result = self._get_current(item)
420
+ result["error"] = error
421
+ self.log("Error: {}".format(e))
422
+ if self.has_param(item, "no_screenshot"):
423
+ result["no_screenshot"] = True
424
+ return self.make_response(item, result)
425
+
426
+ def cmd_keyboard_key(self, item: dict) -> dict:
427
+ error = None
428
+ try:
429
+ if not self._permit("allow_keyboard"):
430
+ raise RuntimeError("Keyboard not permitted by settings.")
431
+ self._ensure_browser()
432
+ key = self.get_param(item, "key")
433
+ modifier = self.get_param(item, "modifier", None)
434
+ if modifier:
435
+ self._call("keypress_combo", {"keys": [modifier, key]})
436
+ else:
437
+ self._call("keypress", {"key": key})
438
+ result = self._get_current(item)
439
+ except Exception as e:
440
+ error = str(e)
441
+ result = self._get_current(item)
442
+ result["error"] = error
443
+ self.log("Error: {}".format(e))
444
+ if self.has_param(item, "no_screenshot"):
445
+ result["no_screenshot"] = True
446
+ return self.make_response(item, result)
447
+
448
+ def cmd_keyboard_type(self, item: dict) -> dict:
449
+ try:
450
+ if not self._permit("allow_keyboard"):
451
+ raise RuntimeError("Keyboard not permitted by settings.")
452
+ self._ensure_browser()
453
+ text = self.get_param(item, "text", "") or ""
454
+ modifier = self.get_param(item, "modifier", None)
455
+ payload = {"text": text}
456
+ if modifier:
457
+ payload["modifier"] = modifier
458
+ self._call("type", payload)
459
+ result = self._get_current(item)
460
+ except Exception as e:
461
+ result = self.throw_error(e)
462
+ if self.has_param(item, "no_screenshot"):
463
+ result["no_screenshot"] = True
464
+ return self.make_response(item, result)
465
+
466
+ def cmd_make_screenshot(self, item: dict) -> dict:
467
+ try:
468
+ self._ensure_browser()
469
+ ret = self._call("screenshot", {"full_page": False})
470
+ img_bytes = ret.get("image", b"")
471
+ meta = self._get_current(item)
472
+ if img_bytes:
473
+ self.signals.screenshot.emit(meta, img_bytes)
474
+ result = meta
475
+ self.log("Response: {}".format(result))
476
+ except Exception as e:
477
+ result = self.throw_error(e)
478
+ if self.has_param(item, "no_screenshot"):
479
+ result["no_screenshot"] = True
480
+ return self.make_response(item, result)
481
+
482
+ # ========================= Google/OpenAI Computer Use commands ========================= #
483
+
484
+ def cmd_wait_5_seconds(self, item: dict) -> dict:
485
+ return self.cmd_wait({"cmd": "wait", "params": {"seconds": 5}})
486
+
487
+ def cmd_go_back(self, item: dict) -> dict:
488
+ try:
489
+ self._ensure_browser()
490
+ self._call("go_back", {})
491
+ result = self._get_current(item)
492
+ except Exception as e:
493
+ result = self.throw_error(e)
494
+ return self.make_response(item, result)
495
+
496
+ def cmd_go_forward(self, item: dict) -> dict:
497
+ try:
498
+ self._ensure_browser()
499
+ self._call("go_forward", {})
500
+ result = self._get_current(item)
501
+ except Exception as e:
502
+ result = self.throw_error(e)
503
+ return self.make_response(item, result)
504
+
505
+ def cmd_search(self, item: dict) -> dict:
506
+ try:
507
+ self._ensure_browser()
508
+ self._call("navigate", {"url": "https://www.google.com"})
509
+ result = self._get_current(item)
510
+ except Exception as e:
511
+ result = self.throw_error(e)
512
+ return self.make_response(item, result)
513
+
514
+ def cmd_navigate(self, item: dict) -> dict:
515
+ try:
516
+ self._ensure_browser()
517
+ url = self.get_param(item, "url", "") or ""
518
+ if url:
519
+ self._call("navigate", {"url": url})
520
+ result = self._get_current(item)
521
+ except Exception as e:
522
+ result = self.throw_error(e)
523
+ return self.make_response(item, result)
524
+
525
+ def cmd_click_at(self, item: dict) -> dict:
526
+ try:
527
+ self._ensure_browser()
528
+ x = self._denorm_x(int(self.get_param(item, "x")))
529
+ y = self._denorm_y(int(self.get_param(item, "y")))
530
+ # Single combined call
531
+ self._call("click_at", {"x": x, "y": y, "button": "left", "count": 1})
532
+ result = self._get_current(item)
533
+ except Exception as e:
534
+ result = self.throw_error(e)
535
+ return self.make_response(item, result)
536
+
537
+ def cmd_hover_at(self, item: dict) -> dict:
538
+ try:
539
+ self._ensure_browser()
540
+ x = self._denorm_x(int(self.get_param(item, "x")))
541
+ y = self._denorm_y(int(self.get_param(item, "y")))
542
+ # Single combined call for hover
543
+ self._call("hover_at", {"x": x, "y": y})
544
+ result = self._get_current(item)
545
+ except Exception as e:
546
+ result = self.throw_error(e)
547
+ return self.make_response(item, result)
548
+
549
+ def cmd_type_text_at(self, item: dict) -> dict:
550
+ try:
551
+ self._ensure_browser()
552
+ x = self._denorm_x(int(self.get_param(item, "x")))
553
+ y = self._denorm_y(int(self.get_param(item, "y")))
554
+ text = self.get_param(item, "text", "") or ""
555
+ press_enter = bool(self.get_param(item, "press_enter", True))
556
+ clear_before = bool(self.get_param(item, "clear_before_typing", True))
557
+ # Single combined call for full flow
558
+ self._call("type_text_at", {
559
+ "x": x,
560
+ "y": y,
561
+ "text": text,
562
+ "press_enter": press_enter,
563
+ "clear_before_typing": clear_before,
564
+ })
565
+ result = self._get_current(item)
566
+ except Exception as e:
567
+ result = self.throw_error(e)
568
+ return self.make_response(item, result)
569
+
570
+ def cmd_key_combination(self, item: dict) -> dict:
571
+ try:
572
+ self._ensure_browser()
573
+ keys = self.get_param(item, "keys", None)
574
+ if isinstance(keys, str):
575
+ parts = [p.strip() for p in keys.replace("+", " ").split() if p.strip()]
576
+ else:
577
+ parts = list(keys or [])
578
+ self._call("keypress_combo", {"keys": parts})
579
+ result = self._get_current(item)
580
+ except Exception as e:
581
+ result = self.throw_error(e)
582
+ return self.make_response(item, result)
583
+
584
+ def cmd_scroll_document(self, item: dict) -> dict:
585
+ try:
586
+ self._ensure_browser()
587
+ direction = str(self.get_param(item, "direction", "down")).lower()
588
+ magnitude = int(self.get_param(item, "magnitude", self._default_scroll_px))
589
+ dx, dy = 0, 0
590
+ if direction == "down":
591
+ dy = magnitude
592
+ elif direction == "up":
593
+ dy = -magnitude
594
+ elif direction == "left":
595
+ dx = -magnitude
596
+ elif direction == "right":
597
+ dx = magnitude
598
+ self._call("scroll", {"dx": dx, "dy": dy})
599
+ result = self._get_current(item)
600
+ except Exception as e:
601
+ result = self.throw_error(e)
602
+ return self.make_response(item, result)
603
+
604
+ def cmd_scroll_at(self, item: dict) -> dict:
605
+ try:
606
+ self._ensure_browser()
607
+ direction = str(self.get_param(item, "direction", "down")).lower()
608
+ magnitude = int(self.get_param(item, "magnitude", self._default_scroll_px))
609
+ x = self._denorm_x(int(self.get_param(item, "x"))) if self.has_param(item, "x") else None
610
+ y = self._denorm_y(int(self.get_param(item, "y"))) if self.has_param(item, "y") else None
611
+ dx, dy = 0, 0
612
+ if direction == "down":
613
+ dy = magnitude
614
+ elif direction == "up":
615
+ dy = -magnitude
616
+ elif direction == "left":
617
+ dx = -magnitude
618
+ elif direction == "right":
619
+ dx = magnitude
620
+ payload = {"dx": dx, "dy": dy}
621
+ if x is not None and y is not None:
622
+ payload["x"] = x
623
+ payload["y"] = y
624
+ self._call("scroll", payload)
625
+ result = self._get_current(item)
626
+ except Exception as e:
627
+ result = self.throw_error(e)
628
+ return self.make_response(item, result)
629
+
630
+ def cmd_drag_and_drop(self, item: dict) -> dict:
631
+ try:
632
+ self._ensure_browser()
633
+ x0 = self._denorm_x(int(self.get_param(item, "x")))
634
+ y0 = self._denorm_y(int(self.get_param(item, "y")))
635
+ x1 = self._denorm_x(int(self.get_param(item, "destination_x")))
636
+ y1 = self._denorm_y(int(self.get_param(item, "destination_y")))
637
+ self._call("drag", {"x": x0, "y": y0, "dx": x1, "dy": y1})
638
+ result = self._get_current(item)
639
+ except Exception as e:
640
+ result = self.throw_error(e)
641
+ return self.make_response(item, result)
642
+
643
+ # ========================= Action-style convenience ========================= #
644
+
645
+ def cmd_click(self, item: dict) -> dict:
646
+ item2 = dict(item)
647
+ if self.has_param(item, "x") and self.has_param(item, "y"):
648
+ x = int(self.get_param(item, "x")); y = int(self.get_param(item, "y"))
649
+ if 0 <= x <= 999 and 0 <= y <= 999:
650
+ item2["params"] = dict(item.get("params", {}))
651
+ item2["params"]["x"] = self._denorm_x(x)
652
+ item2["params"]["y"] = self._denorm_y(y)
653
+ return self.cmd_mouse_click(item2)
654
+
655
+ def cmd_double_click(self, item: dict) -> dict:
656
+ item2 = dict(item)
657
+ p = dict(item.get("params", {}))
658
+ p["num_clicks"] = 2
659
+ item2["params"] = p
660
+ return self.cmd_click(item2)
661
+
662
+ def cmd_move(self, item: dict) -> dict:
663
+ item2 = dict(item)
664
+ p = dict(item.get("params", {}))
665
+ if "x" in p and "y" in p:
666
+ x = int(p["x"]); y = int(p["y"])
667
+ if 0 <= x <= 999 and 0 <= y <= 999:
668
+ p["x"] = self._denorm_x(x); p["y"] = self._denorm_y(y)
669
+ item2["params"] = p
670
+ item2["cmd"] = "mouse_move"
671
+ return self.cmd_mouse_move(item2)
672
+
673
+ def cmd_type_text(self, item: dict) -> dict:
674
+ item2 = dict(item)
675
+ item2["cmd"] = "keyboard_type"
676
+ return self.cmd_keyboard_type(item2)
677
+
678
+ def cmd_keypress(self, item: dict) -> dict:
679
+ try:
680
+ self._ensure_browser()
681
+ keys = self.get_param(item, "keys", []) or []
682
+ for k in keys:
683
+ self._call("keypress", {"key": k})
684
+ result = self._get_current(item)
685
+ except Exception as e:
686
+ result = self.throw_error(e)
687
+ return self.make_response(item, result)
688
+
689
+ def cmd_scroll(self, item: dict) -> dict:
690
+ item2 = dict(item)
691
+ p = dict(item.get("params", {}))
692
+ x = p.get("x", None); y = p.get("y", None)
693
+ if x is not None and y is not None:
694
+ if 0 <= int(x) <= 999 and 0 <= int(y) <= 999:
695
+ p["x"] = self._denorm_x(int(x)); p["y"] = self._denorm_y(int(y))
696
+ dx = int(p.get("scroll_x", p.get("dx", 0))); dy = int(p.get("scroll_y", p.get("dy", 0)))
697
+ p["dx"], p["dy"] = dx, dy
698
+ p["unit"] = "px"
699
+ item2["params"] = p
700
+ item2["cmd"] = "mouse_scroll"
701
+ return self.cmd_mouse_scroll(item2)
702
+
703
+ def cmd_drag(self, item: dict) -> dict:
704
+ try:
705
+ self._ensure_browser()
706
+ path = self.get_param(item, "path", [])
707
+ if not path or len(path) < 2:
708
+ x = self.get_param(item, "x", None)
709
+ y = self.get_param(item, "y", None)
710
+ dx = self.get_param(item, "dx", None)
711
+ dy = self.get_param(item, "dy", None)
712
+ if None in (x, y, dx, dy):
713
+ return self.make_response(item, self._get_current(item))
714
+ pts = [{"x": x, "y": y}, {"x": dx, "y": dy}]
715
+ else:
716
+ pts = path
717
+
718
+ def den(p):
719
+ xx = int(p["x"]); yy = int(p["y"])
720
+ if 0 <= xx <= 999 and 0 <= yy <= 999:
721
+ return self._denorm_x(xx), self._denorm_y(yy)
722
+ return xx, yy
723
+
724
+ x0, y0 = den(pts[0]); x1, y1 = den(pts[1])
725
+ self._call("drag", {"x": x0, "y": y0, "dx": x1, "dy": y1})
726
+ result = self._get_current(item)
727
+ except Exception as e:
728
+ result = self.throw_error(e)
729
+ return self.make_response(item, result)