answer42 0.7.0__tar.gz → 0.8.0__tar.gz
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.
- {answer42-0.7.0 → answer42-0.8.0}/PKG-INFO +19 -4
- {answer42-0.7.0 → answer42-0.8.0}/README.md +18 -3
- {answer42-0.7.0 → answer42-0.8.0}/docs/architecture.md +5 -2
- {answer42-0.7.0 → answer42-0.8.0}/pyproject.toml +1 -1
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/html_agent.py +283 -21
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/runtime.py +69 -1
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/server.py +170 -15
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/tool_groups.py +1 -1
- {answer42-0.7.0 → answer42-0.8.0}/.gitignore +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/LICENSE +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/credentials.example.json +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/docs/agent-installation.md +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/docs/assets/answer42-logo.png +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/docs/assets/platform42-logo.svg +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/docs/installation.md +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/scripts/build_cf.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/scripts/e2e_http_smoke.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/scripts/e2e_stable.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/scripts/openclaw_mcp_autoreload.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/scripts/rag_cli.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/scripts/read_webclient_start_info.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/Configuration.xml +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Catalogs//320/237/320/241_/320/222/320/273/320/260/320/264/320/265/320/273/320/265/321/206.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Catalogs//320/237/320/241_/320/241/320/277/321/200/320/260/320/262/320/276/321/207/320/275/320/270/320/2721.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Configuration.xml +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Ext/ManagerModule.bsl" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Ext/ObjectModule.bsl" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Templates//320/236/321/201/320/275/320/276/320/262/320/275/320/260/321/217/320/241/321/205/320/265/320/274/320/260/320/232/320/276/320/274/320/277/320/276/320/275/320/276/320/262/320/272/320/270/320/224/320/260/320/275/320/275/321/213/321/205/Ext/Template.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Templates//320/236/321/201/320/275/320/276/320/262/320/275/320/260/321/217/320/241/321/205/320/265/320/274/320/260/320/232/320/276/320/274/320/277/320/276/320/275/320/276/320/262/320/272/320/270/320/224/320/260/320/275/320/275/321/213/321/205.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262.xml" +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/__init__.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/assets/__init__.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/auth.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/bridge.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/credentials.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/credentials_cli.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/os_support.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/platform.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/protocol.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/__init__.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/detect.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/dump.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/model.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/parsers.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/service.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/store.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/summary_backend.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/rag/summary_vector.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/recorder.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/release_helper.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/skill_installer.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/webclient_probe.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/webkit_patch.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/win_desktop.py +0 -0
- {answer42-0.7.0 → answer42-0.8.0}/src/mcp_1c/window_control.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: answer42
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Answer42 — The Answer to Life, Universe, and 1C — UI Driver. MCP-powered 1C:Enterprise UI automation: click, fill, navigate, test, and introspect managed forms through the test-client API
|
|
5
5
|
Author: Marvin (AI Assistant), 42Clouds, and contributors
|
|
6
6
|
Author-email: "Kosolapov Stanislav (proDOOMman)" <prodoomman@gmail.com>
|
|
@@ -229,7 +229,17 @@ answer42
|
|
|
229
229
|
|
|
230
230
|
**Статический HTML** — инструменты группы `forms` через API клиента тестирования: `html_field_content` (HTML поля в том виде, в каком страница загрузилась, и её видимый текст), `html_field_click_link`, `html_field_input`.
|
|
231
231
|
|
|
232
|
-
**Живые страницы** — группа `html`.
|
|
232
|
+
**Живые страницы** — группа `html`. Способ доступа к встроенному WebKit клиента выбирает параметр `start_session(html_mode=...)` (значение по умолчанию можно задать переменной `ONEC_MCP_HTML_MODE`):
|
|
233
|
+
|
|
234
|
+
| `html_mode` | Где | Как |
|
|
235
|
+
|---|---|---|
|
|
236
|
+
| `auto` (по умолчанию) | везде | Linux — `remote`; Windows — `patched`, если в Web Inspector платформы установлен актуальный мост, иначе `console` |
|
|
237
|
+
| `console` | Windows | горячая клавиша инспектора и короткий загрузчик, введённый в его консоль |
|
|
238
|
+
| `patched` | Windows, Linux | мост, встроенный в интерфейс инспектора: на Windows — в Web Inspector платформы (сначала `answer42 webkit-inspector install`, см. ниже), на Linux — подставляется для сессии, ничего устанавливать не нужно |
|
|
239
|
+
| `remote` | Linux | удалённый отладчик WebKitGTK (`WEBKIT_INSPECTOR_SERVER`) |
|
|
240
|
+
| `disabled` | везде | мост не запускается, инструменты `html_*` недоступны |
|
|
241
|
+
|
|
242
|
+
Явно выбранный режим не подменяется другим: если он недоступен (`patched` без моста в платформе, режим другой ОС), `start_session` сразу возвращает ошибку с подсказкой. Мост ничего не делает, пока не вызван инструмент `html_*`: на Windows инспектор открывается только при первом вызове, на Linux клиент запускается со встроенным сервером отладки.
|
|
233
243
|
|
|
234
244
|
| Инструмент | Что делает |
|
|
235
245
|
|---|---|
|
|
@@ -241,6 +251,7 @@ answer42
|
|
|
241
251
|
| `html_wait` | дождаться появления или исчезновения текста / элемента |
|
|
242
252
|
| `html_eval` | выполнить произвольный JavaScript в странице — **только с `--dev-tools`** (см. ниже) |
|
|
243
253
|
| `html_console` | ошибки и предупреждения консоли страницы и скрипты, которые эта сборка WebKit не разбирает (например `?.`) |
|
|
254
|
+
| `html_download` | скачать файл по ссылке страницы изнутри страницы (с её cookie и сеансом) и сохранить в каталог сессии: путь, размер, тип, HTTP-статус |
|
|
244
255
|
| `html_inspector_patch` | Windows: встроить мост в Web Inspector установленных платформ 1С или убрать его (необязательно, см. ниже) |
|
|
245
256
|
|
|
246
257
|
Если живых страниц несколько, нужная выбирается параметром `page` (id из `html_pages` или часть URL / заголовка).
|
|
@@ -250,8 +261,12 @@ answer42
|
|
|
250
261
|
Как это устроено:
|
|
251
262
|
|
|
252
263
|
- **Windows.** Встроенный WebKit клиента 1С не имеет сервера удалённой отладки, но его Web Inspector открывается по Ctrl+Alt+Shift+F12. При первом вызове `html_*` Answer42 открывает инспектор поля (он встраивается в нижнюю часть формы), переключает его на вкладку Console и вводит в консоль короткий загрузчик. Загрузчик подключается к локальному WebSocket Answer42 по одноразовому токену, получает мост и выполняет операции в странице. Подключение к странице занимает около 2–3 секунд. После перезагрузки страницы загрузчик вводится заново. Платформа используется как установлена.
|
|
253
|
-
- **Windows, мост в инспекторе платформы (
|
|
254
|
-
- **Linux.** HTML-поля рисует системный WebKitGTK. Answer42 запускает тест-клиент с `WEBKIT_INSPECTOR_SERVER=127.0.0.1:<порт>` и подключается к удалённому отладчику WebKitGTK напрямую: окна инспектора и ввода с клавиатуры нет.
|
|
264
|
+
- **Windows, мост в инспекторе платформы (`html_mode=patched`).** `answer42 webkit-inspector install` (или инструмент `html_inspector_patch(action="install")`) встраивает мост в Web Inspector установленной платформы: `bin\WebKit.resources\WebInspectorUI\Main.html` подключает `Answer42Bridge.js`. Каталог платформы доступен на запись только администратору, поэтому команда сама запрашивает права через UAC — одно подтверждение на все выбранные платформы — и разрешает текущему пользователю менять файл с портом сессии; сессиям права администратора больше не нужны. Если процесс Answer42 запущен из среды, которая не может показать запрос UAC (например, из песочницы агента), через 90 секунд команда вернёт ошибку с готовой командой для PowerShell, запущенного от имени администратора. После этого интерфейс инспектора подключается к Answer42 сам: в консоль ничего не вводится, мост переживает перезагрузку страницы, а `html_console` видит сообщения с момента её загрузки. Если инспектор открывает человек, мост ничего не делает. `answer42 webkit-inspector status` показывает состояние платформ, `uninstall` возвращает исходные файлы. После обновления Answer42 или платформы мост нужно поставить заново: `html_mode=patched` с устаревшим мостом не запускается. Ключ `--version` выбирает платформу, без него — все установленные.
|
|
265
|
+
- **Linux, `remote` (по умолчанию).** HTML-поля рисует системный WebKitGTK. Answer42 запускает тест-клиент с `WEBKIT_INSPECTOR_SERVER=127.0.0.1:<порт>` и подключается к удалённому отладчику WebKitGTK напрямую: окна инспектора и ввода с клавиатуры нет.
|
|
266
|
+
- **Linux, `patched`.** Инспектор WebKitGTK в 1С открывается той же клавишей Ctrl+Alt+Shift+F12 и встраивается в форму. Интерфейс инспектора вшит в системную `libwebkit2gtk`, поэтому Answer42 его не меняет: переменная GLib `G_RESOURCE_OVERLAYS` подставляет тест-клиенту из каталога сессии один файл инспектора (`localizedStrings.js`) с мостом. Answer42 ставит фокус в HTML-поле средствами клиента тестирования и отправляет клавишу через `xdotool`, после чего интерфейс инспектора подключается сам (около 1 с). Нужен `xdotool`; на живом дисплее горячая клавиша уходит в окно 1С.
|
|
267
|
+
- **Linux, `console`** не поддерживается: встроенный инспектор WebKitGTK не отдельное окно X11, поэтому клавиши уходят в элемент с фокусом — в саму страницу. В проверке набранный загрузчик попал в поле ввода чата и был отправлен как сообщение.
|
|
268
|
+
|
|
269
|
+
**Внешний браузер на Linux.** Ссылки и файлы, которые 1С открывает «снаружи» (например, «Скачать» в HTML-поле или `ЗапуститьПриложение`), платформа открывает через `xdg-open` и ждёт его завершения, а `xdg-open` ждёт браузер, который ему пришлось запустить. Без Answer42 клиент 1С висел, пока не закроют Chrome, а с ним вставали мост тест-клиента и инструменты `html_*`. Поэтому тест-клиент получает в `PATH` обёртку `xdg-open` от Answer42: она записывает адрес и сразу возвращает управление, а на настоящем дисплее открывает адрес в браузере отдельно от клиента. Записанные адреса видны в `external_opens` ответов `html_field_click_link` и `session_status`; файл по такому адресу скачивает `html_download`. `ONEC_MCP_EXTERNAL_OPEN`: `auto` (по умолчанию: на собственном Xvfb Answer42 только записывать, на настоящем дисплее ещё и открывать), `open`, `record`, `off` (без обёртки).
|
|
255
270
|
|
|
256
271
|
Ограничения: на Windows инспектор остаётся встроенным в форму и попадает на скриншоты. `html_console` на Linux и с мостом в инспекторе платформы отдаёт сообщения с момента загрузки страницы, а с загрузчиком в консоли — с момента подключения (`collected_since`); неподдержанный синтаксис в скриптах страницы находит проверка скриптов (`check_scripts`). Встроенный WebKit Windows-клиента 1С не поддерживает часть современного JavaScript (например `?.`), а WebKitGTK на Linux — поддерживает: одна и та же страница может работать в одном клиенте и ломаться в другом.
|
|
257
272
|
|
|
@@ -187,7 +187,17 @@ answer42
|
|
|
187
187
|
|
|
188
188
|
**Статический HTML** — инструменты группы `forms` через API клиента тестирования: `html_field_content` (HTML поля в том виде, в каком страница загрузилась, и её видимый текст), `html_field_click_link`, `html_field_input`.
|
|
189
189
|
|
|
190
|
-
**Живые страницы** — группа `html`.
|
|
190
|
+
**Живые страницы** — группа `html`. Способ доступа к встроенному WebKit клиента выбирает параметр `start_session(html_mode=...)` (значение по умолчанию можно задать переменной `ONEC_MCP_HTML_MODE`):
|
|
191
|
+
|
|
192
|
+
| `html_mode` | Где | Как |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `auto` (по умолчанию) | везде | Linux — `remote`; Windows — `patched`, если в Web Inspector платформы установлен актуальный мост, иначе `console` |
|
|
195
|
+
| `console` | Windows | горячая клавиша инспектора и короткий загрузчик, введённый в его консоль |
|
|
196
|
+
| `patched` | Windows, Linux | мост, встроенный в интерфейс инспектора: на Windows — в Web Inspector платформы (сначала `answer42 webkit-inspector install`, см. ниже), на Linux — подставляется для сессии, ничего устанавливать не нужно |
|
|
197
|
+
| `remote` | Linux | удалённый отладчик WebKitGTK (`WEBKIT_INSPECTOR_SERVER`) |
|
|
198
|
+
| `disabled` | везде | мост не запускается, инструменты `html_*` недоступны |
|
|
199
|
+
|
|
200
|
+
Явно выбранный режим не подменяется другим: если он недоступен (`patched` без моста в платформе, режим другой ОС), `start_session` сразу возвращает ошибку с подсказкой. Мост ничего не делает, пока не вызван инструмент `html_*`: на Windows инспектор открывается только при первом вызове, на Linux клиент запускается со встроенным сервером отладки.
|
|
191
201
|
|
|
192
202
|
| Инструмент | Что делает |
|
|
193
203
|
|---|---|
|
|
@@ -199,6 +209,7 @@ answer42
|
|
|
199
209
|
| `html_wait` | дождаться появления или исчезновения текста / элемента |
|
|
200
210
|
| `html_eval` | выполнить произвольный JavaScript в странице — **только с `--dev-tools`** (см. ниже) |
|
|
201
211
|
| `html_console` | ошибки и предупреждения консоли страницы и скрипты, которые эта сборка WebKit не разбирает (например `?.`) |
|
|
212
|
+
| `html_download` | скачать файл по ссылке страницы изнутри страницы (с её cookie и сеансом) и сохранить в каталог сессии: путь, размер, тип, HTTP-статус |
|
|
202
213
|
| `html_inspector_patch` | Windows: встроить мост в Web Inspector установленных платформ 1С или убрать его (необязательно, см. ниже) |
|
|
203
214
|
|
|
204
215
|
Если живых страниц несколько, нужная выбирается параметром `page` (id из `html_pages` или часть URL / заголовка).
|
|
@@ -208,8 +219,12 @@ answer42
|
|
|
208
219
|
Как это устроено:
|
|
209
220
|
|
|
210
221
|
- **Windows.** Встроенный WebKit клиента 1С не имеет сервера удалённой отладки, но его Web Inspector открывается по Ctrl+Alt+Shift+F12. При первом вызове `html_*` Answer42 открывает инспектор поля (он встраивается в нижнюю часть формы), переключает его на вкладку Console и вводит в консоль короткий загрузчик. Загрузчик подключается к локальному WebSocket Answer42 по одноразовому токену, получает мост и выполняет операции в странице. Подключение к странице занимает около 2–3 секунд. После перезагрузки страницы загрузчик вводится заново. Платформа используется как установлена.
|
|
211
|
-
- **Windows, мост в инспекторе платформы (
|
|
212
|
-
- **Linux.** HTML-поля рисует системный WebKitGTK. Answer42 запускает тест-клиент с `WEBKIT_INSPECTOR_SERVER=127.0.0.1:<порт>` и подключается к удалённому отладчику WebKitGTK напрямую: окна инспектора и ввода с клавиатуры нет.
|
|
222
|
+
- **Windows, мост в инспекторе платформы (`html_mode=patched`).** `answer42 webkit-inspector install` (или инструмент `html_inspector_patch(action="install")`) встраивает мост в Web Inspector установленной платформы: `bin\WebKit.resources\WebInspectorUI\Main.html` подключает `Answer42Bridge.js`. Каталог платформы доступен на запись только администратору, поэтому команда сама запрашивает права через UAC — одно подтверждение на все выбранные платформы — и разрешает текущему пользователю менять файл с портом сессии; сессиям права администратора больше не нужны. Если процесс Answer42 запущен из среды, которая не может показать запрос UAC (например, из песочницы агента), через 90 секунд команда вернёт ошибку с готовой командой для PowerShell, запущенного от имени администратора. После этого интерфейс инспектора подключается к Answer42 сам: в консоль ничего не вводится, мост переживает перезагрузку страницы, а `html_console` видит сообщения с момента её загрузки. Если инспектор открывает человек, мост ничего не делает. `answer42 webkit-inspector status` показывает состояние платформ, `uninstall` возвращает исходные файлы. После обновления Answer42 или платформы мост нужно поставить заново: `html_mode=patched` с устаревшим мостом не запускается. Ключ `--version` выбирает платформу, без него — все установленные.
|
|
223
|
+
- **Linux, `remote` (по умолчанию).** HTML-поля рисует системный WebKitGTK. Answer42 запускает тест-клиент с `WEBKIT_INSPECTOR_SERVER=127.0.0.1:<порт>` и подключается к удалённому отладчику WebKitGTK напрямую: окна инспектора и ввода с клавиатуры нет.
|
|
224
|
+
- **Linux, `patched`.** Инспектор WebKitGTK в 1С открывается той же клавишей Ctrl+Alt+Shift+F12 и встраивается в форму. Интерфейс инспектора вшит в системную `libwebkit2gtk`, поэтому Answer42 его не меняет: переменная GLib `G_RESOURCE_OVERLAYS` подставляет тест-клиенту из каталога сессии один файл инспектора (`localizedStrings.js`) с мостом. Answer42 ставит фокус в HTML-поле средствами клиента тестирования и отправляет клавишу через `xdotool`, после чего интерфейс инспектора подключается сам (около 1 с). Нужен `xdotool`; на живом дисплее горячая клавиша уходит в окно 1С.
|
|
225
|
+
- **Linux, `console`** не поддерживается: встроенный инспектор WebKitGTK не отдельное окно X11, поэтому клавиши уходят в элемент с фокусом — в саму страницу. В проверке набранный загрузчик попал в поле ввода чата и был отправлен как сообщение.
|
|
226
|
+
|
|
227
|
+
**Внешний браузер на Linux.** Ссылки и файлы, которые 1С открывает «снаружи» (например, «Скачать» в HTML-поле или `ЗапуститьПриложение`), платформа открывает через `xdg-open` и ждёт его завершения, а `xdg-open` ждёт браузер, который ему пришлось запустить. Без Answer42 клиент 1С висел, пока не закроют Chrome, а с ним вставали мост тест-клиента и инструменты `html_*`. Поэтому тест-клиент получает в `PATH` обёртку `xdg-open` от Answer42: она записывает адрес и сразу возвращает управление, а на настоящем дисплее открывает адрес в браузере отдельно от клиента. Записанные адреса видны в `external_opens` ответов `html_field_click_link` и `session_status`; файл по такому адресу скачивает `html_download`. `ONEC_MCP_EXTERNAL_OPEN`: `auto` (по умолчанию: на собственном Xvfb Answer42 только записывать, на настоящем дисплее ещё и открывать), `open`, `record`, `off` (без обёртки).
|
|
213
228
|
|
|
214
229
|
Ограничения: на Windows инспектор остаётся встроенным в форму и попадает на скриншоты. `html_console` на Linux и с мостом в инспекторе платформы отдаёт сообщения с момента загрузки страницы, а с загрузчиком в консоли — с момента подключения (`collected_since`); неподдержанный синтаксис в скриптах страницы находит проверка скриптов (`check_scripts`). Встроенный WebKit Windows-клиента 1С не поддерживает часть современного JavaScript (например `?.`), а WebKitGTK на Linux — поддерживает: одна и та же страница может работать в одном клиенте и ломаться в другом.
|
|
215
230
|
|
|
@@ -91,7 +91,7 @@ class SessionState:
|
|
|
91
91
|
|
|
92
92
|
`dynamic_list_sort_by_column` — отдельный быстрый tool для сортировки уже открытого динамического списка по видимой колонке через API списка. Он не является fallback-веткой `dynamic_list_set_settings`.
|
|
93
93
|
- табличные документы: `tabular_documents` (список или ячейки одного документа по `name`/`title`), `tabular_document_save`;
|
|
94
|
-
- HTML-поля: `html_field_content`, `html_field_click_link`, `html_field_input` (API тест-клиента) и живые страницы `html_pages`, `html_text`, `html_query`, `html_click`, `html_type`, `html_wait`, `html_console` (через `mcp_1c.html_agent`); `html_eval` (произвольный JavaScript) и `dev_eval` (произвольный BSL) регистрируются только с `--dev-tools` / `ONEC_MCP_DEV_TOOLS=1` (`DEV_TOOL_NAMES`, `_register_dev_tools`);
|
|
94
|
+
- HTML-поля: `html_field_content`, `html_field_click_link`, `html_field_input` (API тест-клиента) и живые страницы `html_pages`, `html_text`, `html_query`, `html_click`, `html_type`, `html_wait`, `html_console` (через `mcp_1c.html_agent`); `html_download` (файл по ссылке страницы, запросом изнутри страницы); `html_eval` (произвольный JavaScript) и `dev_eval` (произвольный BSL) регистрируются только с `--dev-tools` / `ONEC_MCP_DEV_TOOLS=1` (`DEV_TOOL_NAMES`, `_register_dev_tools`);
|
|
95
95
|
- доказательная запись: `recording_start`, `recording_capture`, `recording_stop`;
|
|
96
96
|
- RAG: `rag_source_add`, `rag_sources_add_from_directory`, `rag_source_roots_list`, `rag_sources_list`, `rag_available_configurations`, `rag_snapshot_create`, `rag_snapshots_list`, `rag_index_build`, `rag_lookup_object`, `rag_query`, `rag_session_autoconfigure`.
|
|
97
97
|
|
|
@@ -119,6 +119,8 @@ Windows-аналог Xvfb: скрытые рабочие столы (`CreateDesk
|
|
|
119
119
|
|
|
120
120
|
### `mcp_1c.html_agent`
|
|
121
121
|
|
|
122
|
+
Режим задаёт `start_session(html_mode=...)` (`resolve_mode`): `auto` — `remote` на Linux, а на Windows `HtmlInspector.check_platform` выбирает `patched`, если в Web Inspector платформы клиента установлен актуальный мост, иначе `console`; `console` и `patched` — только Windows, `remote` — только Linux. На Linux `console` отклоняется: клавиши через XTEST уходят в элемент с фокусом, а встроенный инспектор WebKitGTK — не отдельное окно X11, поэтому загрузчик попал бы в саму страницу. `patched` на Linux подставляет через `G_RESOURCE_OVERLAYS` файл `Localizations/en.lproj/localizedStrings.js` интерфейса инспектора (пустой словарь строк — интерфейс показывает английские ключи — плюс конфиг и `LINUX_BRIDGE_JS`, который учитывает домен `Target`); фокус в поле ставит `activate_field` клиента тестирования (`_HtmlFieldDriver` в `mcp_1c.server`), клавишу отправляет `xdotool`. `disabled` не запускает мост. Явно выбранный режим строгий: `patched` без актуального моста в платформе отклоняется ещё до запуска клиента (`HtmlInspector.check_platform`), а при сбое подключения нет отката на другой режим.
|
|
123
|
+
|
|
122
124
|
Мост к живым страницам HTML-полей. API тест-клиента отдаёт только HTML, с которым страница загрузилась, поэтому для страниц, которые рисуют себя скриптами, Answer42 работает со встроенным WebKit клиента напрямую. Операции над страницей — одна самодостаточная ES5-функция `OPS_JS` (`info`, `text`, `html`, `query`, `click`, `type`, `eval`, `hook_console`, `console`, `check_scripts`), которая выполняется в странице и возвращает JSON. `HtmlInspector` — состояние моста сессии: страницы `p1`, `p2`, …, выбор страницы, ошибки подключения.
|
|
123
125
|
|
|
124
126
|
```mermaid
|
|
@@ -140,7 +142,7 @@ flowchart LR
|
|
|
140
142
|
|
|
141
143
|
Дальше `_type_into_console`: `WM_SETFOCUS` делает интерфейс инспектора активным для WebKit, не трогая фокус внутри его документа; Ctrl+Alt+C открывает вкладку Console, которая сама ставит фокус в строку ввода (инспектор запоминает вкладку). На вкладку Elements ввод попадать не должен: её дерево DOM забирает фокус, когда догружается, и набранные в нём клавиши, особенно Enter, правят DOM страницы. Esc тоже нельзя — его первой обрабатывает 1С и закрывает форму. Загрузчик `loader_js` (около 110 символов) вводится за комментарием `//`: автодополнение консоли асинхронно отвечает на набранный код и может заменить набранное после него, но внутри комментария не срабатывает. Home и два Delete снимают `//`, Enter выполняет строку. Загрузчик открывает WebSocket с одноразовым токеном в адресе, сервер в ответ присылает `PAGE_BRIDGE_JS`, который выполняет присланные выражения через `eval`. Подключение к странице занимает около 2–3 секунд; время по этапам — в `last_connect` статуса моста. Мост живёт в странице: после перезагрузки следующий вызов вводит загрузчик заново.
|
|
142
144
|
|
|
143
|
-
**Windows, мост в инспекторе платформы** (`mcp_1c.webkit_patch`, необязательно). `install` дописывает в `WebKit.resources\WebInspectorUI\Main.html` платформы `Answer42BridgeConfig.js` и `Answer42Bridge.js` (исходный `Main.html` сохраняется рядом). Каталог в Program Files доступен на запись только администратору, поэтому модуль перезапускает себя с повышением прав (`ShellExecuteEx` с `runas`, одно подтверждение UAC на все платформы) и выдаёт текущему пользователю право менять `Answer42BridgeConfig.js` (`icacls`). Перед тем как открыть инспектор, сессия записывает в этот файл свой порт и одноразовый токен (под файловой блокировкой: платформу могут делить несколько сессий) и очищает его сразу после. Мост в интерфейсе инспектора подключается к `HtmlInspector` по пути `/answer42-inspector/<токен>`, отправляет в бэкенд сообщения протокола с id от `BRIDGE_BASE_ID` и возвращает ответы на них, а также копии `Console.messageAdded`; остальное идёт в обычный интерфейс инспектора. Операции над страницей — `Runtime.evaluate` с `returnByValue`; мост переживает перезагрузку страницы, а сообщения консоли видны с её загрузки.
|
|
145
|
+
**Windows, мост в инспекторе платформы** (`mcp_1c.webkit_patch`, необязательно). `install` дописывает в `WebKit.resources\WebInspectorUI\Main.html` платформы `Answer42BridgeConfig.js` и `Answer42Bridge.js` (исходный `Main.html` сохраняется рядом). Каталог в Program Files доступен на запись только администратору, поэтому модуль перезапускает себя с повышением прав (`ShellExecuteEx` с `runas`, одно подтверждение UAC на все платформы) и выдаёт текущему пользователю право менять `Answer42BridgeConfig.js` (`icacls`). Перед тем как открыть инспектор, сессия записывает в этот файл свой порт и одноразовый токен (под файловой блокировкой: платформу могут делить несколько сессий) и очищает его сразу после. Мост в интерфейсе инспектора подключается к `HtmlInspector` по пути `/answer42-inspector/<токен>`, отправляет в бэкенд сообщения протокола с id от `BRIDGE_BASE_ID` и возвращает ответы на них, а также копии `Console.messageAdded`; остальное идёт в обычный интерфейс инспектора. Операции над страницей — `Runtime.evaluate` с `returnByValue`; мост переживает перезагрузку страницы, а сообщения консоли видны с её загрузки. Режим включается явно (`html_mode=patched`); если инспектор поля уже открыт без моста, его нужно закрыть.
|
|
144
146
|
|
|
145
147
|
**Linux.** HTML-поля рисует системный WebKitGTK (`libwebkit2gtk-4.0`). `HtmlInspector.client_env()` добавляет тест-клиенту `WEBKIT_INSPECTOR_SERVER=127.0.0.1:<свободный порт>` (`runtime.start_test_client(extra_env=...)`). `RemoteInspectorClient` подключается к этому серверу как фронтенд: сообщения — размер (uint32 BE), флаг порядка байт, имя и параметры GVariant (`SetupInspectorClient`, `SetTargetList (ta(tsssb))`, `Setup (tt)`, `SendMessageToBackend (tts)`, `SendMessageToFrontend (tts)`). Каждая страница — цель `WebPage`; протокол инспектора мультиплексирован доменом `Target`, поэтому `Runtime.evaluate` уходит в `Target.sendMessageToTarget` текущей page-цели (она меняется при навигации, `Target.didCommitProvisionalTarget`). Домен `Console` включается на каждой page-цели, и WebKit отдаёт сообщения с момента загрузки страницы. Пустые скрытые страницы `about:blank` пропускаются. Некорректное сообщение может уронить клиент 1С, поэтому отправляются только перечисленные формы сообщений.
|
|
146
148
|
|
|
@@ -266,6 +268,7 @@ MCP-инструмент `rag_sources_add_from_directory(path, watch=true)` ищ
|
|
|
266
268
|
| `ONEC_MCP_WS_HOST` | WebSocket host | `127.0.0.1` |
|
|
267
269
|
| `ONEC_MCP_WS_PORT` | WebSocket port | `8765` |
|
|
268
270
|
| `ONEC_MCP_REQUEST_TIMEOUT` | Таймаут запроса, сек | `60` |
|
|
271
|
+
| `ONEC_MCP_EXTERNAL_OPEN` | Linux: что делать с адресами и файлами, которые тест-клиент открывает через `xdg-open` (обёртка `runtime.external_open_env`): `auto` — на собственном Xvfb только записывать в `external_opens`, на настоящем дисплее ещё и открывать отдельно от клиента; `open`; `record`; `off` — без обёртки | `auto` |
|
|
269
272
|
| `ONEC_MCP_WINDOWS_DESKTOP` | Windows: `hidden` — запускать менеджер и клиент 1С на скрытых рабочих столах сессии; `current` — на текущем рабочем столе | `current` |
|
|
270
273
|
| `ONEC_MCP_DATA_DIR` | Рабочая директория | OS tempdir + `answer42` (`/tmp/answer42` on Linux, `%TEMP%\answer42` on Windows) |
|
|
271
274
|
| `MCP_1C_RAG_DB` | Путь к RAG SQLite | `build/rag/onec-rag.sqlite` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "answer42"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Answer42 — The Answer to Life, Universe, and 1C — UI Driver. MCP-powered 1C:Enterprise UI automation: click, fill, navigate, test, and introspect managed forms through the test-client API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
Binary file
|
|
Binary file
|
|
@@ -33,9 +33,12 @@ import datetime as _dt
|
|
|
33
33
|
import itertools
|
|
34
34
|
import json
|
|
35
35
|
import logging
|
|
36
|
+
import os
|
|
36
37
|
import secrets
|
|
38
|
+
import shutil
|
|
37
39
|
import socket
|
|
38
40
|
import struct
|
|
41
|
+
import subprocess
|
|
39
42
|
import time
|
|
40
43
|
from contextlib import suppress
|
|
41
44
|
from dataclasses import dataclass, field
|
|
@@ -194,6 +197,30 @@ OPS_JS = r"""function (op, args) {
|
|
|
194
197
|
return describe(el, a.index || 0);
|
|
195
198
|
},
|
|
196
199
|
eval: function (a) { return (0, eval)(a.code); },
|
|
200
|
+
fetch: function (a) {
|
|
201
|
+
var url = a.url;
|
|
202
|
+
if (!url) {
|
|
203
|
+
var el = one(a);
|
|
204
|
+
url = el.href || el.getAttribute('href') || el.getAttribute('data-href') || el.src || '';
|
|
205
|
+
if (!url) { throw new Error('the element has no link (href, data-href or src)'); }
|
|
206
|
+
}
|
|
207
|
+
var link = document.createElement('a');
|
|
208
|
+
link.href = url;
|
|
209
|
+
var x = new XMLHttpRequest();
|
|
210
|
+
x.open('GET', link.href, false);
|
|
211
|
+
x.overrideMimeType('text/plain; charset=x-user-defined');
|
|
212
|
+
x.send(null);
|
|
213
|
+
var body = x.responseText || '', size = body.length;
|
|
214
|
+
if (a.max_bytes && size > a.max_bytes) { throw new Error('the file is too large: ' + size + ' bytes, the limit is ' + a.max_bytes); }
|
|
215
|
+
var chunks = [], step = 0x6000;
|
|
216
|
+
for (var i = 0; i < size; i += step) {
|
|
217
|
+
var part = body.substr(i, step), bytes = new Array(part.length);
|
|
218
|
+
for (var j = 0; j < part.length; j++) { bytes[j] = part.charCodeAt(j) & 0xff; }
|
|
219
|
+
chunks.push(String.fromCharCode.apply(null, bytes));
|
|
220
|
+
}
|
|
221
|
+
return { url: x.responseURL || link.href, status: x.status, content_type: x.getResponseHeader('Content-Type') || '',
|
|
222
|
+
disposition: x.getResponseHeader('Content-Disposition') || '', size: size, base64: btoa(chunks.join('')) };
|
|
223
|
+
},
|
|
197
224
|
hook_console: function () {
|
|
198
225
|
if (window.__answer42Console) { return false; }
|
|
199
226
|
var log = window.__answer42Console = [];
|
|
@@ -774,14 +801,115 @@ class RemoteInspectorClient:
|
|
|
774
801
|
return value.get("value")
|
|
775
802
|
|
|
776
803
|
|
|
804
|
+
|
|
805
|
+
# --- Linux, local inspector (html_mode patched) ---
|
|
806
|
+
#
|
|
807
|
+
# 1C opens the WebKitGTK inspector of the focused HTML field on Ctrl+Alt+Shift+F12, docked into
|
|
808
|
+
# the form. GTK widgets are no X11 windows, so the field gets the focus through the test client
|
|
809
|
+
# (activate_field) and the keys go to the client's window with xdotool (XTEST). patched: GLib's
|
|
810
|
+
# G_RESOURCE_OVERLAYS replaces one resource of the inspector front end, its localized strings
|
|
811
|
+
# (the UI falls back to the English keys), with a file that also carries the session's port,
|
|
812
|
+
# token and LINUX_BRIDGE_JS; no system file changes and no administrator rights.
|
|
813
|
+
|
|
814
|
+
INSPECTOR_STRINGS_RESOURCE = "/org/webkit/inspector/Localizations/en.lproj"
|
|
815
|
+
INSPECTOR_STRINGS_FILE = "localizedStrings.js"
|
|
816
|
+
LINUX_INSPECTOR_KEYS = "ctrl+alt+shift+F12"
|
|
817
|
+
|
|
818
|
+
# Like webkit_patch.BRIDGE_JS, plus the Target domain: WebKitGTK's front end talks to the page
|
|
819
|
+
# through Target.sendMessageToTarget / Target.dispatchMessageFromTarget.
|
|
820
|
+
LINUX_BRIDGE_JS = r"""(function () {
|
|
821
|
+
var config = window.ANSWER42_BRIDGE;
|
|
822
|
+
window.ANSWER42_BRIDGE = null;
|
|
823
|
+
if (!config || !config.port || !config.token) { return; }
|
|
824
|
+
var BASE_ID = __BASE_ID__;
|
|
825
|
+
function start() {
|
|
826
|
+
if (!window.InspectorFrontendAPI || !window.InspectorFrontendHost || !InspectorFrontendAPI.dispatchMessageAsync) { setTimeout(start, 50); return; }
|
|
827
|
+
var ws = new WebSocket('ws://127.0.0.1:' + config.port + '__PATH__' + config.token), queue = [];
|
|
828
|
+
function post(obj) { var text = JSON.stringify(obj); if (ws.readyState === 1) { ws.send(text); } else { queue.push(text); } }
|
|
829
|
+
function parse(message) {
|
|
830
|
+
if (typeof message !== 'string') { return message; }
|
|
831
|
+
if (message.indexOf('"id":') < 0 && message.indexOf('"Target.') < 0 && message.indexOf('Console.messageAdded') < 0) { return null; }
|
|
832
|
+
try { return JSON.parse(message); } catch (e) { return null; }
|
|
833
|
+
}
|
|
834
|
+
function divert(obj) {
|
|
835
|
+
if (!obj) { return false; }
|
|
836
|
+
if (typeof obj.id === 'number' && obj.id >= BASE_ID) { post({ type: 'message', message: obj }); return true; }
|
|
837
|
+
if (obj.method === 'Target.dispatchMessageFromTarget') {
|
|
838
|
+
var inner = obj.params && obj.params.message, o = null;
|
|
839
|
+
if (typeof inner === 'string' && (inner.indexOf('"id":') >= 0 || inner.indexOf('Console.messageAdded') >= 0)) {
|
|
840
|
+
try { o = JSON.parse(inner); } catch (e) { o = null; }
|
|
841
|
+
}
|
|
842
|
+
if (o && typeof o.id === 'number' && o.id >= BASE_ID) { post({ type: 'message', message: o }); return true; }
|
|
843
|
+
if (o && o.method === 'Console.messageAdded') { post({ type: 'event', message: o }); }
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
if (obj.method && obj.method.indexOf('Target.') === 0) { post({ type: 'target', message: obj }); }
|
|
847
|
+
if (obj.method === 'Console.messageAdded') { post({ type: 'event', message: obj }); }
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
['dispatchMessageAsync', 'dispatchMessage'].forEach(function (name) {
|
|
851
|
+
var original = InspectorFrontendAPI[name];
|
|
852
|
+
if (!original) { return; }
|
|
853
|
+
InspectorFrontendAPI[name] = function (message) {
|
|
854
|
+
if (divert(parse(message))) { return undefined; }
|
|
855
|
+
return original.apply(this, arguments);
|
|
856
|
+
};
|
|
857
|
+
});
|
|
858
|
+
ws.onopen = function () { ws.send(JSON.stringify({ type: 'hello', version: 1 })); while (queue.length) { ws.send(queue.shift()); } };
|
|
859
|
+
ws.onmessage = function (event) {
|
|
860
|
+
var m;
|
|
861
|
+
try { m = JSON.parse(event.data); } catch (e) { return; }
|
|
862
|
+
if (m.type === 'send') { InspectorFrontendHost.sendMessageToBackend(JSON.stringify(m.message)); }
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
start();
|
|
866
|
+
})();
|
|
867
|
+
""".replace("__BASE_ID__", str(webkit_patch.BRIDGE_BASE_ID)).replace("__PATH__", webkit_patch.INSPECTOR_PATH)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def write_inspector_strings(overlay_dir: Path, port: int = 0, token: str = "") -> None:
|
|
871
|
+
"""The overlay localizedStrings.js: empty strings, the bridge config (or none) and LINUX_BRIDGE_JS."""
|
|
872
|
+
overlay_dir.mkdir(parents=True, exist_ok=True)
|
|
873
|
+
config = json.dumps({"port": int(port), "token": token}) if port and token else "null"
|
|
874
|
+
text = f"var localizedStrings = {{}};\nwindow.ANSWER42_BRIDGE = {config};\n{LINUX_BRIDGE_JS}"
|
|
875
|
+
tmp = overlay_dir / (INSPECTOR_STRINGS_FILE + ".tmp")
|
|
876
|
+
tmp.write_text(text, encoding="utf-8")
|
|
877
|
+
tmp.replace(overlay_dir / INSPECTOR_STRINGS_FILE)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _xdotool(display: str, *args: str) -> subprocess.CompletedProcess:
|
|
881
|
+
env = {**os.environ, "DISPLAY": display} if display else None
|
|
882
|
+
return subprocess.run(["xdotool", *args], env=env, capture_output=True, text=True, timeout=30)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _client_x_window(display: str, pid: int) -> str:
|
|
886
|
+
"""The client's main X11 window: the visible window of the pid that has a title."""
|
|
887
|
+
for window in _xdotool(display, "search", "--pid", str(pid), "--onlyvisible", "--name", "").stdout.split():
|
|
888
|
+
if _xdotool(display, "getwindowname", window).stdout.strip():
|
|
889
|
+
return window
|
|
890
|
+
raise HtmlAgentError(f"No visible window of the test client (pid {pid}) on display {display or '$DISPLAY'}")
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def _x_keys(display: str, pid: int, *keys: str) -> None:
|
|
894
|
+
window = _client_x_window(display, pid)
|
|
895
|
+
_xdotool(display, "windowfocus", "--sync", window)
|
|
896
|
+
for key in keys:
|
|
897
|
+
_xdotool(display, "key", "--clearmodifiers", key)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
|
|
777
901
|
@dataclass
|
|
778
902
|
class InspectorPage:
|
|
779
903
|
page_id: str
|
|
780
904
|
connection: Any
|
|
781
905
|
hwnd: int = 0
|
|
782
906
|
target: tuple[int, int] | None = None
|
|
783
|
-
#
|
|
907
|
+
# patched: the connection is the inspector front end, not the page.
|
|
784
908
|
front_end: bool = False
|
|
909
|
+
# WebKitGTK front end: the page is reached through this target of the Target domain.
|
|
910
|
+
page_target: str = ""
|
|
911
|
+
# Linux console / patched: the HTML field the page belongs to.
|
|
912
|
+
field_name: str = ""
|
|
785
913
|
console_log: list[dict[str, Any]] = field(default_factory=list)
|
|
786
914
|
url: str = ""
|
|
787
915
|
title: str = ""
|
|
@@ -798,12 +926,41 @@ class InspectorPage:
|
|
|
798
926
|
}
|
|
799
927
|
|
|
800
928
|
|
|
929
|
+
HTML_MODES = ("auto", "console", "patched", "remote", "disabled")
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def resolve_mode(mode: str = "auto") -> str:
|
|
933
|
+
"""The way to reach live pages: auto stays auto on Windows (see check_platform), remote on Linux.
|
|
934
|
+
|
|
935
|
+
console (Windows): open the embedded Web Inspector with its hot key and type
|
|
936
|
+
the loader into its console; patched (Windows): the bridge built into the
|
|
937
|
+
platform's Web Inspector (webkit_patch); remote (Linux): the WebKitGTK remote
|
|
938
|
+
inspector of a client started with WEBKIT_INSPECTOR_SERVER.
|
|
939
|
+
"""
|
|
940
|
+
mode = (mode or "auto").strip().lower()
|
|
941
|
+
if mode not in HTML_MODES:
|
|
942
|
+
raise HtmlAgentError(f"Unknown html_mode {mode!r}: use {', '.join(HTML_MODES)}")
|
|
943
|
+
if mode == "auto":
|
|
944
|
+
return "auto" if os_support.IS_WINDOWS else "remote"
|
|
945
|
+
if mode == "remote" and os_support.IS_WINDOWS:
|
|
946
|
+
raise HtmlAgentError("html_mode=remote is for the Linux client (WebKitGTK); on Windows use console or patched")
|
|
947
|
+
if mode == "console" and not os_support.IS_WINDOWS:
|
|
948
|
+
# The docked WebKitGTK inspector is no X11 window: keys sent with XTEST go to the focused widget,
|
|
949
|
+
# which stays the page (its input field), so the loader would be typed and submitted there.
|
|
950
|
+
raise HtmlAgentError(
|
|
951
|
+
"html_mode=console is not available on Linux: keys cannot be routed to the WebKitGTK inspector "
|
|
952
|
+
"console and would land in the page itself; use patched (nothing is typed) or remote"
|
|
953
|
+
)
|
|
954
|
+
return mode
|
|
955
|
+
|
|
956
|
+
|
|
801
957
|
class HtmlInspector:
|
|
802
958
|
"""A session's page bridge server plus the bookkeeping of the client's WebKit views."""
|
|
803
959
|
|
|
804
960
|
MAX_ATTEMPTS = 2
|
|
805
961
|
|
|
806
|
-
def __init__(self) -> None:
|
|
962
|
+
def __init__(self, mode: str = "auto") -> None:
|
|
963
|
+
self.mode = resolve_mode(mode)
|
|
807
964
|
self.port = 0
|
|
808
965
|
self.pages: dict[str, InspectorPage] = {}
|
|
809
966
|
self.desktop = ""
|
|
@@ -820,8 +977,13 @@ class HtmlInspector:
|
|
|
820
977
|
self._attempts: dict[int, int] = {}
|
|
821
978
|
self._open_lock = asyncio.Lock()
|
|
822
979
|
self.last_connect: dict[str, Any] = {} # Windows: timings of the latest page connection
|
|
823
|
-
#
|
|
824
|
-
self.remote_port =
|
|
980
|
+
# remote: the client is started with its WebKit remote inspector server on this port.
|
|
981
|
+
self.remote_port = free_local_port() if self.mode == "remote" else 0
|
|
982
|
+
# Linux console / patched: finds and focuses HTML fields through the test client (set by the server).
|
|
983
|
+
self.field_driver: Any = None
|
|
984
|
+
self.overlay_dir: Path | None = None # Linux patched: the session's G_RESOURCE_OVERLAYS directory
|
|
985
|
+
self._outer: dict[int, int] = {} # Target.sendMessageToTarget id -> the inner message id
|
|
986
|
+
self._field_keys: dict[str, int] = {}
|
|
825
987
|
self._remote: RemoteInspectorClient | None = None
|
|
826
988
|
|
|
827
989
|
async def start(self) -> int:
|
|
@@ -845,16 +1007,42 @@ class HtmlInspector:
|
|
|
845
1007
|
self._tokens.clear()
|
|
846
1008
|
|
|
847
1009
|
def client_env(self) -> dict[str, str]:
|
|
848
|
-
"""Environment for the test client:
|
|
849
|
-
|
|
1010
|
+
"""Environment for the test client: the WebKit remote inspector (remote) or the inspector overlay (Linux patched)."""
|
|
1011
|
+
if self.remote_port:
|
|
1012
|
+
return {"WEBKIT_INSPECTOR_SERVER": f"127.0.0.1:{self.remote_port}"}
|
|
1013
|
+
if self._linux_local() and self.mode == "patched" and self.overlay_dir is not None:
|
|
1014
|
+
write_inspector_strings(self.overlay_dir)
|
|
1015
|
+
return {"G_RESOURCE_OVERLAYS": f"{INSPECTOR_STRINGS_RESOURCE}={self.overlay_dir}"}
|
|
1016
|
+
return {}
|
|
1017
|
+
|
|
1018
|
+
def _linux_local(self) -> bool:
|
|
1019
|
+
return not os_support.IS_WINDOWS and self.mode in {"console", "patched"}
|
|
850
1020
|
|
|
851
1021
|
def attach(self, desktop: str, pid: int, client_path: str = "") -> None:
|
|
852
1022
|
self.desktop, self.client_pid = desktop, int(pid or 0)
|
|
853
1023
|
self.client_bin = str(Path(client_path).parent) if client_path else ""
|
|
854
1024
|
|
|
855
|
-
def
|
|
856
|
-
"""
|
|
857
|
-
|
|
1025
|
+
def check_platform(self, client_path: str) -> None:
|
|
1026
|
+
"""Settle auto for this client's platform; fail early when the chosen mode cannot work with it.
|
|
1027
|
+
|
|
1028
|
+
Windows auto prefers the bridge built into the platform's Web Inspector
|
|
1029
|
+
(webkit_patch) when it is installed and current, and types into the
|
|
1030
|
+
console otherwise.
|
|
1031
|
+
"""
|
|
1032
|
+
if self.mode == "auto":
|
|
1033
|
+
self.mode = "patched" if webkit_patch.is_ready(Path(client_path).parent) else "console"
|
|
1034
|
+
if self._linux_local():
|
|
1035
|
+
if not shutil.which("xdotool"): # Ctrl+Alt+Shift+F12 that opens the inspector
|
|
1036
|
+
raise HtmlAgentError(f"html_mode={self.mode} on Linux needs xdotool (apt install xdotool), or use html_mode=remote")
|
|
1037
|
+
return
|
|
1038
|
+
if self.mode == "patched":
|
|
1039
|
+
bin_dir = Path(client_path).parent
|
|
1040
|
+
if not webkit_patch.is_ready(bin_dir):
|
|
1041
|
+
raise HtmlAgentError(
|
|
1042
|
+
f"html_mode=patched: the Web Inspector of the platform in {bin_dir} has no current Answer42 bridge; "
|
|
1043
|
+
f"install it with html_inspector_patch(action='install', version='{bin_dir.parent.name}') "
|
|
1044
|
+
"(or `answer42 webkit-inspector install`), or use html_mode=console"
|
|
1045
|
+
)
|
|
858
1046
|
|
|
859
1047
|
def status(self) -> dict[str, Any]:
|
|
860
1048
|
return {
|
|
@@ -862,7 +1050,7 @@ class HtmlInspector:
|
|
|
862
1050
|
"running": self._server is not None,
|
|
863
1051
|
"pages_alive": sum(1 for p in self.pages.values() if not p.closed),
|
|
864
1052
|
**({"remote_inspector_port": self.remote_port} if self.remote_port else {}),
|
|
865
|
-
|
|
1053
|
+
"mode": self.mode,
|
|
866
1054
|
**({"last_connect": self.last_connect} if self.last_connect else {}),
|
|
867
1055
|
**({"last_error": self.last_error} if self.last_error else {}),
|
|
868
1056
|
}
|
|
@@ -917,9 +1105,21 @@ class HtmlInspector:
|
|
|
917
1105
|
waiting.set_result(message)
|
|
918
1106
|
elif message.get("type") == "message": # a patched front end relays a protocol reply
|
|
919
1107
|
reply = message.get("message") or {}
|
|
920
|
-
|
|
1108
|
+
inner = self._outer.pop(reply.get("id"), None)
|
|
1109
|
+
if inner is not None: # the Target.sendMessageToTarget reply: only an error matters
|
|
1110
|
+
waiting = self._pending.pop(inner, None) if "error" in reply else None
|
|
1111
|
+
else:
|
|
1112
|
+
waiting = self._pending.pop(reply.get("id"), None)
|
|
921
1113
|
if waiting is not None and not waiting.done():
|
|
922
1114
|
waiting.set_result(reply)
|
|
1115
|
+
elif message.get("type") == "target": # WebKitGTK front end: the page target and its changes
|
|
1116
|
+
event = message.get("message") or {}
|
|
1117
|
+
params = event.get("params") or {}
|
|
1118
|
+
info = params.get("targetInfo") or {}
|
|
1119
|
+
if event.get("method") == "Target.targetCreated" and info.get("type") == "page" and not info.get("isProvisional"):
|
|
1120
|
+
page.page_target = str(info.get("targetId") or "")
|
|
1121
|
+
elif event.get("method") == "Target.didCommitProvisionalTarget":
|
|
1122
|
+
page.page_target = str(params.get("newTargetId") or "")
|
|
923
1123
|
elif message.get("type") == "event": # ... or a console message
|
|
924
1124
|
params = (message.get("message") or {}).get("params") or {}
|
|
925
1125
|
page.console_log.append(console_entry(params.get("message") or {}))
|
|
@@ -942,8 +1142,14 @@ class HtmlInspector:
|
|
|
942
1142
|
message_id = next(self._ids)
|
|
943
1143
|
if page.front_end:
|
|
944
1144
|
message_id += webkit_patch.BRIDGE_BASE_ID # the front end relays only ids from BRIDGE_BASE_ID
|
|
945
|
-
|
|
946
|
-
"expression": expression, "returnByValue": True, "doNotPauseOnExceptionsAndMuteConsole": True}}
|
|
1145
|
+
evaluate = {"id": message_id, "method": "Runtime.evaluate", "params": {
|
|
1146
|
+
"expression": expression, "returnByValue": True, "doNotPauseOnExceptionsAndMuteConsole": True}}
|
|
1147
|
+
if page.page_target:
|
|
1148
|
+
outer_id = next(self._ids) + webkit_patch.BRIDGE_BASE_ID
|
|
1149
|
+
self._outer[outer_id] = message_id
|
|
1150
|
+
evaluate = {"id": outer_id, "method": "Target.sendMessageToTarget", "params": {
|
|
1151
|
+
"targetId": page.page_target, "message": json.dumps(evaluate, ensure_ascii=False)}}
|
|
1152
|
+
outgoing: dict[str, Any] = {"type": "send", "message": evaluate}
|
|
947
1153
|
else:
|
|
948
1154
|
outgoing = {"id": message_id, "expression": expression}
|
|
949
1155
|
future = asyncio.get_running_loop().create_future()
|
|
@@ -983,9 +1189,12 @@ class HtmlInspector:
|
|
|
983
1189
|
"""Bridge every WebKit view of the client that has no live page yet; return new page ids."""
|
|
984
1190
|
if not self.client_pid:
|
|
985
1191
|
raise HtmlAgentError("The test client is not running for the HTML page bridge")
|
|
986
|
-
if
|
|
1192
|
+
if self.mode == "remote":
|
|
987
1193
|
async with self._open_lock:
|
|
988
1194
|
return await self._open_remote()
|
|
1195
|
+
if self._linux_local():
|
|
1196
|
+
async with self._open_lock:
|
|
1197
|
+
return await self._open_linux_local()
|
|
989
1198
|
async with self._open_lock:
|
|
990
1199
|
live = {p.hwnd for p in self.pages.values() if not p.closed}
|
|
991
1200
|
opened: list[str] = []
|
|
@@ -1032,6 +1241,53 @@ class HtmlInspector:
|
|
|
1032
1241
|
opened.append(page.page_id)
|
|
1033
1242
|
return opened
|
|
1034
1243
|
|
|
1244
|
+
async def _open_linux_local(self) -> list[str]:
|
|
1245
|
+
"""Linux console / patched: open the inspector of each HTML field of the active form without a live page."""
|
|
1246
|
+
if self.field_driver is None:
|
|
1247
|
+
raise HtmlAgentError("The HTML page bridge cannot reach the test client's form fields")
|
|
1248
|
+
live = {p.field_name for p in self.pages.values() if not p.closed}
|
|
1249
|
+
opened: list[str] = []
|
|
1250
|
+
for name in await self.field_driver.html_fields():
|
|
1251
|
+
key = self._field_keys.setdefault(name, len(self._field_keys) + 1)
|
|
1252
|
+
if name in live or self._attempts.get(key, 0) >= self.MAX_ATTEMPTS:
|
|
1253
|
+
continue
|
|
1254
|
+
page = await self._connect_linux(name, key)
|
|
1255
|
+
if page is not None:
|
|
1256
|
+
opened.append(page.page_id)
|
|
1257
|
+
return opened
|
|
1258
|
+
|
|
1259
|
+
async def _connect_linux(self, name: str, key: int) -> InspectorPage | None:
|
|
1260
|
+
self._attempts[key] = self._attempts.get(key, 0) + 1
|
|
1261
|
+
started = time.monotonic()
|
|
1262
|
+
timings: dict[str, Any] = {"field": name, "mode": self.mode}
|
|
1263
|
+
self.last_connect = timings
|
|
1264
|
+
patched = True # console is refused on Linux (resolve_mode)
|
|
1265
|
+
token, future = self.expect(key, webkit_patch.INSPECTOR_PATH)
|
|
1266
|
+
try:
|
|
1267
|
+
if self.overlay_dir is not None:
|
|
1268
|
+
await asyncio.to_thread(write_inspector_strings, self.overlay_dir, self.port, token)
|
|
1269
|
+
await self.field_driver.focus(name)
|
|
1270
|
+
await asyncio.to_thread(_x_keys, self.desktop, self.client_pid, LINUX_INSPECTOR_KEYS)
|
|
1271
|
+
page = await asyncio.wait_for(future, PATCHED_CONNECT_SECONDS)
|
|
1272
|
+
for _ in range(60): # the page target arrives right after the front end connects
|
|
1273
|
+
if page.page_target or page.closed:
|
|
1274
|
+
break
|
|
1275
|
+
await asyncio.sleep(0.05)
|
|
1276
|
+
timings["connected_seconds"] = round(time.monotonic() - started, 2)
|
|
1277
|
+
except (HtmlAgentError, TimeoutError, OSError, subprocess.SubprocessError) as exc:
|
|
1278
|
+
self.last_error = str(exc) or f"The page of HTML field {name} did not connect within the timeout"
|
|
1279
|
+
LOGGER.warning("%s", self.last_error)
|
|
1280
|
+
return None
|
|
1281
|
+
finally:
|
|
1282
|
+
self._tokens.pop(token, None)
|
|
1283
|
+
if patched and self.overlay_dir is not None:
|
|
1284
|
+
await asyncio.to_thread(write_inspector_strings, self.overlay_dir)
|
|
1285
|
+
page.field_name = name
|
|
1286
|
+
self._attempts.pop(key, None)
|
|
1287
|
+
with suppress(HtmlAgentError):
|
|
1288
|
+
await self.refresh(page)
|
|
1289
|
+
return page
|
|
1290
|
+
|
|
1035
1291
|
async def _front_end(self, view: int) -> int:
|
|
1036
1292
|
"""The docked inspector front end of ``view``, opening the inspector when needed."""
|
|
1037
1293
|
views = set(await self._views())
|
|
@@ -1080,14 +1336,20 @@ class HtmlInspector:
|
|
|
1080
1336
|
started = time.monotonic()
|
|
1081
1337
|
timings: dict[str, Any] = {"view": view, "typed": 0}
|
|
1082
1338
|
self.last_connect = timings
|
|
1083
|
-
if
|
|
1339
|
+
if self.mode == "patched":
|
|
1340
|
+
if view in self._front_ends and self._front_ends[view] in set(await self._views()):
|
|
1341
|
+
self.last_error = (f"The inspector of WebKit view {view} is open without the Answer42 bridge "
|
|
1342
|
+
"(it was opened before or its bridge closed): close it and call again")
|
|
1343
|
+
return None
|
|
1084
1344
|
patched = await self._connect_patched(view, timings)
|
|
1085
|
-
if patched is
|
|
1086
|
-
self.
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1345
|
+
if patched is None:
|
|
1346
|
+
self.last_error = str(timings.get("patched_error") or "the patched inspector did not connect")
|
|
1347
|
+
LOGGER.warning("%s", self.last_error)
|
|
1348
|
+
return None
|
|
1349
|
+
self._attempts.pop(view, None)
|
|
1350
|
+
with suppress(HtmlAgentError):
|
|
1351
|
+
await self.refresh(patched)
|
|
1352
|
+
return patched
|
|
1091
1353
|
token, future = self.expect(view)
|
|
1092
1354
|
try:
|
|
1093
1355
|
front_end = await self._front_end(view)
|