answer42 0.2.22__tar.gz → 0.2.24__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.2.22 → answer42-0.2.24}/.gitignore +1 -1
- {answer42-0.2.22 → answer42-0.2.24}/PKG-INFO +2 -2
- {answer42-0.2.22 → answer42-0.2.24}/README.md +1 -1
- {answer42-0.2.22 → answer42-0.2.24}/docs/architecture.md +4 -4
- {answer42-0.2.22 → answer42-0.2.24}/pyproject.toml +1 -1
- {answer42-0.2.22 → answer42-0.2.24}/scripts/e2e_stable.py +1 -1
- {answer42-0.2.22 → answer42-0.2.24}/scripts/openclaw_mcp_autoreload.py +1 -1
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl +129 -80
- {answer42-0.2.22 → answer42-0.2.24}/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 +1 -1
- {answer42-0.2.22 → answer42-0.2.24}/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 +2 -2
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/credentials.py +2 -2
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/os_support.py +1 -1
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/runtime.py +1 -1
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/server.py +133 -39
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/window_control.py +19 -4
- {answer42-0.2.22 → answer42-0.2.24}/LICENSE +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/credentials.example.json +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/docs/agent-installation.md +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/docs/assets/answer42-logo.png +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/docs/assets/platform42-logo.svg +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/docs/installation.md +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/scripts/build_cf.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/scripts/build_pages.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/scripts/rag_cli.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/Configuration.xml +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/client_cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/client_cf/Configuration.xml +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/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.2.22 → answer42-0.2.24}/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.2.22 → answer42-0.2.24}/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.2.22 → answer42-0.2.24}/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.2.22 → answer42-0.2.24}/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.2.22 → answer42-0.2.24}/src/mcp_1c/__init__.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/__init__.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/skills/answer42/SKILL.md +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/bridge.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/platform.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/protocol.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/__init__.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/detect.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/model.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/parsers.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/service.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/store.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/recorder.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/release_helper.py +0 -0
- {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/skill_installer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: answer42
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.24
|
|
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>
|
|
@@ -137,7 +137,7 @@ export ONEC_MCP_CREDENTIALS_FILE=/secure/path/credentials.json
|
|
|
137
137
|
Если переменная окружения не задана, MCP-сервер читает файл по умолчанию:
|
|
138
138
|
|
|
139
139
|
```text
|
|
140
|
-
~/.
|
|
140
|
+
~/.answer42-credentials.json
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
Формат файла:
|
|
@@ -103,7 +103,7 @@ export ONEC_MCP_CREDENTIALS_FILE=/secure/path/credentials.json
|
|
|
103
103
|
Если переменная окружения не задана, MCP-сервер читает файл по умолчанию:
|
|
104
104
|
|
|
105
105
|
```text
|
|
106
|
-
~/.
|
|
106
|
+
~/.answer42-credentials.json
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
Формат файла:
|
|
@@ -26,7 +26,7 @@ class SessionState:
|
|
|
26
26
|
ws_port: int # порт WebSocket bridge
|
|
27
27
|
ibsrv_port: int # порт HTTP-gate автономного сервера (0 если file-direct fallback)
|
|
28
28
|
testclient_port: int # TCP-порт клиента тестирования
|
|
29
|
-
data_dir: Path # временная директория сессии (tempdir/
|
|
29
|
+
data_dir: Path # временная директория сессии (tempdir/answer42/sessions/{session_id})
|
|
30
30
|
base_url: str
|
|
31
31
|
username: str
|
|
32
32
|
password: str
|
|
@@ -82,7 +82,7 @@ class SessionState:
|
|
|
82
82
|
Группы tools:
|
|
83
83
|
|
|
84
84
|
- жизненный цикл: `start_session`, `stop_session`, `session_status`, `credentials_check`;
|
|
85
|
-
- автоматизация UI: `active_window`, `windows_list`, `activate_window`, `goto_start_page`, `goto_previous_window`, `goto_next_window`, `ui_tree`, `find_object`, `activate_object`, `open_navigation_link`, `click_button`, `set_field_value`, `save_form`, `close_form`, `screenshot
|
|
85
|
+
- автоматизация UI: `active_window`, `windows_list`, `activate_window`, `goto_start_page`, `goto_previous_window`, `goto_next_window`, `ui_tree`, `find_object`, `activate_object`, `open_navigation_link`, `click_button`, `set_field_value`, `save_form`, `close_form`, `screenshot`;
|
|
86
86
|
- таблицы: `table_rows`, `table_find_row`, `table_goto_row`, `table_current_row`, `table_set_field`, `table_choose_field_from_list`, `table_move_row`;
|
|
87
87
|
- динамические списки: `dynamic_list_available_fields`, `dynamic_list_apply_settings`, `dynamic_list_set_order`, `dynamic_list_output`, `dynamic_list_open_settings`, `dynamic_list_clear_settings`, `dynamic_list_open_form_settings`;
|
|
88
88
|
- табличные документы: `tabular_documents`, `tabular_document_text`, `tabular_document_save`;
|
|
@@ -95,7 +95,7 @@ class SessionState:
|
|
|
95
95
|
|
|
96
96
|
### `mcp_1c.os_support`
|
|
97
97
|
|
|
98
|
-
Кроссплатформенные OS-хелперы: `file_lock` (POSIX `fcntl` / Windows `msvcrt`), `kill_process_tree` (SIGTERM/SIGKILL / `taskkill`), `subprocess_creation_kwargs` (start_new_session / CREATE_NEW_PROCESS_GROUP), `executable_name` (`.exe` на Windows), `default_data_dir` (tempdir/
|
|
98
|
+
Кроссплатформенные OS-хелперы: `file_lock` (POSIX `fcntl` / Windows `msvcrt`), `kill_process_tree` (SIGTERM/SIGKILL / `taskkill`), `subprocess_creation_kwargs` (start_new_session / CREATE_NEW_PROCESS_GROUP), `executable_name` (`.exe` на Windows), `default_data_dir` (tempdir/answer42).
|
|
99
99
|
|
|
100
100
|
### `mcp_1c.platform`
|
|
101
101
|
|
|
@@ -207,7 +207,7 @@ machine-readable code and remediation message for the agent.
|
|
|
207
207
|
| `ONEC_MCP_WS_HOST` | WebSocket host | `127.0.0.1` |
|
|
208
208
|
| `ONEC_MCP_WS_PORT` | WebSocket port | `8765` |
|
|
209
209
|
| `ONEC_MCP_REQUEST_TIMEOUT` | Таймаут запроса, сек | `60` |
|
|
210
|
-
| `ONEC_MCP_DATA_DIR` | Рабочая директория | `/tmp/
|
|
210
|
+
| `ONEC_MCP_DATA_DIR` | Рабочая директория | `/tmp/answer42` |
|
|
211
211
|
| `MCP_1C_RAG_DB` | Путь к RAG SQLite | `build/rag/onec-rag.sqlite` |
|
|
212
212
|
| `ONEC_MCP_DISABLE_RAG` | Отключить встроенный RAG (`1/true/yes/on`) | пусто |
|
|
213
213
|
| `ONEC_MCP_RAG_SOURCE_DIRS` | `:`/`;`-separated directories for startup RAG discovery/watch | пусто |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "answer42"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.24"
|
|
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"
|
|
@@ -66,7 +66,7 @@ def _prebuild_shared_client_base(shared_id: str) -> None:
|
|
|
66
66
|
|
|
67
67
|
platform_dir = _platform.find_platform_dir(os.environ.get("ONEC_VERSION") or None)
|
|
68
68
|
ibcmd = str(platform_dir / "ibcmd")
|
|
69
|
-
db_path = Path(os.getenv("ONEC_MCP_SHARED_CLIENT_BASE_DIR", "/tmp/
|
|
69
|
+
db_path = Path(os.getenv("ONEC_MCP_SHARED_CLIENT_BASE_DIR", "/tmp/answer42/shared-client-bases")) / shared_id / "ib"
|
|
70
70
|
if db_path.exists() and any(db_path.iterdir()):
|
|
71
71
|
return
|
|
72
72
|
cf_path = runtime.DEFAULT_CLIENT_CF_BUILT
|
|
@@ -16,7 +16,7 @@ import sys
|
|
|
16
16
|
import time
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
-
DEFAULT_PATTERNS = ("src/mcp_1c/**/*.py", "pyproject.toml")
|
|
19
|
+
DEFAULT_PATTERNS = ("src/mcp_1c/**/*.py", "src/cf/**/*.bsl", "pyproject.toml")
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def snapshot(root: Path, patterns: tuple[str, ...]) -> str:
|
|
@@ -264,8 +264,6 @@
|
|
|
264
264
|
Возврат ПерейтиАктивнымОкномMCP("previous_window", Параметры);
|
|
265
265
|
ИначеЕсли Метод = "goto_next_window" Тогда
|
|
266
266
|
Возврат ПерейтиАктивнымОкномMCP("next_window", Параметры);
|
|
267
|
-
ИначеЕсли Метод = "maximize_test_client_window" Тогда
|
|
268
|
-
Возврат РазвернутьОкноКлиентаТестирования();
|
|
269
267
|
ИначеЕсли Метод = "ui_tree" Тогда
|
|
270
268
|
Возврат ДеревоИнтерфейса(Параметры);
|
|
271
269
|
ИначеЕсли Метод = "command_bar" Тогда
|
|
@@ -298,6 +296,8 @@
|
|
|
298
296
|
Возврат ТекстТабличногоДокумента(Параметры);
|
|
299
297
|
ИначеЕсли Метод = "tabular_document_save" Тогда
|
|
300
298
|
Возврат СохранитьТабличныйДокументMCP(Параметры);
|
|
299
|
+
ИначеЕсли Метод = "tabular_document_state_display" Тогда
|
|
300
|
+
Возврат ОтображениеСостоянияТабличногоДокументаMCP(Параметры);
|
|
301
301
|
ИначеЕсли Метод = "set_field_value" Тогда
|
|
302
302
|
Возврат УстановитьЗначениеПоля(Параметры);
|
|
303
303
|
ИначеЕсли Метод = "activate_field" Тогда
|
|
@@ -2938,19 +2938,22 @@
|
|
|
2938
2938
|
Попытка
|
|
2939
2939
|
Подчиненные = БезопасныеПодчиненные(Объект);
|
|
2940
2940
|
Для Каждого Подчиненный Из Подчиненные Цикл
|
|
2941
|
-
|
|
2942
|
-
Если ТипТек = "тестируемоеполетабличногодокументаформы" Тогда
|
|
2941
|
+
Если ЭтоИскомоеПолеТабличногоДокумента(Подчиненный, "", "") Тогда
|
|
2943
2942
|
Элемент = Новый Структура;
|
|
2944
2943
|
Элемент.Вставить("name", БезопасноеСвойство(Подчиненный, "Имя", ""));
|
|
2945
2944
|
Элемент.Вставить("title", БезопасныйЗаголовок(Подчиненный));
|
|
2945
|
+
Элемент.Вставить("type", Строка(ТипЗнч(Подчиненный)));
|
|
2946
|
+
Элемент.Вставить("data", БезопасноеПредставлениеДанныхПоля(Подчиненный));
|
|
2946
2947
|
Если ВключатьТекст Тогда
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
Элемент.Вставить("
|
|
2953
|
-
|
|
2948
|
+
ТекстДокумента = ТекстТабличногоДокументаИзПоля(Подчиненный);
|
|
2949
|
+
Если ТекстДокумента.Свойство("error") Тогда
|
|
2950
|
+
Элемент.Вставить("text_error", ТекстДокумента.error);
|
|
2951
|
+
Иначе
|
|
2952
|
+
Элемент.Вставить("rows", ТекстДокумента.rows);
|
|
2953
|
+
Элемент.Вставить("columns", ТекстДокумента.columns);
|
|
2954
|
+
Элемент.Вставить("address", ТекстДокумента.address);
|
|
2955
|
+
Элемент.Вставить("text", ТекстДокумента.text);
|
|
2956
|
+
КонецЕсли;
|
|
2954
2957
|
КонецЕсли;
|
|
2955
2958
|
Результат.Добавить(Элемент);
|
|
2956
2959
|
КонецЕсли;
|
|
@@ -2987,35 +2990,101 @@
|
|
|
2987
2990
|
Если Поле = Неопределено Тогда
|
|
2988
2991
|
Возврат Новый Структура("found", Ложь);
|
|
2989
2992
|
КонецЕсли;
|
|
2993
|
+
ДанныеТекста = ТекстТабличногоДокументаИзПоля(Поле);
|
|
2994
|
+
Если ДанныеТекста.Свойство("error") Тогда
|
|
2995
|
+
Возврат Новый Структура("found,error", Истина, ДанныеТекста.error);
|
|
2996
|
+
КонецЕсли;
|
|
2997
|
+
Возврат Новый Структура("found,rows,columns,address,text,cells", Истина, ДанныеТекста.rows, ДанныеТекста.columns, ДанныеТекста.address, ДанныеТекста.text, ДанныеТекста.cells);
|
|
2998
|
+
КонецФункции
|
|
2999
|
+
|
|
3000
|
+
&НаКлиенте
|
|
3001
|
+
Функция ТекстТабличногоДокументаИзПоля(Поле)
|
|
2990
3002
|
Попытка
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
КоличествоКолонок = БезопасноеСвойство(Поле, "КоличествоКолонок", 0);
|
|
3003
|
+
КоличествоСтрок = Поле.ПолучитьРазмерОбластиДанныхДокументаПоВертикали();
|
|
3004
|
+
КоличествоКолонок = Поле.ПолучитьРазмерОбластиДанныхДокументаПоГоризонтали();
|
|
2994
3005
|
Если КоличествоСтрок = 0 Или КоличествоКолонок = 0 Тогда
|
|
2995
|
-
Возврат Новый Структура("
|
|
3006
|
+
Возврат Новый Структура("rows,columns,address,text,cells", 0, 0, "", "", Новый Массив);
|
|
2996
3007
|
КонецЕсли;
|
|
3008
|
+
АдресВсейОбласти = АдресОбластиТабличногоДокументаMCP(1, 1, КоличествоСтрок, КоличествоКолонок);
|
|
3009
|
+
ТекстВсейОбласти = Поле.ПолучитьТекстОбласти(АдресВсейОбласти);
|
|
2997
3010
|
МаксСтрок = Мин(КоличествоСтрок, 50);
|
|
2998
3011
|
МаксКолонок = Мин(КоличествоКолонок, 20);
|
|
2999
3012
|
Ячейки = Новый Массив;
|
|
3000
3013
|
Для НомерСтроки = 1 По МаксСтрок Цикл
|
|
3001
|
-
|
|
3014
|
+
СтрокаТекстов = Новый Массив;
|
|
3002
3015
|
Для НомерКолонки = 1 По МаксКолонок Цикл
|
|
3003
3016
|
Попытка
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
Строка.Добавить(?(ТипЗнч(Текст) = Тип("Строка"), Текст, Строка(Текст)));
|
|
3017
|
+
АдресЯчейки = АдресОбластиТабличногоДокументаMCP(НомерСтроки, НомерКолонки, НомерСтроки, НомерКолонки);
|
|
3018
|
+
СтрокаТекстов.Добавить(Поле.ПолучитьТекстОбласти(АдресЯчейки));
|
|
3007
3019
|
Исключение
|
|
3008
|
-
|
|
3020
|
+
СтрокаТекстов.Добавить("");
|
|
3009
3021
|
КонецПопытки;
|
|
3010
3022
|
КонецЦикла;
|
|
3011
|
-
Ячейки.Добавить(
|
|
3023
|
+
Ячейки.Добавить(СтрокаТекстов);
|
|
3012
3024
|
КонецЦикла;
|
|
3013
|
-
|
|
3025
|
+
Возврат Новый Структура("rows,columns,address,text,cells", КоличествоСтрок, КоличествоКолонок, АдресВсейОбласти, ТекстВсейОбласти, Ячейки);
|
|
3026
|
+
Исключение
|
|
3027
|
+
Возврат Новый Структура("error", ОписаниеОшибки());
|
|
3028
|
+
КонецПопытки;
|
|
3029
|
+
КонецФункции
|
|
3030
|
+
|
|
3031
|
+
&НаКлиенте
|
|
3032
|
+
Функция АдресОбластиТабличногоДокументаMCP(НачальнаяСтрока, НачальнаяКолонка, КонечнаяСтрока, КонечнаяКолонка)
|
|
3033
|
+
Начало = "R" + XMLСтрока(НачальнаяСтрока) + "C" + XMLСтрока(НачальнаяКолонка);
|
|
3034
|
+
Конец = "R" + XMLСтрока(КонечнаяСтрока) + "C" + XMLСтрока(КонечнаяКолонка);
|
|
3035
|
+
Возврат Начало + ":" + Конец;
|
|
3036
|
+
КонецФункции
|
|
3037
|
+
|
|
3038
|
+
&НаКлиенте
|
|
3039
|
+
Функция ОтображениеСостоянияТабличногоДокументаMCP(Параметры)
|
|
3040
|
+
Если ТестируемоеПриложение = Неопределено Тогда
|
|
3041
|
+
ВызватьИсключение "Тестируемое приложение не подключено.";
|
|
3042
|
+
КонецЕсли;
|
|
3043
|
+
АктивноеОкно = ТестируемоеПриложение.ПолучитьАктивноеОкно();
|
|
3044
|
+
Имя = СвойствоСтруктуры(Параметры, "name", "");
|
|
3045
|
+
Заголовок = СвойствоСтруктуры(Параметры, "title", "");
|
|
3046
|
+
Глубина = СвойствоСтруктуры(Параметры, "max_depth", 8);
|
|
3047
|
+
Поле = НайтиТабличныйДокументПоПараметрам(АктивноеОкно, Имя, Заголовок, 0, Глубина);
|
|
3048
|
+
Если Поле = Неопределено Тогда
|
|
3049
|
+
Возврат Новый Структура("found", Ложь);
|
|
3050
|
+
КонецЕсли;
|
|
3051
|
+
Попытка
|
|
3052
|
+
Отображение = Поле.ПолучитьОтображениеСостояния();
|
|
3053
|
+
Если Отображение = Неопределено Тогда
|
|
3054
|
+
Возврат Новый Структура("found,state_display", Истина, Неопределено);
|
|
3055
|
+
КонецЕсли;
|
|
3056
|
+
Состояние = Новый Структура;
|
|
3057
|
+
Состояние.Вставить("display_variant", Строка(БезопасноеСвойство(Отображение, "ВариантОтображения", "")));
|
|
3058
|
+
Состояние.Вставить("visible", БезопасноеСвойство(Отображение, "Видимость", Ложь));
|
|
3059
|
+
Состояние.Вставить("additional_show_mode", Строка(БезопасноеСвойство(Отображение, "ДополнительныйРежимОтображения", "")));
|
|
3060
|
+
Состояние.Вставить("command_name", Строка(БезопасноеСвойство(Отображение, "ИмяКоманды", "")));
|
|
3061
|
+
Состояние.Вставить("picture", Строка(БезопасноеСвойство(Отображение, "Картинка", "")));
|
|
3062
|
+
Состояние.Вставить("text", Строка(БезопасноеСвойство(Отображение, "Текст", "")));
|
|
3063
|
+
Возврат Новый Структура("found,state_display", Истина, Состояние);
|
|
3014
3064
|
Исключение
|
|
3015
3065
|
Возврат Новый Структура("found,error", Истина, ОписаниеОшибки());
|
|
3016
3066
|
КонецПопытки;
|
|
3017
3067
|
КонецФункции
|
|
3018
3068
|
|
|
3069
|
+
&НаКлиенте
|
|
3070
|
+
Функция ФайлСуществуетMCP(Путь)
|
|
3071
|
+
Попытка
|
|
3072
|
+
Файл = Новый Файл(Путь);
|
|
3073
|
+
Возврат Файл.Существует();
|
|
3074
|
+
Исключение
|
|
3075
|
+
Возврат Ложь;
|
|
3076
|
+
КонецПопытки;
|
|
3077
|
+
КонецФункции
|
|
3078
|
+
|
|
3079
|
+
&НаКлиенте
|
|
3080
|
+
Функция БезопасноеПредставлениеДанныхПоля(Поле)
|
|
3081
|
+
Попытка
|
|
3082
|
+
Возврат Строка(Поле.ПолучитьПредставлениеДанных());
|
|
3083
|
+
Исключение
|
|
3084
|
+
Возврат "";
|
|
3085
|
+
КонецПопытки;
|
|
3086
|
+
КонецФункции
|
|
3087
|
+
|
|
3019
3088
|
&НаКлиенте
|
|
3020
3089
|
Функция СохранитьТабличныйДокументMCP(Параметры)
|
|
3021
3090
|
Если ТестируемоеПриложение = Неопределено Тогда
|
|
@@ -3036,62 +3105,34 @@
|
|
|
3036
3105
|
КонецЕсли;
|
|
3037
3106
|
|
|
3038
3107
|
// Для tested form field extension for spreadsheet document direct Записать()
|
|
3039
|
-
// недоступен/ненадежен;
|
|
3040
|
-
//
|
|
3041
|
-
//
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
КонецЦикла;
|
|
3108
|
+
// недоступен/ненадежен; сначала для любого формата перебираем фильтры
|
|
3109
|
+
// штатного файлового диалога, а Python-сторона проверяет сигнатуру/расширение
|
|
3110
|
+
// реально созданного файла нужного формата.
|
|
3111
|
+
ФайлыФильтров = Новый Массив;
|
|
3112
|
+
ОшибкиФильтров = Новый Массив;
|
|
3113
|
+
ВариантыСохранитьКак = Новый Массив;
|
|
3114
|
+
ВариантыСохранитьКак.Добавить(Истина);
|
|
3115
|
+
ВариантыСохранитьКак.Добавить(Ложь);
|
|
3116
|
+
Для Каждого СохранитьКак Из ВариантыСохранитьКак Цикл
|
|
3117
|
+
Для ИндексФильтра = 1 По 30 Цикл
|
|
3118
|
+
ПутьПопытки = ДобавитьСуффиксКПутиMCP(Путь, "_saveas_" + ?(СохранитьКак, "true", "false") + "_filter" + Строка(ИндексФильтра));
|
|
3119
|
+
Попытка
|
|
3120
|
+
ТестируемоеПриложение.УстановитьРезультатДиалогаВыбораФайла(Истина, ПутьПопытки, ИндексФильтра);
|
|
3121
|
+
Поле.ЗаписатьСодержимоеВФайл(СохранитьКак);
|
|
3122
|
+
// Не проверяем существование файла на стороне 1С синхронно: web/ws
|
|
3123
|
+
// клиент может материализовать файл чуть позже. Возвращаем все
|
|
3124
|
+
// успешные попытки, а Python-сторона проверяет факт, сигнатуру и формат.
|
|
3125
|
+
ФайлыФильтров.Добавить(Новый Структура("path,filter_index,save_as", ПутьПопытки, ИндексФильтра, СохранитьКак));
|
|
3126
|
+
Исключение
|
|
3127
|
+
ОшибкиФильтров.Добавить(Новый Структура("filter_index,save_as,error", ИндексФильтра, СохранитьКак, ОписаниеОшибки()));
|
|
3128
|
+
КонецПопытки;
|
|
3061
3129
|
КонецЦикла;
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
Возврат Новый Структура("saved,path,format,method,files,errors", Ложь, Путь, ФорматФайла, "write_content_to_file_dialog_filters", Файлы, Ошибки);
|
|
3130
|
+
КонецЦикла;
|
|
3131
|
+
Если ФайлыФильтров.Количество() > 0 Тогда
|
|
3132
|
+
Возврат Новый Структура("saved,path,format,method,files,errors", Истина, Путь, ФорматФайла, "write_content_to_file_dialog_filters", ФайлыФильтров, ОшибкиФильтров);
|
|
3066
3133
|
КонецЕсли;
|
|
3067
3134
|
|
|
3068
|
-
|
|
3069
|
-
Попытка
|
|
3070
|
-
ТестируемоеПриложение.УстановитьРезультатДиалогаВыбораФайла(Истина, Путь, ИндексФильтраДиалогаТабличногоДокументаMCP(ФорматФайла));
|
|
3071
|
-
Поле.ЗаписатьСодержимоеВФайл(Истина);
|
|
3072
|
-
Возврат Новый Структура("saved,path,format,method,save_as", Истина, Путь, ФорматФайла, "write_content_to_file_dialog", Истина);
|
|
3073
|
-
Исключение
|
|
3074
|
-
ОшибкаЗаписатьСодержимое = ОписаниеОшибки();
|
|
3075
|
-
КонецПопытки;
|
|
3076
|
-
Попытка
|
|
3077
|
-
ТестируемоеПриложение.УстановитьРезультатДиалогаВыбораФайла(Истина, Путь, ИндексФильтраДиалогаТабличногоДокументаMCP(ФорматФайла));
|
|
3078
|
-
Поле.ЗаписатьСодержимоеВФайл(Ложь);
|
|
3079
|
-
Возврат Новый Структура("saved,path,format,method,save_as", Истина, Путь, ФорматФайла, "write_content_to_file_dialog", Ложь);
|
|
3080
|
-
Исключение
|
|
3081
|
-
Если Не ПустаяСтрока(ОшибкаЗаписатьСодержимое) Тогда
|
|
3082
|
-
ОшибкаЗаписатьСодержимое = ОшибкаЗаписатьСодержимое + Символы.ПС + ОписаниеОшибки();
|
|
3083
|
-
Иначе
|
|
3084
|
-
ОшибкаЗаписатьСодержимое = ОписаниеОшибки();
|
|
3085
|
-
КонецЕсли;
|
|
3086
|
-
КонецПопытки;
|
|
3087
|
-
|
|
3088
|
-
Попытка
|
|
3089
|
-
ТипФайла = ТипФайлаТабличногоДокументаMCP(ФорматФайла);
|
|
3090
|
-
Поле.Записать(Путь, ТипФайла);
|
|
3091
|
-
Возврат Новый Структура("saved,path,format,method", Истина, Путь, ФорматФайла, "write_direct");
|
|
3092
|
-
Исключение
|
|
3093
|
-
Возврат Новый Структура("saved,path,format,error,content_error", Ложь, Путь, ФорматФайла, ОписаниеОшибки(), ОшибкаЗаписатьСодержимое);
|
|
3094
|
-
КонецПопытки;
|
|
3135
|
+
Возврат Новый Структура("saved,path,format,method,files,errors,error", Ложь, Путь, ФорматФайла, "write_content_to_file_dialog_filters", ФайлыФильтров, ОшибкиФильтров, "Ни один фильтр файлового диалога не выполнил сохранение табличного документа");
|
|
3095
3136
|
КонецФункции
|
|
3096
3137
|
|
|
3097
3138
|
&НаКлиенте
|
|
@@ -3118,8 +3159,6 @@
|
|
|
3118
3159
|
Возврат ТипФайлаТабличногоДокумента.XLSX;
|
|
3119
3160
|
ИначеЕсли Формат = "xls" Тогда
|
|
3120
3161
|
Возврат ТипФайлаТабличногоДокумента.XLS;
|
|
3121
|
-
ИначеЕсли Формат = "mxl" Тогда
|
|
3122
|
-
Возврат ТипФайлаТабличногоДокумента.MXL;
|
|
3123
3162
|
ИначеЕсли Формат = "html" Или Формат = "htm" Тогда
|
|
3124
3163
|
Возврат ТипФайлаТабличногоДокумента.HTML;
|
|
3125
3164
|
ИначеЕсли Формат = "txt" Тогда
|
|
@@ -3138,8 +3177,6 @@
|
|
|
3138
3177
|
Возврат 2;
|
|
3139
3178
|
ИначеЕсли Формат = "xls" Тогда
|
|
3140
3179
|
Возврат 3;
|
|
3141
|
-
ИначеЕсли Формат = "mxl" Тогда
|
|
3142
|
-
Возврат 4;
|
|
3143
3180
|
ИначеЕсли Формат = "html" Или Формат = "htm" Тогда
|
|
3144
3181
|
Возврат 5;
|
|
3145
3182
|
ИначеЕсли Формат = "txt" Тогда
|
|
@@ -3179,7 +3216,10 @@
|
|
|
3179
3216
|
ТипТек = НормализованноеИмяТипа(Строка(ТипЗнч(Объект)));
|
|
3180
3217
|
ЭтоПолеТабличногоДокумента = ТипТек = "тестируемоеполетабличногодокументаформы";
|
|
3181
3218
|
Если Не ЭтоПолеТабличногоДокумента И ТипТек = "тестируемоеполеформы" Тогда
|
|
3182
|
-
|
|
3219
|
+
// На некоторых отчетах ПолучитьПредставлениеДанных() у поля табличного
|
|
3220
|
+
// документа падает, пока поле недоступно для интерактивных действий.
|
|
3221
|
+
// Поэтому сначала проверяем Вид поля, затем представление данных.
|
|
3222
|
+
ЭтоПолеТабличногоДокумента = ВидПоляТабличныйДокумент(Объект) Или ПредставлениеДанныхПоляТабличныйДокумент(Объект);
|
|
3183
3223
|
КонецЕсли;
|
|
3184
3224
|
Если Не ЭтоПолеТабличногоДокумента Тогда
|
|
3185
3225
|
Возврат Ложь;
|
|
@@ -3189,6 +3229,15 @@
|
|
|
3189
3229
|
Возврат (ПустаяСтрока(Имя) Или ИмяТек = Имя) И (ПустаяСтрока(Заголовок) Или ЗаголовокТек = Заголовок);
|
|
3190
3230
|
КонецФункции
|
|
3191
3231
|
|
|
3232
|
+
&НаКлиенте
|
|
3233
|
+
Функция ВидПоляТабличныйДокумент(Поле)
|
|
3234
|
+
Попытка
|
|
3235
|
+
Возврат НРег(Строка(Поле.Вид)) = "полетабличногодокумента";
|
|
3236
|
+
Исключение
|
|
3237
|
+
Возврат Ложь;
|
|
3238
|
+
КонецПопытки;
|
|
3239
|
+
КонецФункции
|
|
3240
|
+
|
|
3192
3241
|
&НаКлиенте
|
|
3193
3242
|
Функция ПредставлениеДанныхПоляТабличныйДокумент(Поле)
|
|
3194
3243
|
Попытка
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<Owners/>
|
|
37
37
|
<SubordinationUse>ToItems</SubordinationUse>
|
|
38
38
|
<CodeLength>9</CodeLength>
|
|
39
|
-
<DescriptionLength>
|
|
39
|
+
<DescriptionLength>80</DescriptionLength>
|
|
40
40
|
<CodeType>String</CodeType>
|
|
41
41
|
<CodeAllowedLength>Variable</CodeAllowedLength>
|
|
42
42
|
<CodeSeries>WholeCatalog</CodeSeries>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<Owners/>
|
|
37
37
|
<SubordinationUse>ToItems</SubordinationUse>
|
|
38
38
|
<CodeLength>9</CodeLength>
|
|
39
|
-
<DescriptionLength>
|
|
39
|
+
<DescriptionLength>80</DescriptionLength>
|
|
40
40
|
<CodeType>String</CodeType>
|
|
41
41
|
<CodeAllowedLength>Variable</CodeAllowedLength>
|
|
42
42
|
<CodeSeries>WholeCatalog</CodeSeries>
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
<Type>
|
|
91
91
|
<v8:Type>xs:string</v8:Type>
|
|
92
92
|
<v8:StringQualifiers>
|
|
93
|
-
<v8:Length>
|
|
93
|
+
<v8:Length>80</v8:Length>
|
|
94
94
|
<v8:AllowedLength>Variable</v8:AllowedLength>
|
|
95
95
|
</v8:StringQualifiers>
|
|
96
96
|
</Type>
|
|
Binary file
|
|
@@ -5,7 +5,7 @@ Reads a JSON file mapping base URLs (exact or wildcard) to
|
|
|
5
5
|
a sample ``credentials.example.json`` is provided for reference.
|
|
6
6
|
|
|
7
7
|
Environment variable ``ONEC_MCP_CREDENTIALS_FILE`` overrides the
|
|
8
|
-
default path (``~/.
|
|
8
|
+
default path (``~/.answer42-credentials.json``).
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -19,7 +19,7 @@ LOGGER = logging.getLogger(__name__)
|
|
|
19
19
|
|
|
20
20
|
DEFAULT_CREDENTIALS_PATH = Path(
|
|
21
21
|
os.getenv("ONEC_MCP_CREDENTIALS_FILE",
|
|
22
|
-
str(Path.home() / ".
|
|
22
|
+
str(Path.home() / ".answer42-credentials.json"))
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
|
|
@@ -24,7 +24,7 @@ def default_data_dir() -> Path:
|
|
|
24
24
|
override = os.getenv("ONEC_MCP_DATA_DIR")
|
|
25
25
|
if override:
|
|
26
26
|
return Path(override)
|
|
27
|
-
return Path(tempfile.gettempdir()) / "
|
|
27
|
+
return Path(tempfile.gettempdir()) / "answer42"
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def runtime_path(env_name: str, *parts: str) -> Path:
|
|
@@ -32,7 +32,7 @@ DEFAULT_IBSRV_HTTP_PORT = 8314
|
|
|
32
32
|
DEFAULT_TEST_CLIENT_PORT = 1538
|
|
33
33
|
DEFAULT_IBSRV_DIRECT_PORT = 1541
|
|
34
34
|
DEFAULT_DISPLAY_START = 99
|
|
35
|
-
DEFAULT_DISPLAY_RES = "
|
|
35
|
+
DEFAULT_DISPLAY_RES = "1920x1200x24"
|
|
36
36
|
DEFAULT_PLATFORM_DIR = f"/opt/1cv8/x86_64/{DEFAULT_ONEC_VERSION}"
|
|
37
37
|
DEFAULT_THICK_CLIENT = f"{DEFAULT_PLATFORM_DIR}/1cv8"
|
|
38
38
|
DEFAULT_THIN_CLIENT = f"{DEFAULT_PLATFORM_DIR}/1cv8c"
|
|
@@ -2653,14 +2653,16 @@ async def _connect_test_client_impl(
|
|
|
2653
2653
|
|
|
2654
2654
|
def _auto_maximize_on_error(display: str) -> None:
|
|
2655
2655
|
try:
|
|
2656
|
-
_maximize_external_window(title_contains="", display=display, width=None, height=None)
|
|
2656
|
+
_maximize_external_window(title_contains="Клиент тестирования (1С:Предприятие)", display=display, width=None, height=None)
|
|
2657
2657
|
except Exception as exc:
|
|
2658
2658
|
logging.debug("auto maximize after failed connect_test_client failed: %s", exc)
|
|
2659
2659
|
|
|
2660
2660
|
|
|
2661
2661
|
def _auto_maximize_test_client_result(result: Any, display: str) -> None:
|
|
2662
2662
|
try:
|
|
2663
|
-
mr = _maximize_external_window(title_contains="", display=display, width=None, height=None)
|
|
2663
|
+
mr = _maximize_external_window(title_contains="Клиент тестирования (1С:Предприятие)", display=display, width=None, height=None)
|
|
2664
|
+
if not mr.get("ok"):
|
|
2665
|
+
mr = _maximize_external_window(title_contains="", display=display, width=None, height=None)
|
|
2664
2666
|
if isinstance(result, dict):
|
|
2665
2667
|
result["auto_maximize"] = mr
|
|
2666
2668
|
except Exception as exc:
|
|
@@ -3052,34 +3054,6 @@ async def decoration_click(name: Annotated[str, "Decoration technical name"] = "
|
|
|
3052
3054
|
return await _bridge_call_recorded(session_id, "decoration_click", {"name": name, "title": title})
|
|
3053
3055
|
|
|
3054
3056
|
|
|
3055
|
-
@mcp.tool()
|
|
3056
|
-
async def maximize_test_client_window(*,
|
|
3057
|
-
session_id: Annotated[str, "Session id"] = DEFAULT_SESSION,
|
|
3058
|
-
) -> Any:
|
|
3059
|
-
"""Activate and maximize the 1C test-client OS window via wmctrl/xdotool/Xlib."""
|
|
3060
|
-
sess = _session(session_id)
|
|
3061
|
-
return _maximize_external_window(title_contains="", display=sess.display, width=None, height=None)
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
@mcp.tool()
|
|
3065
|
-
def maximize_test_client_external_window(
|
|
3066
|
-
title_contains: Annotated[str, "Window title substring; empty means choose likely 1C test-client window"] = "",
|
|
3067
|
-
display: Annotated[str, "X11 display override (empty = use session's)"] = "",
|
|
3068
|
-
width: Annotated[int, "Target width; 0 means display width"] = 0,
|
|
3069
|
-
height: Annotated[int, "Target height; 0 means display height"] = 0,
|
|
3070
|
-
*,
|
|
3071
|
-
session_id: Annotated[str, "Session id"] = DEFAULT_SESSION,
|
|
3072
|
-
) -> dict[str, Any]:
|
|
3073
|
-
"""Maximize/resize the 1C test-client OS window: wmctrl -> xdotool -> Xlib fallback."""
|
|
3074
|
-
sess = _session(session_id)
|
|
3075
|
-
return _maximize_external_window(
|
|
3076
|
-
title_contains=title_contains,
|
|
3077
|
-
display=display or sess.display,
|
|
3078
|
-
width=width or None,
|
|
3079
|
-
height=height or None,
|
|
3080
|
-
)
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
3057
|
@mcp.tool()
|
|
3084
3058
|
async def ui_tree(max_depth: Annotated[int, "Maximum UI tree depth"] = 4,
|
|
3085
3059
|
include_rag_types: Annotated[bool, "Enrich UI elements with best-effort types from the local RAG index"] = True,
|
|
@@ -3288,7 +3262,7 @@ async def tabular_document_save(
|
|
|
3288
3262
|
name: Annotated[str, "Tabular document field technical name"] = "",
|
|
3289
3263
|
title: Annotated[str, "Tabular document field visible title"] = "",
|
|
3290
3264
|
path: Annotated[str, "Output file path; empty = auto-generated temp path"] = "",
|
|
3291
|
-
format: Annotated[str, "Output format: pdf | xlsx | xls |
|
|
3265
|
+
format: Annotated[str, "Output format: pdf | xlsx | xls | html | txt"] = "pdf",
|
|
3292
3266
|
max_depth: Annotated[int, "Maximum search depth"] = 8,
|
|
3293
3267
|
*,
|
|
3294
3268
|
session_id: Annotated[str, "Session id"] = DEFAULT_SESSION,
|
|
@@ -3298,15 +3272,32 @@ async def tabular_document_save(
|
|
|
3298
3272
|
return _select_saved_tabular_document_file(result, format)
|
|
3299
3273
|
|
|
3300
3274
|
|
|
3275
|
+
@mcp.tool()
|
|
3276
|
+
async def tabular_document_state_display(
|
|
3277
|
+
name: Annotated[str, "Tabular document field technical name"] = "",
|
|
3278
|
+
title: Annotated[str, "Tabular document field visible title"] = "",
|
|
3279
|
+
max_depth: Annotated[int, "Maximum search depth"] = 8,
|
|
3280
|
+
*,
|
|
3281
|
+
session_id: Annotated[str, "Session id"] = DEFAULT_SESSION,
|
|
3282
|
+
) -> Any:
|
|
3283
|
+
"""Return the state display of a tabular document field, when exposed by 1C."""
|
|
3284
|
+
return await _bridge_call_recorded(session_id, "tabular_document_state_display", {"name": name, "title": title, "max_depth": max_depth})
|
|
3285
|
+
|
|
3286
|
+
|
|
3301
3287
|
def _select_saved_tabular_document_file(result: Any, format: str) -> Any:
|
|
3302
3288
|
if not isinstance(result, dict):
|
|
3303
3289
|
return result
|
|
3304
3290
|
files = result.get("files")
|
|
3305
3291
|
if not isinstance(files, list) or not files:
|
|
3292
|
+
if result.get("found", True) is not False:
|
|
3293
|
+
result["saved"] = False
|
|
3294
|
+
result.setdefault("error", f"No files were produced for requested tabular document format: {format.lower()}")
|
|
3306
3295
|
return result
|
|
3307
3296
|
selected = _find_saved_tabular_document_file(files, format)
|
|
3308
3297
|
if selected is None:
|
|
3309
3298
|
result["selected"] = None
|
|
3299
|
+
result["saved"] = False
|
|
3300
|
+
result["error"] = f"No produced file matches requested tabular document format: {format.lower()}"
|
|
3310
3301
|
return result
|
|
3311
3302
|
selected_path = selected.get("path")
|
|
3312
3303
|
requested_path = result.get("path")
|
|
@@ -3333,22 +3324,54 @@ def _find_saved_tabular_document_file(files: list[Any], format: str) -> dict[str
|
|
|
3333
3324
|
if not p.is_file():
|
|
3334
3325
|
continue
|
|
3335
3326
|
try:
|
|
3336
|
-
|
|
3327
|
+
data = p.read_bytes()
|
|
3328
|
+
header = data[:8]
|
|
3337
3329
|
except OSError:
|
|
3338
3330
|
continue
|
|
3339
3331
|
if wanted == "pdf" and header.startswith(b"%PDF"):
|
|
3340
3332
|
return item
|
|
3341
|
-
if wanted
|
|
3333
|
+
if wanted == "xlsx" and _zip_contains(p, "xl/workbook.xml"):
|
|
3334
|
+
return item
|
|
3335
|
+
if wanted == "docx" and _zip_contains(p, "word/document.xml"):
|
|
3342
3336
|
return item
|
|
3343
3337
|
if wanted == "xls" and header.startswith(bytes.fromhex("d0cf11e0a1b11ae1")):
|
|
3344
3338
|
return item
|
|
3345
|
-
if wanted in {"html", "htm"} and (
|
|
3339
|
+
if wanted in {"html", "htm"} and (data.startswith(b"<") or data.startswith(b"\xef\xbb\xbf<")):
|
|
3346
3340
|
return item
|
|
3347
|
-
if wanted
|
|
3341
|
+
if wanted == "txt" and _looks_like_plain_text(data):
|
|
3348
3342
|
return item
|
|
3349
3343
|
return None
|
|
3350
3344
|
|
|
3351
3345
|
|
|
3346
|
+
def _zip_contains(path: Path, member: str) -> bool:
|
|
3347
|
+
import zipfile
|
|
3348
|
+
|
|
3349
|
+
try:
|
|
3350
|
+
with zipfile.ZipFile(path) as archive:
|
|
3351
|
+
return member in archive.namelist()
|
|
3352
|
+
except Exception:
|
|
3353
|
+
return False
|
|
3354
|
+
|
|
3355
|
+
|
|
3356
|
+
def _looks_like_plain_text(data: bytes) -> bool:
|
|
3357
|
+
if not data:
|
|
3358
|
+
return False
|
|
3359
|
+
sample = data[:4096]
|
|
3360
|
+
if sample.startswith(b"\xef\xbb\xbf"):
|
|
3361
|
+
sample = sample[3:]
|
|
3362
|
+
stripped = sample.lstrip().lower()
|
|
3363
|
+
if stripped.startswith((b"<", b"<!doctype", b"<html")):
|
|
3364
|
+
return False
|
|
3365
|
+
if b"\x00" in sample:
|
|
3366
|
+
return False
|
|
3367
|
+
try:
|
|
3368
|
+
sample.decode("utf-8")
|
|
3369
|
+
return True
|
|
3370
|
+
except UnicodeDecodeError:
|
|
3371
|
+
return False
|
|
3372
|
+
|
|
3373
|
+
|
|
3374
|
+
|
|
3352
3375
|
@mcp.tool()
|
|
3353
3376
|
async def toggle_field(
|
|
3354
3377
|
field_name: Annotated[str, "Field technical name"],
|
|
@@ -4219,6 +4242,34 @@ async def close_form(*,
|
|
|
4219
4242
|
return await _bridge_call_recorded(session_id, "close_form", {})
|
|
4220
4243
|
|
|
4221
4244
|
|
|
4245
|
+
def _x11_screen_size(display: str | None = None) -> tuple[int, int] | None:
|
|
4246
|
+
env = os.environ.copy()
|
|
4247
|
+
if display:
|
|
4248
|
+
env["DISPLAY"] = display
|
|
4249
|
+
try:
|
|
4250
|
+
out = subprocess.run(
|
|
4251
|
+
["xdpyinfo"],
|
|
4252
|
+
env=env,
|
|
4253
|
+
check=True,
|
|
4254
|
+
text=True,
|
|
4255
|
+
stdout=subprocess.PIPE,
|
|
4256
|
+
stderr=subprocess.PIPE,
|
|
4257
|
+
timeout=10,
|
|
4258
|
+
).stdout
|
|
4259
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
4260
|
+
return None
|
|
4261
|
+
for line in out.splitlines():
|
|
4262
|
+
if "dimensions:" not in line:
|
|
4263
|
+
continue
|
|
4264
|
+
try:
|
|
4265
|
+
dimensions = line.split("dimensions:", 1)[1].strip().split()[0]
|
|
4266
|
+
width, height = dimensions.split("x", 1)
|
|
4267
|
+
return int(width), int(height)
|
|
4268
|
+
except Exception:
|
|
4269
|
+
return None
|
|
4270
|
+
return None
|
|
4271
|
+
|
|
4272
|
+
|
|
4222
4273
|
def _active_x11_window_geometry(display: str | None = None) -> dict[str, int] | None:
|
|
4223
4274
|
env = os.environ.copy()
|
|
4224
4275
|
if display:
|
|
@@ -4236,6 +4287,9 @@ def _active_x11_window_geometry(display: str | None = None) -> dict[str, int] |
|
|
|
4236
4287
|
except (subprocess.SubprocessError, FileNotFoundError):
|
|
4237
4288
|
return None
|
|
4238
4289
|
|
|
4290
|
+
screen_size = _x11_screen_size(display)
|
|
4291
|
+
screen_width = screen_size[0] if screen_size else None
|
|
4292
|
+
screen_height = screen_size[1] if screen_size else None
|
|
4239
4293
|
candidates: list[tuple[int, dict[str, int]]] = []
|
|
4240
4294
|
for line in tree.splitlines():
|
|
4241
4295
|
if '("1cv8c" "1cv8c")' not in line and '("1cv8" "1cv8")' not in line:
|
|
@@ -4247,17 +4301,51 @@ def _active_x11_window_geometry(display: str | None = None) -> dict[str, int] |
|
|
|
4247
4301
|
width, height, left, top = map(int, match.groups()[1:])
|
|
4248
4302
|
if width < 300 or height < 150:
|
|
4249
4303
|
continue
|
|
4250
|
-
if left < 0 or top < 0:
|
|
4251
|
-
continue
|
|
4252
4304
|
priority = 0
|
|
4253
4305
|
if title and title not in {"1cv8c", "1cv8", "Конфигурация"}:
|
|
4254
4306
|
priority += 100
|
|
4255
|
-
if
|
|
4307
|
+
if any(word in title for word in (
|
|
4308
|
+
"1С:Предприятие",
|
|
4309
|
+
"Бухгалтерия",
|
|
4310
|
+
"Предприятие",
|
|
4311
|
+
"Управление",
|
|
4312
|
+
"Зарплата",
|
|
4313
|
+
"ERP",
|
|
4314
|
+
"Комплексная",
|
|
4315
|
+
"Розница",
|
|
4316
|
+
)):
|
|
4256
4317
|
priority += 50
|
|
4257
4318
|
if any(word in title for word in ("создание", "Инструменты", "Шаблон", "Сессия", "DEV-AREA")):
|
|
4258
4319
|
priority += 25
|
|
4259
4320
|
priority += min(width * height // 10000, 50)
|
|
4260
|
-
|
|
4321
|
+
|
|
4322
|
+
# ``xwininfo -root -tree`` reports child coordinates relative to their
|
|
4323
|
+
# parent. For many 1C/X11 windows this means the client area appears as
|
|
4324
|
+
# e.g. ``1280x1024+0+0`` even when the decorated top-level window is
|
|
4325
|
+
# slightly larger or shifted. mss needs root-relative, in-bounds
|
|
4326
|
+
# monitor coordinates, so normalize the candidate to the virtual screen
|
|
4327
|
+
# instead of discarding negative/off-screen decorations.
|
|
4328
|
+
capture_left = max(0, left)
|
|
4329
|
+
capture_top = max(0, top)
|
|
4330
|
+
capture_width = width - max(0, -left)
|
|
4331
|
+
capture_height = height - max(0, -top)
|
|
4332
|
+
if screen_width is not None:
|
|
4333
|
+
capture_width = min(capture_width, max(0, screen_width - capture_left))
|
|
4334
|
+
if screen_height is not None:
|
|
4335
|
+
capture_height = min(capture_height, max(0, screen_height - capture_top))
|
|
4336
|
+
if capture_width < 300 or capture_height < 150:
|
|
4337
|
+
continue
|
|
4338
|
+
candidates.append((priority, {
|
|
4339
|
+
"left": capture_left,
|
|
4340
|
+
"top": capture_top,
|
|
4341
|
+
"width": capture_width,
|
|
4342
|
+
"height": capture_height,
|
|
4343
|
+
"title": title,
|
|
4344
|
+
"xwininfo_left": left,
|
|
4345
|
+
"xwininfo_top": top,
|
|
4346
|
+
"xwininfo_width": width,
|
|
4347
|
+
"xwininfo_height": height,
|
|
4348
|
+
}))
|
|
4261
4349
|
if not candidates:
|
|
4262
4350
|
return None
|
|
4263
4351
|
candidates.sort(key=lambda item: item[0], reverse=True)
|
|
@@ -4343,6 +4431,12 @@ def screenshot(
|
|
|
4343
4431
|
os.environ["DISPLAY"] = resolved_display
|
|
4344
4432
|
try:
|
|
4345
4433
|
geometry = _active_x11_window_geometry(resolved_display) if window else None
|
|
4434
|
+
if window and geometry is None:
|
|
4435
|
+
raise RuntimeError(
|
|
4436
|
+
"Cannot capture 1C window screenshot: window geometry was not detected. "
|
|
4437
|
+
"Use window=false to capture the whole virtual display, or maximize/resize the "
|
|
4438
|
+
"test client window and retry."
|
|
4439
|
+
)
|
|
4346
4440
|
with mss.mss() as sct:
|
|
4347
4441
|
monitor = geometry or sct.monitors[0]
|
|
4348
4442
|
image = sct.grab(monitor)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ctypes
|
|
4
4
|
import ctypes.wintypes
|
|
5
|
+
import logging
|
|
5
6
|
import os
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
@@ -9,6 +10,10 @@ import time
|
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
13
|
+
LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
TEST_CLIENT_WINDOW_TITLE = "Клиент тестирования (1С:Предприятие)"
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
@dataclass
|
|
14
19
|
class WindowControlResult:
|
|
@@ -97,6 +102,8 @@ def _maximize_with_wmctrl(title_contains: str, display: str, width: int | None,
|
|
|
97
102
|
candidates.append(item)
|
|
98
103
|
if _title_matches(title, title_contains):
|
|
99
104
|
chosen=item
|
|
105
|
+
if chosen is None and not title_contains and len(candidates) == 1:
|
|
106
|
+
chosen = candidates[0]
|
|
100
107
|
if chosen is None:
|
|
101
108
|
return WindowControlResult(False, "wmctrl", candidates=candidates, error="window not found")
|
|
102
109
|
wid=chosen["id"]
|
|
@@ -113,8 +120,12 @@ def _maximize_with_xdotool(title_contains: str, display: str, width: int | None,
|
|
|
113
120
|
return WindowControlResult(False, "xdotool", error="xdotool not installed")
|
|
114
121
|
try:
|
|
115
122
|
sw, sh = (width, height) if width and height else _screen_size(display, env)
|
|
116
|
-
pattern = title_contains or
|
|
123
|
+
pattern = title_contains or TEST_CLIENT_WINDOW_TITLE
|
|
117
124
|
ids = subprocess.check_output(["xdotool", "search", "--name", pattern], env=env, text=True).split()
|
|
125
|
+
if not ids and not title_contains:
|
|
126
|
+
all_ids = subprocess.check_output(["xdotool", "search", "--class", "1cv8"], env=env, text=True).split()
|
|
127
|
+
if len(all_ids) == 1:
|
|
128
|
+
ids = all_ids
|
|
118
129
|
if not ids:
|
|
119
130
|
return WindowControlResult(False, "xdotool", error="window not found")
|
|
120
131
|
wid=ids[-1]
|
|
@@ -128,11 +139,11 @@ def _maximize_with_xdotool(title_contains: str, display: str, width: int | None,
|
|
|
128
139
|
|
|
129
140
|
|
|
130
141
|
def _title_matches(title: str, title_contains: str) -> bool:
|
|
131
|
-
if title_contains
|
|
132
|
-
return
|
|
142
|
+
if title_contains:
|
|
143
|
+
return title_contains.lower() in title.lower()
|
|
133
144
|
if "Конфигурация" in title:
|
|
134
145
|
return False
|
|
135
|
-
return
|
|
146
|
+
return TEST_CLIENT_WINDOW_TITLE.lower() in title.lower()
|
|
136
147
|
|
|
137
148
|
|
|
138
149
|
def _maximize_with_xlib(title_contains: str, display: str, width: int | None, height: int | None, env: dict[str, str]) -> WindowControlResult:
|
|
@@ -167,6 +178,7 @@ def _maximize_with_xlib(title_contains: str, display: str, width: int | None, he
|
|
|
167
178
|
pass
|
|
168
179
|
return ""
|
|
169
180
|
chosen=None
|
|
181
|
+
candidate_windows=[]
|
|
170
182
|
for win in walk(root):
|
|
171
183
|
title=get_name(win)
|
|
172
184
|
if not title:
|
|
@@ -179,8 +191,11 @@ def _maximize_with_xlib(title_contains: str, display: str, width: int | None, he
|
|
|
179
191
|
continue
|
|
180
192
|
item={"title": title, "width": geo.width, "height": geo.height}
|
|
181
193
|
candidates.append(item)
|
|
194
|
+
candidate_windows.append((win, item))
|
|
182
195
|
if _title_matches(title, title_contains):
|
|
183
196
|
chosen=(win, item)
|
|
197
|
+
if chosen is None and not title_contains and len(candidate_windows) == 1:
|
|
198
|
+
chosen = candidate_windows[0]
|
|
184
199
|
if chosen is None:
|
|
185
200
|
return WindowControlResult(False, "xlib", candidates=candidates, error="window not found")
|
|
186
201
|
win,item=chosen
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{answer42-0.2.22 → answer42-0.2.24}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|