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.
Files changed (58) hide show
  1. {answer42-0.2.22 → answer42-0.2.24}/.gitignore +1 -1
  2. {answer42-0.2.22 → answer42-0.2.24}/PKG-INFO +2 -2
  3. {answer42-0.2.22 → answer42-0.2.24}/README.md +1 -1
  4. {answer42-0.2.22 → answer42-0.2.24}/docs/architecture.md +4 -4
  5. {answer42-0.2.22 → answer42-0.2.24}/pyproject.toml +1 -1
  6. {answer42-0.2.22 → answer42-0.2.24}/scripts/e2e_stable.py +1 -1
  7. {answer42-0.2.22 → answer42-0.2.24}/scripts/openclaw_mcp_autoreload.py +1 -1
  8. {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
  9. {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
  10. {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
  11. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
  12. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/credentials.py +2 -2
  13. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/os_support.py +1 -1
  14. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/runtime.py +1 -1
  15. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/server.py +133 -39
  16. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/window_control.py +19 -4
  17. {answer42-0.2.22 → answer42-0.2.24}/LICENSE +0 -0
  18. {answer42-0.2.22 → answer42-0.2.24}/credentials.example.json +0 -0
  19. {answer42-0.2.22 → answer42-0.2.24}/docs/agent-installation.md +0 -0
  20. {answer42-0.2.22 → answer42-0.2.24}/docs/assets/answer42-logo.png +0 -0
  21. {answer42-0.2.22 → answer42-0.2.24}/docs/assets/platform42-logo.svg +0 -0
  22. {answer42-0.2.22 → answer42-0.2.24}/docs/installation.md +0 -0
  23. {answer42-0.2.22 → answer42-0.2.24}/scripts/build_cf.py +0 -0
  24. {answer42-0.2.22 → answer42-0.2.24}/scripts/build_pages.py +0 -0
  25. {answer42-0.2.22 → answer42-0.2.24}/scripts/rag_cli.py +0 -0
  26. {answer42-0.2.22 → answer42-0.2.24}/src/cf/ConfigDumpInfo.xml +0 -0
  27. {answer42-0.2.22 → answer42-0.2.24}/src/cf/Configuration.xml +0 -0
  28. {answer42-0.2.22 → answer42-0.2.24}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
  29. {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
  30. {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
  31. {answer42-0.2.22 → answer42-0.2.24}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
  32. {answer42-0.2.22 → answer42-0.2.24}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
  33. {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
  34. {answer42-0.2.22 → answer42-0.2.24}/src/client_cf/ConfigDumpInfo.xml +0 -0
  35. {answer42-0.2.22 → answer42-0.2.24}/src/client_cf/Configuration.xml +0 -0
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/__init__.py +0 -0
  43. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
  44. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/__init__.py +0 -0
  45. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/skills/answer42/SKILL.md +0 -0
  46. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
  47. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/bridge.py +0 -0
  48. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/platform.py +0 -0
  49. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/protocol.py +0 -0
  50. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/__init__.py +0 -0
  51. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/detect.py +0 -0
  52. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/model.py +0 -0
  53. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/parsers.py +0 -0
  54. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/service.py +0 -0
  55. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/rag/store.py +0 -0
  56. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/recorder.py +0 -0
  57. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/release_helper.py +0 -0
  58. {answer42-0.2.22 → answer42-0.2.24}/src/mcp_1c/skill_installer.py +0 -0
@@ -7,7 +7,7 @@ venv/
7
7
 
8
8
  # Local credentials (never commit secrets)
9
9
  credentials.json
10
- .onec-mcp-credentials.json
10
+ .answer42-credentials.json
11
11
 
12
12
  # Build artefacts
13
13
  build/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: answer42
3
- Version: 0.2.22
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
- ~/.onec-mcp-credentials.json
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
- ~/.onec-mcp-credentials.json
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/onec-mcp/sessions/{session_id})
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`, `maximize_test_client_window`, `maximize_test_client_external_window`;
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/onec-mcp).
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/onec-mcp` |
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.22"
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/onec-mcp/shared-client-bases")) / shared_id / "ib"
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
- Элемент.Вставить("text", Текст);
2951
- Исключение
2952
- Элемент.Вставить("text_error", ОписаниеОшибки());
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
- Поле.УстановитьТекущуюОбласть(1, 1, 1, 1);
2992
- КоличествоСтрок = БезопасноеСвойство(Поле, "КоличествоСтрок", 0);
2993
- КоличествоКолонок = БезопасноеСвойство(Поле, "КоличествоКолонок", 0);
3003
+ КоличествоСтрок = Поле.ПолучитьРазмерОбластиДанныхДокументаПоВертикали();
3004
+ КоличествоКолонок = Поле.ПолучитьРазмерОбластиДанныхДокументаПоГоризонтали();
2994
3005
  Если КоличествоСтрок = 0 Или КоличествоКолонок = 0 Тогда
2995
- Возврат Новый Структура("found,rows,columns,cells", Истина, 0, 0, Новый Массив);
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
- Возврат Новый Структура("found,rows,columns,cells", Истина, КоличествоСтрок, КоличествоКолонок, Ячейки);
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
- // недоступен/ненадежен; используем штатный метод tested field
3040
- // ЗаписатьСодержимоеВФайл(<СохранитьКак>) и очередь результата файлового
3041
- // диалога. Для PDF пробуем несколько индексов фильтра и оба значения
3042
- // <СохранитьКак> (Истина/Ложь), чтобы не угадывать поведение конкретной формы.
3043
- Если ФорматФайла = "pdf" Тогда
3044
- Файлы = Новый Массив;
3045
- Ошибки = Новый Массив;
3046
- ВариантыСохранитьКак = Новый Массив;
3047
- ВариантыСохранитьКак.Добавить(Истина);
3048
- ВариантыСохранитьКак.Добавить(Ложь);
3049
- Для Каждого СохранитьКак Из ВариантыСохранитьКак Цикл
3050
- Для ИндексФильтра = 1 По 10 Цикл
3051
- ПутьПопытки = ДобавитьСуффиксКПутиMCP(Путь, "_saveas_" + ?(СохранитьКак, "true", "false") + "_filter" + Строка(ИндексФильтра));
3052
- Попытка
3053
- ТестируемоеПриложение.УстановитьРезультатДиалогаВыбораФайла(Истина, ПутьПопытки, ИндексФильтра);
3054
- Поле.ЗаписатьСодержимоеВФайл(СохранитьКак);
3055
- ОписаниеФайла = Новый Структура("path,filter_index,save_as", ПутьПопытки, ИндексФильтра, СохранитьКак);
3056
- Файлы.Добавить(ОписаниеФайла);
3057
- Исключение
3058
- Ошибки.Добавить(Новый Структура("filter_index,save_as,error", ИндексФильтра, СохранитьКак, ОписаниеОшибки()));
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
- Если Файлы.Количество() > 0 Тогда
3063
- Возврат Новый Структура("saved,path,format,method,files,errors", Истина, Путь, ФорматФайла, "write_content_to_file_dialog_filters", Файлы, Ошибки);
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>25</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>25</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>10</v8:Length>
93
+ <v8:Length>80</v8:Length>
94
94
  <v8:AllowedLength>Variable</v8:AllowedLength>
95
95
  </v8:StringQualifiers>
96
96
  </Type>
@@ -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 (``~/.onec-mcp-credentials.json``).
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() / ".onec-mcp-credentials.json"))
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()) / "onec-mcp"
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 = "1280x1024x24"
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 | mxl | html | txt"] = "pdf",
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
- header = p.read_bytes()[:8]
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 in {"xlsx", "docx"} and header.startswith(b"PK\x03\x04"):
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 (header.startswith(b"<") or header.startswith(b"\xef\xbb\xbf<")):
3339
+ if wanted in {"html", "htm"} and (data.startswith(b"<") or data.startswith(b"\xef\xbb\xbf<")):
3346
3340
  return item
3347
- if wanted in {"txt", "mxl"} and p.stat().st_size > 0:
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 "1С:Предприятие" in title:
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
- candidates.append((priority, {"left": left, "top": top, "width": width, "height": height, "title": title}))
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 and title_contains.lower() not in title.lower():
132
- return False
142
+ if title_contains:
143
+ return title_contains.lower() in title.lower()
133
144
  if "Конфигурация" in title:
134
145
  return False
135
- return any(s in title for s in ["DEV-AREA", "Начальная", "Приложения", "1С:Предприятие", "ЭмФешн", "Время создания"])
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