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.
- pygpt_net/CHANGELOG.txt +7 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app_core.py +4 -2
- pygpt_net/controller/__init__.py +5 -1
- pygpt_net/controller/assistant/assistant.py +1 -4
- pygpt_net/controller/assistant/batch.py +5 -504
- pygpt_net/controller/assistant/editor.py +5 -5
- pygpt_net/controller/assistant/files.py +16 -16
- pygpt_net/controller/chat/handler/google_stream.py +307 -1
- pygpt_net/controller/chat/handler/worker.py +8 -1
- pygpt_net/controller/chat/image.py +2 -2
- pygpt_net/controller/dialogs/confirm.py +73 -101
- pygpt_net/controller/lang/mapping.py +9 -9
- pygpt_net/controller/painter/capture.py +50 -1
- pygpt_net/controller/presets/presets.py +2 -1
- pygpt_net/controller/remote_store/__init__.py +12 -0
- pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
- pygpt_net/controller/remote_store/google/batch.py +402 -0
- pygpt_net/controller/remote_store/google/store.py +615 -0
- pygpt_net/controller/remote_store/openai/__init__.py +12 -0
- pygpt_net/controller/remote_store/openai/batch.py +524 -0
- pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
- pygpt_net/controller/remote_store/remote_store.py +35 -0
- pygpt_net/controller/ui/ui.py +20 -1
- pygpt_net/core/assistants/assistants.py +3 -15
- pygpt_net/core/db/database.py +5 -3
- pygpt_net/core/locale/placeholder.py +35 -0
- pygpt_net/core/remote_store/__init__.py +12 -0
- pygpt_net/core/remote_store/google/__init__.py +11 -0
- pygpt_net/core/remote_store/google/files.py +224 -0
- pygpt_net/core/remote_store/google/store.py +248 -0
- pygpt_net/core/remote_store/openai/__init__.py +11 -0
- pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
- pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
- pygpt_net/core/remote_store/remote_store.py +24 -0
- pygpt_net/data/config/config.json +8 -4
- pygpt_net/data/config/models.json +77 -3
- pygpt_net/data/config/settings.json +45 -0
- pygpt_net/data/locale/locale.de.ini +41 -41
- pygpt_net/data/locale/locale.en.ini +53 -43
- pygpt_net/data/locale/locale.es.ini +41 -41
- pygpt_net/data/locale/locale.fr.ini +41 -41
- pygpt_net/data/locale/locale.it.ini +41 -41
- pygpt_net/data/locale/locale.pl.ini +42 -42
- pygpt_net/data/locale/locale.uk.ini +41 -41
- pygpt_net/data/locale/locale.zh.ini +41 -41
- pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
- pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
- pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
- pygpt_net/item/assistant.py +1 -211
- pygpt_net/item/ctx.py +3 -1
- pygpt_net/item/store.py +238 -0
- pygpt_net/migrations/Version20260102190000.py +35 -0
- pygpt_net/migrations/__init__.py +3 -1
- pygpt_net/plugin/cmd_mouse_control/config.py +470 -1
- pygpt_net/plugin/cmd_mouse_control/plugin.py +488 -22
- pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
- pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
- pygpt_net/plugin/idx_llama_index/config.py +2 -2
- pygpt_net/provider/api/google/__init__.py +16 -54
- pygpt_net/provider/api/google/chat.py +546 -129
- pygpt_net/provider/api/google/computer.py +190 -0
- pygpt_net/provider/api/google/realtime/realtime.py +2 -2
- pygpt_net/provider/api/google/remote_tools.py +93 -0
- pygpt_net/provider/api/google/store.py +546 -0
- pygpt_net/provider/api/google/worker/__init__.py +0 -0
- pygpt_net/provider/api/google/worker/importer.py +392 -0
- pygpt_net/provider/api/openai/computer.py +10 -1
- pygpt_net/provider/api/openai/store.py +6 -6
- pygpt_net/provider/api/openai/worker/importer.py +24 -24
- pygpt_net/provider/core/config/patch.py +16 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
- pygpt_net/provider/core/model/patch.py +17 -3
- pygpt_net/provider/core/preset/json_file.py +13 -7
- pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
- pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
- pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
- pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
- pygpt_net/provider/llms/google.py +2 -2
- pygpt_net/ui/base/config_dialog.py +3 -2
- pygpt_net/ui/dialog/assistant.py +3 -3
- pygpt_net/ui/dialog/plugins.py +3 -1
- pygpt_net/ui/dialog/remote_store_google.py +539 -0
- pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
- pygpt_net/ui/dialogs.py +5 -3
- pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
- pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
- pygpt_net/ui/menu/tools.py +13 -5
- pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
- pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
- pygpt_net/ui/widget/element/button.py +4 -4
- pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
- pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
- pygpt_net/ui/widget/option/checkbox_list.py +47 -9
- pygpt_net/ui/widget/option/combo.py +39 -3
- {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/METADATA +33 -2
- {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/RECORD +133 -108
- {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/entry_points.txt +0 -0
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.02 02:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
|
+
import time
|
|
12
|
+
import os
|
|
11
13
|
|
|
12
14
|
from PySide6.QtCore import Slot, QTimer
|
|
13
15
|
|
|
@@ -31,22 +33,54 @@ class Plugin(BasePlugin):
|
|
|
31
33
|
self.prefix = "Mouse"
|
|
32
34
|
self.order = 100
|
|
33
35
|
self.allowed_cmds = [
|
|
36
|
+
"open_web_browser",
|
|
34
37
|
"get_mouse_position",
|
|
35
38
|
"mouse_move",
|
|
39
|
+
"mouse_drag",
|
|
36
40
|
"mouse_click",
|
|
37
41
|
"mouse_scroll",
|
|
38
|
-
"mouse_drag",
|
|
39
42
|
"get_screenshot",
|
|
40
43
|
"keyboard_key",
|
|
41
44
|
"keyboard_keys",
|
|
42
45
|
"keyboard_type",
|
|
43
46
|
"wait",
|
|
47
|
+
"wait_5_seconds",
|
|
48
|
+
"go_back",
|
|
49
|
+
"go_forward",
|
|
50
|
+
"search",
|
|
51
|
+
"navigate",
|
|
52
|
+
"click_at",
|
|
53
|
+
"hover_at",
|
|
54
|
+
"type_text_at",
|
|
55
|
+
"key_combination",
|
|
56
|
+
"scroll_document",
|
|
57
|
+
"scroll_at",
|
|
58
|
+
"drag_and_drop",
|
|
59
|
+
"click",
|
|
60
|
+
"double_click",
|
|
61
|
+
"move",
|
|
62
|
+
"type",
|
|
63
|
+
"keypress",
|
|
64
|
+
"scroll",
|
|
65
|
+
"drag"
|
|
44
66
|
]
|
|
45
67
|
self.use_locale = True
|
|
46
68
|
self.worker = None
|
|
47
69
|
self.config = Config(self)
|
|
48
70
|
self.init_options()
|
|
49
71
|
|
|
72
|
+
# Playwright sandbox context
|
|
73
|
+
self.page = None
|
|
74
|
+
self.pw = None
|
|
75
|
+
self.browser = None
|
|
76
|
+
self.context = None
|
|
77
|
+
self.viewport_w = 1440
|
|
78
|
+
self.viewport_h = 900
|
|
79
|
+
|
|
80
|
+
# Pointer tracking for sandbox operations
|
|
81
|
+
self.pointer_x = 0
|
|
82
|
+
self.pointer_y = 0
|
|
83
|
+
|
|
50
84
|
def init_options(self):
|
|
51
85
|
"""Initialize options"""
|
|
52
86
|
self.config.from_defaults(self)
|
|
@@ -85,6 +119,30 @@ class Plugin(BasePlugin):
|
|
|
85
119
|
if self.has_cmd(option):
|
|
86
120
|
data['cmd'].append(self.get_cmd(option)) # append command
|
|
87
121
|
|
|
122
|
+
def is_sandbox(self) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Check if sandbox mode is enabled
|
|
125
|
+
|
|
126
|
+
:return: True if sandbox mode is enabled
|
|
127
|
+
"""
|
|
128
|
+
return bool(self.window.core.config.get("remote_tools.computer_use.sandbox", False))
|
|
129
|
+
|
|
130
|
+
def get_worker(self):
|
|
131
|
+
"""
|
|
132
|
+
Get Worker instance based on sandbox option
|
|
133
|
+
|
|
134
|
+
:return: Worker instance (native or sandboxed/Playwright)
|
|
135
|
+
"""
|
|
136
|
+
if self.is_sandbox():
|
|
137
|
+
from .worker_sandbox import Worker
|
|
138
|
+
worker = Worker()
|
|
139
|
+
worker.signals.start.connect(self.on_playwright_start)
|
|
140
|
+
# Connect main-thread Playwright control channel
|
|
141
|
+
worker.signals.call.connect(self.on_playwright_call)
|
|
142
|
+
return worker
|
|
143
|
+
from .worker import Worker
|
|
144
|
+
return Worker()
|
|
145
|
+
|
|
88
146
|
def cmd(self, ctx: CtxItem, cmds: list):
|
|
89
147
|
"""
|
|
90
148
|
Event: CMD_EXECUTE
|
|
@@ -92,8 +150,6 @@ class Plugin(BasePlugin):
|
|
|
92
150
|
:param ctx: CtxItem
|
|
93
151
|
:param cmds: commands dict
|
|
94
152
|
"""
|
|
95
|
-
from .worker import Worker
|
|
96
|
-
|
|
97
153
|
is_cmd = False
|
|
98
154
|
my_commands = []
|
|
99
155
|
for item in cmds:
|
|
@@ -108,7 +164,7 @@ class Plugin(BasePlugin):
|
|
|
108
164
|
self.cmd_prepare(ctx, my_commands)
|
|
109
165
|
|
|
110
166
|
try:
|
|
111
|
-
worker =
|
|
167
|
+
worker = self.get_worker()
|
|
112
168
|
worker.from_defaults(self)
|
|
113
169
|
worker.cmds = my_commands
|
|
114
170
|
worker.ctx = ctx
|
|
@@ -129,15 +185,27 @@ class Plugin(BasePlugin):
|
|
|
129
185
|
:param item: command item to execute
|
|
130
186
|
:return:
|
|
131
187
|
"""
|
|
132
|
-
from .worker import Worker
|
|
133
|
-
|
|
134
188
|
item["params"]["no_screenshot"] = True # do not take screenshot for single command call
|
|
135
|
-
worker =
|
|
189
|
+
worker = self.get_worker()
|
|
136
190
|
worker.from_defaults(self)
|
|
137
191
|
worker.cmds = [item]
|
|
138
192
|
worker.ctx = CtxItem()
|
|
139
193
|
worker.run() # sync
|
|
140
194
|
|
|
195
|
+
def is_google(self, ctx: CtxItem) -> bool:
|
|
196
|
+
"""
|
|
197
|
+
Check if full response is required based on model provider
|
|
198
|
+
|
|
199
|
+
:param ctx: context (CtxItem)
|
|
200
|
+
:return: True if full response is required, False otherwise
|
|
201
|
+
"""
|
|
202
|
+
model_id = ctx.model
|
|
203
|
+
model_data = self.window.core.models.get(model_id) if model_id else None
|
|
204
|
+
if model_data is not None:
|
|
205
|
+
if model_data.provider == "google":
|
|
206
|
+
return True
|
|
207
|
+
return False
|
|
208
|
+
|
|
141
209
|
@Slot(list, object, dict)
|
|
142
210
|
def handle_finished_more(self, responses: list, ctx: CtxItem = None, extra_data: dict = None):
|
|
143
211
|
"""
|
|
@@ -155,7 +223,8 @@ class Plugin(BasePlugin):
|
|
|
155
223
|
and response["result"]["no_screenshot"]):
|
|
156
224
|
with_screenshot = False
|
|
157
225
|
if ctx is not None:
|
|
158
|
-
|
|
226
|
+
print("APPEND RESPONSE", response)
|
|
227
|
+
self.prepare_reply_ctx(response, ctx)
|
|
159
228
|
ctx.reply = True
|
|
160
229
|
self.handle_delayed(ctx, with_screenshot)
|
|
161
230
|
|
|
@@ -173,9 +242,11 @@ class Plugin(BasePlugin):
|
|
|
173
242
|
|
|
174
243
|
context = BridgeContext()
|
|
175
244
|
context.ctx = ctx
|
|
245
|
+
extra = {}
|
|
246
|
+
extra["response_type"] = "multiple"
|
|
176
247
|
event = KernelEvent(KernelEvent.REPLY_ADD, {
|
|
177
248
|
'context': context,
|
|
178
|
-
'extra':
|
|
249
|
+
'extra': extra,
|
|
179
250
|
})
|
|
180
251
|
self.window.dispatch(event)
|
|
181
252
|
|
|
@@ -186,18 +257,413 @@ class Plugin(BasePlugin):
|
|
|
186
257
|
:param ctx: context (CtxItem)
|
|
187
258
|
"""
|
|
188
259
|
self.window.controller.attachment.clear_silent()
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
260
|
+
if self.is_sandbox():
|
|
261
|
+
path = self.window.controller.painter.capture.screenshot_playwright(page=self.page, silent=True) # Playwright screenshot
|
|
262
|
+
else:
|
|
263
|
+
path = self.window.controller.painter.capture.screenshot(attach_cursor=True, silent=True) # attach screenshot
|
|
264
|
+
if path:
|
|
265
|
+
img_path = self.window.core.filesystem.make_local(path)
|
|
266
|
+
ctx.images_before.append(img_path)
|
|
267
|
+
context = BridgeContext()
|
|
268
|
+
context.ctx = ctx
|
|
269
|
+
event = KernelEvent(KernelEvent.REPLY_ADD, {
|
|
270
|
+
'context': context,
|
|
271
|
+
'extra': {},
|
|
272
|
+
})
|
|
273
|
+
self.window.dispatch(event)
|
|
274
|
+
|
|
275
|
+
@Slot()
|
|
276
|
+
def on_playwright_start(self):
|
|
277
|
+
"""
|
|
278
|
+
Event: PLAYWRIGHT_START
|
|
279
|
+
"""
|
|
280
|
+
self._ensure_browser()
|
|
281
|
+
|
|
282
|
+
def _ensure_browser(self):
|
|
283
|
+
"""Start Playwright browser and page if not started yet."""
|
|
284
|
+
if self.page:
|
|
285
|
+
# check if browser is still connected
|
|
286
|
+
try:
|
|
287
|
+
self.page.screenshot()
|
|
288
|
+
return
|
|
289
|
+
except Exception as e:
|
|
290
|
+
print(e)
|
|
291
|
+
if self.page:
|
|
292
|
+
try:
|
|
293
|
+
self.page.close()
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
self.page = None
|
|
297
|
+
if self.browser:
|
|
298
|
+
try:
|
|
299
|
+
self.browser.close()
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
self.browser = None
|
|
303
|
+
if self.pw:
|
|
304
|
+
try:
|
|
305
|
+
self.pw.stop()
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
self.pw = None
|
|
309
|
+
if self.context:
|
|
310
|
+
try:
|
|
311
|
+
self.context.close()
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
self.context = None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# Playwright is required in sandbox mode
|
|
318
|
+
try:
|
|
319
|
+
from playwright.sync_api import sync_playwright
|
|
320
|
+
except Exception:
|
|
321
|
+
sync_playwright = None
|
|
322
|
+
|
|
323
|
+
if sync_playwright is None:
|
|
324
|
+
raise RuntimeError("Playwright is not installed. Please install 'playwright' and the chosen browser.")
|
|
200
325
|
|
|
326
|
+
# Determine headless and engine from plugin options if available
|
|
327
|
+
headless = True
|
|
328
|
+
engine = "chromium"
|
|
329
|
+
try:
|
|
330
|
+
if hasattr(self, "get_option_value"):
|
|
331
|
+
hv = self.get_option_value("sandbox_headless")
|
|
332
|
+
if hv is not None:
|
|
333
|
+
headless = bool(hv)
|
|
334
|
+
ev = self.get_option_value("sandbox_browser")
|
|
335
|
+
if ev in ("chromium", "firefox", "webkit"):
|
|
336
|
+
engine = ev
|
|
337
|
+
vw = int(self.get_option_value("sandbox_viewport_w") or self.viewport_w)
|
|
338
|
+
vh = int(self.get_option_value("sandbox_viewport_h") or self.viewport_h)
|
|
339
|
+
self.viewport_w, self.viewport_h = vw, vh
|
|
340
|
+
except Exception:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
args = []
|
|
344
|
+
cfg_engine = self.get_option_value("sandbox_engine")
|
|
345
|
+
if cfg_engine:
|
|
346
|
+
engine = cfg_engine
|
|
347
|
+
cfg_args = self.get_option_value("sandbox_args")
|
|
348
|
+
if cfg_args:
|
|
349
|
+
args = [arg.strip() for arg in cfg_args.split(",") if arg.strip()]
|
|
350
|
+
cfg_path = self.get_option_value("sandbox_path")
|
|
351
|
+
if cfg_path:
|
|
352
|
+
os.environ['PLAYWRIGHT_BROWSERS_PATH'] = cfg_path
|
|
353
|
+
err_msg = None
|
|
354
|
+
if cfg_path and not (os.path.exists(cfg_path) and os.path.isdir(cfg_path)):
|
|
355
|
+
err_msg = (f"Playwright browsers path does not exist: {cfg_path}\n\n"
|
|
356
|
+
f"1) Please install Playwright browser(s) on host machine: \n\n"
|
|
357
|
+
f"pip install playwright && playwright install {engine}\n\n"
|
|
358
|
+
f"2) Set path to browsers directory in `Mouse And Keyboard` plugin settings option: "
|
|
359
|
+
f"`Sandbox: Playwright browsers path`.")
|
|
360
|
+
else:
|
|
361
|
+
if os.environ.get("APPIMAGE") and not cfg_path: # set path is required for AppImage version
|
|
362
|
+
err_msg = (f"Playwright browsers path is not set - "
|
|
363
|
+
f"1) Please install Playwright browser(s) on host machine: \n\n"
|
|
364
|
+
f"pip install playwright && playwright install {engine}\n\n"
|
|
365
|
+
f"2) Set path to browsers directory in `Mouse And Keyboard` plugin settings option: "
|
|
366
|
+
f"`Sandbox: Playwright browsers path`.")
|
|
367
|
+
|
|
368
|
+
if err_msg is not None:
|
|
369
|
+
self.error(err_msg)
|
|
370
|
+
raise RuntimeError(err_msg)
|
|
371
|
+
|
|
372
|
+
self.pw = sync_playwright().start()
|
|
373
|
+
launcher = getattr(self.pw, engine)
|
|
374
|
+
self.browser = launcher.launch(
|
|
375
|
+
headless=headless,
|
|
376
|
+
args=args
|
|
377
|
+
)
|
|
378
|
+
self.context = self.browser.new_context(viewport={"width": self.viewport_w, "height": self.viewport_h})
|
|
379
|
+
self.page = self.context.new_page()
|
|
380
|
+
self.page.goto(self.get_option_value("sandbox_home"))
|
|
381
|
+
|
|
382
|
+
# ========================= Playwright ops in main thread (from worker) ========================= #
|
|
383
|
+
|
|
384
|
+
@Slot(str, dict, object, object)
|
|
385
|
+
def on_playwright_call(self, op: str, params: dict, ret: dict, done):
|
|
386
|
+
"""
|
|
387
|
+
Execute Playwright operation in the main (GUI) thread.
|
|
388
|
+
'ret' is a mutable dict to fill with results. 'done' is a threading.Event to signal completion.
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
# Always ensure browser exists before any operation
|
|
392
|
+
if op in ("ensure", "navigate", "go_back", "go_forward", "move", "click",
|
|
393
|
+
"scroll", "drag", "keypress", "keypress_combo", "type",
|
|
394
|
+
"screenshot", "click_at", "hover_at", "type_text_at"):
|
|
395
|
+
self._ensure_browser()
|
|
396
|
+
|
|
397
|
+
if op == "ensure":
|
|
398
|
+
vw, vh = self._get_viewport()
|
|
399
|
+
ret.update({"ok": True, "viewport_w": vw, "viewport_h": vh, "url": self._get_url(),
|
|
400
|
+
"mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
401
|
+
|
|
402
|
+
elif op == "navigate":
|
|
403
|
+
url = str(params.get("url", "") or "")
|
|
404
|
+
if url:
|
|
405
|
+
self.page.goto(url, wait_until="load")
|
|
406
|
+
self._wait_for_load()
|
|
407
|
+
vw, vh = self._get_viewport()
|
|
408
|
+
ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
|
|
409
|
+
"mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
410
|
+
|
|
411
|
+
elif op == "go_back":
|
|
412
|
+
self.page.go_back(wait_until="load")
|
|
413
|
+
vw, vh = self._get_viewport()
|
|
414
|
+
self._wait_for_load()
|
|
415
|
+
ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
|
|
416
|
+
"mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
417
|
+
|
|
418
|
+
elif op == "go_forward":
|
|
419
|
+
self.page.go_forward(wait_until="load")
|
|
420
|
+
self._wait_for_load()
|
|
421
|
+
vw, vh = self._get_viewport()
|
|
422
|
+
ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
|
|
423
|
+
"mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
424
|
+
|
|
425
|
+
elif op == "move":
|
|
426
|
+
x = int(params.get("x"))
|
|
427
|
+
y = int(params.get("y"))
|
|
428
|
+
self.page.mouse.move(x, y)
|
|
429
|
+
self.pointer_x, self.pointer_y = x, y
|
|
430
|
+
self._wait_for_load()
|
|
431
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
|
|
432
|
+
|
|
433
|
+
elif op == "click":
|
|
434
|
+
button = str(params.get("button", "left")).lower()
|
|
435
|
+
count = int(params.get("count", 1))
|
|
436
|
+
x = params.get("x", None)
|
|
437
|
+
y = params.get("y", None)
|
|
438
|
+
if x is None or y is None:
|
|
439
|
+
x, y = self.pointer_x, self.pointer_y
|
|
440
|
+
else:
|
|
441
|
+
x, y = int(x), int(y)
|
|
442
|
+
self.page.mouse.move(x, y)
|
|
443
|
+
self.pointer_x, self.pointer_y = x, y
|
|
444
|
+
self.page.mouse.click(x, y, button=button, click_count=max(1, count))
|
|
445
|
+
self._wait_for_load()
|
|
446
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
|
|
447
|
+
|
|
448
|
+
elif op == "click_at":
|
|
449
|
+
x = int(params.get("x"))
|
|
450
|
+
y = int(params.get("y"))
|
|
451
|
+
button = str(params.get("button", "left")).lower()
|
|
452
|
+
count = int(params.get("count", 1))
|
|
453
|
+
self.page.mouse.move(x, y)
|
|
454
|
+
self.pointer_x, self.pointer_y = x, y
|
|
455
|
+
self.page.mouse.click(x, y, button=button, click_count=max(1, count))
|
|
456
|
+
self._wait_for_load()
|
|
457
|
+
print("cliecked at", x, y)
|
|
458
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
|
|
459
|
+
|
|
460
|
+
elif op == "hover_at":
|
|
461
|
+
x = int(params.get("x"))
|
|
462
|
+
y = int(params.get("y"))
|
|
463
|
+
self.page.mouse.move(x, y)
|
|
464
|
+
self.pointer_x, self.pointer_y = x, y
|
|
465
|
+
self._wait_for_load()
|
|
466
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
|
|
467
|
+
|
|
468
|
+
elif op == "type_text_at":
|
|
469
|
+
x = int(params.get("x"))
|
|
470
|
+
y = int(params.get("y"))
|
|
471
|
+
text = str(params.get("text", "") or "")
|
|
472
|
+
press_enter = bool(params.get("press_enter", True))
|
|
473
|
+
clear_before = bool(params.get("clear_before_typing", True))
|
|
474
|
+
# Focus target
|
|
475
|
+
self.page.mouse.move(x, y)
|
|
476
|
+
self.pointer_x, self.pointer_y = x, y
|
|
477
|
+
self.page.mouse.click(x, y, button="left", click_count=1)
|
|
478
|
+
# Optional clear
|
|
479
|
+
if clear_before:
|
|
480
|
+
# Use Control+A universally; adjust if needed for macOS
|
|
481
|
+
self._press_combo(["Control", "a"])
|
|
482
|
+
self.page.keyboard.press("Backspace")
|
|
483
|
+
# Type
|
|
484
|
+
if text:
|
|
485
|
+
self.page.keyboard.type(text)
|
|
486
|
+
if press_enter:
|
|
487
|
+
self.page.keyboard.press("Enter")
|
|
488
|
+
self._wait_for_load()
|
|
489
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
490
|
+
|
|
491
|
+
elif op == "scroll":
|
|
492
|
+
x = params.get("x", None)
|
|
493
|
+
y = params.get("y", None)
|
|
494
|
+
if x is not None and y is not None:
|
|
495
|
+
xx, yy = int(x), int(y)
|
|
496
|
+
self.page.mouse.move(xx, yy)
|
|
497
|
+
self.pointer_x, self.pointer_y = xx, yy
|
|
498
|
+
dx = int(params.get("dx", 0))
|
|
499
|
+
dy = int(params.get("dy", 0))
|
|
500
|
+
self.page.mouse.wheel(dx, dy)
|
|
501
|
+
self._wait_for_load()
|
|
502
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
503
|
+
|
|
504
|
+
elif op == "drag":
|
|
505
|
+
x = int(params.get("x"))
|
|
506
|
+
y = int(params.get("y"))
|
|
507
|
+
dx = int(params.get("dx"))
|
|
508
|
+
dy = int(params.get("dy"))
|
|
509
|
+
self.page.mouse.move(x, y)
|
|
510
|
+
self.pointer_x, self.pointer_y = x, y
|
|
511
|
+
self.page.mouse.down(button="left")
|
|
512
|
+
self.page.mouse.move(dx, dy, steps=12)
|
|
513
|
+
self.page.mouse.up(button="left")
|
|
514
|
+
self.pointer_x, self.pointer_y = dx, dy
|
|
515
|
+
self._wait_for_load()
|
|
516
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
517
|
+
|
|
518
|
+
elif op == "keypress":
|
|
519
|
+
key = str(params.get("key"))
|
|
520
|
+
self.page.keyboard.press(self._key_to_playwright(key))
|
|
521
|
+
self._wait_for_load()
|
|
522
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
523
|
+
|
|
524
|
+
elif op == "keypress_combo":
|
|
525
|
+
keys = params.get("keys", []) or []
|
|
526
|
+
if isinstance(keys, str):
|
|
527
|
+
keys = [p.strip() for p in keys.replace("+", " ").split() if p.strip()]
|
|
528
|
+
self._press_combo(keys)
|
|
529
|
+
self._wait_for_load()
|
|
530
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
531
|
+
|
|
532
|
+
elif op == "type":
|
|
533
|
+
text = str(params.get("text", "") or "")
|
|
534
|
+
modifier = params.get("modifier", None)
|
|
535
|
+
if modifier:
|
|
536
|
+
mod = self._key_to_playwright(modifier)
|
|
537
|
+
self.page.keyboard.down(mod)
|
|
538
|
+
try:
|
|
539
|
+
self.page.keyboard.type(text)
|
|
540
|
+
finally:
|
|
541
|
+
self.page.keyboard.up(mod)
|
|
542
|
+
else:
|
|
543
|
+
self.page.keyboard.type(text)
|
|
544
|
+
self._wait_for_load()
|
|
545
|
+
ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
546
|
+
|
|
547
|
+
elif op == "screenshot":
|
|
548
|
+
full = bool(params.get("full_page", False))
|
|
549
|
+
img_bytes = self.page.screenshot(full_page=full)
|
|
550
|
+
vw, vh = self._get_viewport()
|
|
551
|
+
ret.update({"ok": True, "image": img_bytes, "url": self._get_url(),
|
|
552
|
+
"viewport_w": vw, "viewport_h": vh,
|
|
553
|
+
"mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
|
|
554
|
+
|
|
555
|
+
else:
|
|
556
|
+
ret.update({"ok": False, "error": f"Unknown op: {op}", "url": self._get_url()})
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
ret.update({"ok": False, "error": str(e), "url": self._get_url()})
|
|
560
|
+
finally:
|
|
561
|
+
try:
|
|
562
|
+
done.set()
|
|
563
|
+
except Exception:
|
|
564
|
+
pass
|
|
565
|
+
|
|
566
|
+
def _wait_for_load(self, timeout: int = 5000):
|
|
567
|
+
"""Wait for page load state."""
|
|
568
|
+
if self.page:
|
|
569
|
+
self.page.wait_for_load_state(timeout=timeout)
|
|
570
|
+
time.sleep(1)
|
|
571
|
+
|
|
572
|
+
# ========================= Helpers for Playwright ops ========================= #
|
|
573
|
+
|
|
574
|
+
def get_last_url(self):
|
|
575
|
+
"""
|
|
576
|
+
Get current page URL
|
|
577
|
+
|
|
578
|
+
:return: URL string
|
|
579
|
+
"""
|
|
580
|
+
return self._get_url()
|
|
581
|
+
|
|
582
|
+
def _get_url(self) -> str:
|
|
583
|
+
try:
|
|
584
|
+
if self.page:
|
|
585
|
+
return self.page.url or ""
|
|
586
|
+
except Exception:
|
|
587
|
+
return ""
|
|
588
|
+
return ""
|
|
589
|
+
|
|
590
|
+
def _get_viewport(self):
|
|
591
|
+
try:
|
|
592
|
+
if self.page:
|
|
593
|
+
vp = self.page.viewport_size
|
|
594
|
+
if callable(vp):
|
|
595
|
+
vp = self.page.viewport_size()
|
|
596
|
+
if isinstance(vp, dict) and "width" in vp and "height" in vp:
|
|
597
|
+
self.viewport_w = int(vp["width"])
|
|
598
|
+
self.viewport_h = int(vp["height"])
|
|
599
|
+
except Exception:
|
|
600
|
+
pass
|
|
601
|
+
return self.viewport_w, self.viewport_h
|
|
602
|
+
|
|
603
|
+
def _key_to_playwright(self, key: str) -> str:
|
|
604
|
+
mapping = {
|
|
605
|
+
"PAGEDOWN": "PageDown",
|
|
606
|
+
"PAGEUP": "PageUp",
|
|
607
|
+
"BACKSPACE": "Backspace",
|
|
608
|
+
"RETURN": "Enter",
|
|
609
|
+
"ENTER": "Enter",
|
|
610
|
+
"ESCAPE": "Escape",
|
|
611
|
+
"ESC": "Escape",
|
|
612
|
+
"LEFT": "ArrowLeft",
|
|
613
|
+
"RIGHT": "ArrowRight",
|
|
614
|
+
"UP": "ArrowUp",
|
|
615
|
+
"DOWN": "ArrowDown",
|
|
616
|
+
"SPACE": " ",
|
|
617
|
+
"TAB": "Tab",
|
|
618
|
+
"CTRL": "Control",
|
|
619
|
+
"CONTROL": "Control",
|
|
620
|
+
"ALT": "Alt",
|
|
621
|
+
"SHIFT": "Shift",
|
|
622
|
+
"CMD": "Meta",
|
|
623
|
+
"SUPER": "Meta",
|
|
624
|
+
"START": "Meta",
|
|
625
|
+
"PRINTSCREEN": "PrintScreen",
|
|
626
|
+
"PRINT_SCREEN": "PrintScreen",
|
|
627
|
+
"PRTSC": "PrintScreen",
|
|
628
|
+
"END": "End",
|
|
629
|
+
"HOME": "Home",
|
|
630
|
+
"DELETE": "Delete",
|
|
631
|
+
"INSERT": "Insert",
|
|
632
|
+
}
|
|
633
|
+
u = key.upper() if isinstance(key, str) else key
|
|
634
|
+
if u in mapping:
|
|
635
|
+
return mapping[u]
|
|
636
|
+
if isinstance(u, str) and len(u) == 2 and u[0] == "F" and u[1].isdigit():
|
|
637
|
+
return u
|
|
638
|
+
if isinstance(u, str) and len(u) == 3 and u[0] == "F" and u[1:].isdigit():
|
|
639
|
+
return u
|
|
640
|
+
return key
|
|
641
|
+
|
|
642
|
+
def _press_combo(self, keys, delay: float = 0.0):
|
|
643
|
+
if not keys:
|
|
644
|
+
return
|
|
645
|
+
# Try chord, e.g., "Control+L"
|
|
646
|
+
if len(keys) >= 2:
|
|
647
|
+
chord = "+".join(self._key_to_playwright(k) for k in keys)
|
|
648
|
+
try:
|
|
649
|
+
self.page.keyboard.press(chord, delay=delay)
|
|
650
|
+
return
|
|
651
|
+
except Exception:
|
|
652
|
+
pass
|
|
653
|
+
# Fallback: modifier down/up sequence
|
|
654
|
+
modifiers = {"Control", "Alt", "Shift", "Meta"}
|
|
655
|
+
downs = []
|
|
656
|
+
try:
|
|
657
|
+
for k in keys:
|
|
658
|
+
pk = self._key_to_playwright(k)
|
|
659
|
+
if pk in modifiers and pk not in downs:
|
|
660
|
+
self.page.keyboard.down(pk)
|
|
661
|
+
downs.append(pk)
|
|
662
|
+
else:
|
|
663
|
+
self.page.keyboard.press(pk, delay=delay)
|
|
664
|
+
finally:
|
|
665
|
+
for m in reversed(downs):
|
|
666
|
+
self.page.keyboard.up(m)
|
|
201
667
|
|
|
202
668
|
def on_system_prompt(self, prompt: str) -> str:
|
|
203
669
|
"""
|
|
@@ -208,4 +674,4 @@ class Plugin(BasePlugin):
|
|
|
208
674
|
"""
|
|
209
675
|
if prompt is not None and prompt.strip() != "":
|
|
210
676
|
prompt += "\n\n"
|
|
211
|
-
return prompt + self.get_option_value("prompt")
|
|
677
|
+
return prompt + self.get_option_value("prompt")
|