answer42 0.3.0__tar.gz → 0.3.2__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.3.0 → answer42-0.3.2}/PKG-INFO +1 -1
- {answer42-0.3.0 → answer42-0.3.2}/pyproject.toml +1 -1
- {answer42-0.3.0 → answer42-0.3.2}/scripts/e2e_stable.py +25 -10
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl +6 -3
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/skills/answer42/SKILL.md +5 -3
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +5 -2
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/dump.py +70 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/server.py +84 -7
- {answer42-0.3.0 → answer42-0.3.2}/.gitignore +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/LICENSE +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/README.md +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/credentials.example.json +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/docs/agent-installation.md +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/docs/architecture.md +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/docs/assets/answer42-logo.png +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/docs/assets/platform42-logo.svg +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/docs/installation.md +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/scripts/build_cf.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/scripts/build_pages.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/scripts/openclaw_mcp_autoreload.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/scripts/rag_cli.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/Configuration.xml +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/client_cf/Catalogs//320/237/320/241_/320/222/320/273/320/260/320/264/320/265/320/273/320/265/321/206.xml" +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/client_cf/Catalogs//320/237/320/241_/320/241/320/277/321/200/320/260/320/262/320/276/321/207/320/275/320/270/320/2721.xml" +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/client_cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/client_cf/Configuration.xml +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/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.3.0 → answer42-0.3.2}/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.3.0 → answer42-0.3.2}/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.3.0 → answer42-0.3.2}/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.3.0 → answer42-0.3.2}/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.3.0 → answer42-0.3.2}/src/mcp_1c/__init__.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/__init__.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/bridge.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/credentials.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/os_support.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/platform.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/protocol.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/__init__.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/detect.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/model.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/parsers.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/service.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/store.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/recorder.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/release_helper.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/runtime.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/skill_installer.py +0 -0
- {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/window_control.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: answer42
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "answer42"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
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"
|
|
@@ -134,6 +134,23 @@ async def call_ui_ok(session: ClientSession, name: str, args: dict | None = None
|
|
|
134
134
|
return value
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def call_ui_optional_error(session: ClientSession, name: str, args: dict | None = None, allowed_contains: str = ""):
|
|
140
|
+
args = args or {}
|
|
141
|
+
result = await session.call_tool(name, args)
|
|
142
|
+
if not result.isError:
|
|
143
|
+
value = _parse_tool_result(result)
|
|
144
|
+
print(f"OK {name}", flush=True)
|
|
145
|
+
return value
|
|
146
|
+
text = "\n".join(getattr(item, "text", "") for item in result.content)
|
|
147
|
+
if allowed_contains and allowed_contains in text:
|
|
148
|
+
print(f"OPTIONAL_ERROR {name}: {allowed_contains}", flush=True)
|
|
149
|
+
return {"optional_error": text}
|
|
150
|
+
await _record_manual_step(session, args.get("session_id", "e2e-mcp"), name, args, {"error": text})
|
|
151
|
+
raise AssertionError(f"{name} unexpectedly failed: {result.content}")
|
|
152
|
+
|
|
153
|
+
|
|
137
154
|
async def call_ui_expected_error(session: ClientSession, name: str, args: dict | None = None, contains: str = ""):
|
|
138
155
|
args = args or {}
|
|
139
156
|
result = await session.call_tool(name, args)
|
|
@@ -288,7 +305,7 @@ async def run_smoke(session: ClientSession, sid: str, suffix: str, screenshot_di
|
|
|
288
305
|
{"navigation_link": "e1cib/list/Справочник.DefinitelyMissing", "session_id": sid},
|
|
289
306
|
contains="Не удалось перейти по навигационной ссылке",
|
|
290
307
|
)
|
|
291
|
-
await
|
|
308
|
+
await call_ui_optional_error(session, "click_button", {"name": "OK", "session_id": sid}, "Кнопка не найдена")
|
|
292
309
|
await capture_screenshot(session, sid, str(screenshot_dir / "01-initial-window.png"))
|
|
293
310
|
|
|
294
311
|
await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Владелец", "session_id": sid})
|
|
@@ -374,11 +391,10 @@ async def run_dynamic_lists(session: ClientSession, sid: str, suffix: str, scree
|
|
|
374
391
|
|
|
375
392
|
await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Справочник1", "session_id": sid})
|
|
376
393
|
await call_ui_ok(session, "table_goto_row", {"name": "Список", "text": item2, "session_id": sid})
|
|
377
|
-
await call_ui_ok(session, "
|
|
378
|
-
await call_ui_ok(session, "dynamic_list_output", {"session_id": sid})
|
|
379
|
-
await capture_screenshot(session, sid, str(screenshot_dir / "06-dynamic-list-output-
|
|
380
|
-
|
|
381
|
-
await capture_screenshot(session, sid, str(screenshot_dir / "07-dynamic-list-output.png"))
|
|
394
|
+
await call_ui_ok(session, "dynamic_list_sort_by_column", {"column_title": "Наименование", "name": "Список", "session_id": sid})
|
|
395
|
+
output = await call_ui_ok(session, "dynamic_list_output", {"session_id": sid})
|
|
396
|
+
await capture_screenshot(session, sid, str(screenshot_dir / "06-dynamic-list-output-result.png"))
|
|
397
|
+
assert output.get("tabular_documents"), output
|
|
382
398
|
tdocs = await call_ui_ok(session, "tabular_documents", {"session_id": sid})
|
|
383
399
|
tdocs_text = str(tdocs)
|
|
384
400
|
has_tabular_document = (
|
|
@@ -411,16 +427,15 @@ async def run_dynamic_lists(session: ClientSession, sid: str, suffix: str, scree
|
|
|
411
427
|
await capture_screenshot(session, sid, str(screenshot_dir / "08-list-settings-open.png"))
|
|
412
428
|
await call_ui_ok(session, "close_form", {"session_id": sid})
|
|
413
429
|
await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Справочник1", "session_id": sid})
|
|
414
|
-
await call_ui_ok(session, "
|
|
430
|
+
await call_ui_ok(session, "form_open_settings", {"session_id": sid})
|
|
415
431
|
await call_ui_ok(session, "close_form", {"session_id": sid})
|
|
416
432
|
|
|
417
|
-
available = await call_ui_ok(session, "
|
|
433
|
+
available = await call_ui_ok(session, "dynamic_list_settings_available_fields", {"session_id": sid})
|
|
418
434
|
assert available.get("count", 0) > 0, available
|
|
419
435
|
assert "Наименование" in str(available), available
|
|
420
|
-
await call_ui_ok(session, "
|
|
436
|
+
await call_ui_ok(session, "dynamic_list_set_settings", {
|
|
421
437
|
"orders": json.dumps([{"field": "Наименование", "direction": "Asc"}], ensure_ascii=False),
|
|
422
438
|
"clear_first": True,
|
|
423
|
-
"name": "Список",
|
|
424
439
|
"session_id": sid,
|
|
425
440
|
}, timeout=180)
|
|
426
441
|
await capture_screenshot(session, sid, str(screenshot_dir / "12-final-list.png"))
|
|
@@ -3738,7 +3738,7 @@
|
|
|
3738
3738
|
КонецЦикла;
|
|
3739
3739
|
Если ОткрытВыпадающийСписокПоляMCP(Поле) Тогда
|
|
3740
3740
|
Попытка
|
|
3741
|
-
Поле.ОжидатьФормированияВыпадающегоСписка(
|
|
3741
|
+
Поле.ОжидатьФормированияВыпадающегоСписка(3);
|
|
3742
3742
|
Представления = Поле.ПолучитьПредставлениеСпискаВыбора();
|
|
3743
3743
|
Если Представления.Количество() > 0 Тогда
|
|
3744
3744
|
ИндексВыбора = ИндексРелевантногоВариантаСпискаВыбораMCP(Представления, Значение);
|
|
@@ -3777,7 +3777,7 @@
|
|
|
3777
3777
|
Если ОткрытВыпадающийСписокПоляMCP(Поле) Тогда
|
|
3778
3778
|
СписокСформирован = Ложь;
|
|
3779
3779
|
Попытка
|
|
3780
|
-
СписокСформирован = Поле.ОжидатьФормированияВыпадающегоСписка(
|
|
3780
|
+
СписокСформирован = Поле.ОжидатьФормированияВыпадающегоСписка(3);
|
|
3781
3781
|
Исключение
|
|
3782
3782
|
КонецПопытки;
|
|
3783
3783
|
Представления = Поле.ПолучитьПредставлениеСпискаВыбора();
|
|
@@ -4872,7 +4872,10 @@
|
|
|
4872
4872
|
|
|
4873
4873
|
&НаКлиенте
|
|
4874
4874
|
Функция ИмяЭлементаНастроекСпискаMCP(Раздел)
|
|
4875
|
-
|
|
4875
|
+
// Не используем Формат(..., "ЧГ="): для 0 он возвращает пустую строку,
|
|
4876
|
+
// из-за чего filter-раздел искался как "...ЭлементДоступныеПоляТаблица"
|
|
4877
|
+
// вместо реального имени "...Элемент0ДоступныеПоляТаблица".
|
|
4878
|
+
Возврат "КомпоновщикНастроекПользовательскиеНастройкиЭлемент" + Строка(НомерЭлементаНастроекСпискаMCP(Раздел));
|
|
4876
4879
|
КонецФункции
|
|
4877
4880
|
|
|
4878
4881
|
&НаКлиенте
|
|
Binary file
|
|
Binary file
|
|
@@ -49,7 +49,7 @@ description: "Работа с MCP Answer42 для 1С UI automation"
|
|
|
49
49
|
- Linux: X11/Xvfb использовать только для скриншотов и окна 1С, не для ввода/кликов/автоматизации.
|
|
50
50
|
- Windows: Answer42 должен запускаться в интерактивной desktop-сессии пользователя; Windows service/headless/заблокированный RDP-сеанс для UI automation и screenshot ненадёжен.
|
|
51
51
|
- Для ссылочных полей предпочитай стандартную форму выбора 1С (`choose_field_from_list`, `choose_field_first_row`, `choose_current_row`), а не ввод строки.
|
|
52
|
-
- `
|
|
52
|
+
- `open_navigation_link` — только для явных навигационных ссылок `e1cib/...`. Не передавай туда псевдокоманды/команды формы вроде `ФормаНайти`, `СледующаяСтрока`, `СписокВывестиСписок`; для них нужны отдельные first-class MCP tools. Если навигационная ссылка некорректна, tool сам попробует подсказать похожие объекты через `rag_lookup_object`, `rag_query` и `metadata_objects_from_catalog`.
|
|
53
53
|
- После каждого действия проверяй диагностику результата: `client_diagnostics.user_messages`, `error_info`, `modal_title`, `modal_buttons`, `modal_text`, `modal_texts`, `modal_is_error_window`.
|
|
54
54
|
- Не закрывай автоматически сложные диалоги с несколькими кнопками (`OK`, `Restart`, `Finish`, `Yes/No/Cancel`). Допустимо автозакрытие только простых предупреждений/ошибок с одной кнопкой закрытия.
|
|
55
55
|
- Для динамических списков предпочитай `table_move_row`, `table_current_row`, `table_selected_rows`, `table_cell_text`; `table_rows` использует навигационный fallback, но на больших списках может быть дорогим.
|
|
@@ -99,10 +99,12 @@ start_session(
|
|
|
99
99
|
|
|
100
100
|
## Навигация
|
|
101
101
|
|
|
102
|
-
- Открывай формы/списки через `
|
|
102
|
+
- Открывай формы/списки через `open_navigation_link` с `e1cib/list/...` или `e1cib/app/...`, когда доступно.
|
|
103
103
|
- Для поиска объектов используй `find_object`, `activate_object`, `ui_tree`. `ui_tree` по умолчанию показывает только видимые/активные для пользователя элементы и всегда добавляет `visible/available/enabled/read_only`; для полной диагностики скрытых страниц/элементов передавай `include_hidden=true`.
|
|
104
|
-
- Для команд формы используй специализированные MCP tools (`table_move_row`, `dynamic_list_*`, `click_button`, `activate_object` и т.п.). Для навигационных команд предпочитай `command_interface_search/commands`, для команд конкретного объекта — `object_command_panel/object_context_menu`; `command_bar` — низкоуровневая диагностика. Не прокидывай имена команд в `
|
|
104
|
+
- Для команд формы используй специализированные MCP tools (`table_move_row`, `dynamic_list_*`, `click_button`, `activate_object` и т.п.). Для навигационных команд предпочитай `command_interface_search/commands`, для команд конкретного объекта — `object_command_panel/object_context_menu`; `command_bar` — низкоуровневая диагностика. Не прокидывай имена команд в `open_navigation_link`.
|
|
105
105
|
- `dynamic_list_output`, `dynamic_list_open_settings`, `dynamic_list_clear_settings`, `form_open_settings` нажимают кнопки формы через test-client API (`НайтиКнопку` + `Нажать()`), а не выполняют имена команд через `ВыполнитьКоманду()`. Если авто-кандидат не подходит, передавай `button` — техническое имя или заголовок кнопки.
|
|
106
|
+
- Для поиска правильных имён метаданных используй `metadata_objects_from_catalog(query=...)` как live fallback к RAG. Он сначала пробует `dynamic_list_output` и парсинг табличного документа (`source_method="dynamic_list_output"`), а если команда вывода недоступна — читает список через навигацию (`source_method="table_rows"`). На больших динамических списках это может быть заметно дороже, но устойчивее к виртуальной подгрузке строк.
|
|
107
|
+
- Если `open_navigation_link` вернул блок `Возможно, вы имели в виду:`, используй предложенную `e1cib/list/...`/`e1cib/app/...` ссылку вместо повторной ручной догадки.
|
|
106
108
|
|
|
107
109
|
## Динамические списки: добавление вложенных полей через «Изменить форму»
|
|
108
110
|
|
|
@@ -30,13 +30,16 @@ rag_session_autoconfigure()
|
|
|
30
30
|
### Если подходящего источника нет — fallback-выгрузка
|
|
31
31
|
|
|
32
32
|
`rag_session_autoconfigure(dump_if_missing=true)` при отсутствии точного совпадения:
|
|
33
|
-
- Для прямого подключения (`/S` или `/F`) — выгружает конфигурацию
|
|
33
|
+
- Для прямого подключения (`/S` или `/F`) — выгружает конфигурацию через Designer `DumpConfigToFiles`, а все установленные расширения одним Designer-вызовом `DumpConfigToFiles -AllExtensions` (по умолчанию `dump_all_extensions=true`).
|
|
34
34
|
- Для веб-сессий (`/WS`) — нужно передать `dump_connection_value="server/ib"` или `file_infobase_path="/path/to/ib"`.
|
|
35
|
+
- Если direct-подключение использует другие креды, чем web-публикация (например multitenant/separated-data база), передай `dump_username`/`dump_password`; иначе будут переиспользованы креды live-сессии.
|
|
35
36
|
- Можно передать `cf_files`/`cfe_files` — они загружаются во временную файловую базу, выгружаются в XML, временная база удаляется.
|
|
37
|
+
- Расширения с именами `EF_*` считаются служебными/fresh patch и не регистрируются в RAG snapshot.
|
|
38
|
+
- Если нужно не выгружать все live-расширения (например, их слишком много), передай `dump_all_extensions=false`.
|
|
36
39
|
|
|
37
40
|
```text
|
|
38
41
|
# Прямое серверное подключение
|
|
39
|
-
rag_session_autoconfigure(dump_if_missing=true, dump_connection_value="dev-area-01.42clouds.com/ms")
|
|
42
|
+
rag_session_autoconfigure(dump_if_missing=true, dump_connection_value="dev-area-01.42clouds.com/ms", dump_username="Admin", dump_password="...")
|
|
40
43
|
|
|
41
44
|
# Из CF/CFE файлов
|
|
42
45
|
rag_session_autoconfigure(dump_if_missing=true, cf_files="config.cf", cfe_files="ext1.cfe,ext2.cfe")
|
|
@@ -102,6 +102,76 @@ def dump_infobase_to_xml(
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def dump_all_extensions_to_xml(
|
|
106
|
+
platform_dir: Path,
|
|
107
|
+
connection_mode: str,
|
|
108
|
+
connection_value: str,
|
|
109
|
+
output_dir: Path,
|
|
110
|
+
username: str = "",
|
|
111
|
+
password: str = "",
|
|
112
|
+
display: str = "",
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
"""Dump all live infobase extensions to XML source subdirectories via Designer.
|
|
115
|
+
|
|
116
|
+
1C Designer supports the ``DumpConfigToFiles ... -AllExtensions`` switch.
|
|
117
|
+
Unlike dumping the main configuration, the output directory becomes a
|
|
118
|
+
container with one child directory per extension; each child is a normal
|
|
119
|
+
Designer XML source tree with its own ``Configuration.xml``.
|
|
120
|
+
"""
|
|
121
|
+
onec = str(platform_dir / os_support.executable_name("1cv8"))
|
|
122
|
+
if not Path(onec).exists():
|
|
123
|
+
raise FileNotFoundError(f"1cv8 (Designer/thick client) not found: {onec}")
|
|
124
|
+
|
|
125
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
effective_display = display or _find_free_display()
|
|
127
|
+
|
|
128
|
+
argv = [onec, "DESIGNER"]
|
|
129
|
+
if connection_mode == "server":
|
|
130
|
+
argv.extend(["/S", connection_value])
|
|
131
|
+
elif connection_mode in ("file-direct", "file"):
|
|
132
|
+
argv.extend(["/F", connection_value])
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError(f"Unsupported connection mode for dump: {connection_mode!r}")
|
|
135
|
+
|
|
136
|
+
if username:
|
|
137
|
+
argv.extend(["/N", username])
|
|
138
|
+
if password:
|
|
139
|
+
argv.extend(["/P", password])
|
|
140
|
+
|
|
141
|
+
argv.extend(["/DumpConfigToFiles", str(output_dir)])
|
|
142
|
+
argv.extend(["-Format", "Hierarchical", "-AllExtensions"])
|
|
143
|
+
argv.extend(["/DisableStartupDialogs", "/DisableStartupMessages"])
|
|
144
|
+
|
|
145
|
+
completed = _run_designer(argv, display=effective_display)
|
|
146
|
+
if completed.returncode != 0:
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
f"Designer DumpConfigToFiles all extensions failed (rc={completed.returncode}): "
|
|
149
|
+
f"stdout={completed.stdout!r} stderr={completed.stderr!r}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
extensions: list[dict[str, Any]] = []
|
|
153
|
+
for child in sorted((item for item in output_dir.iterdir() if item.is_dir()), key=lambda item: item.name.casefold()):
|
|
154
|
+
if not (child / "Configuration.xml").exists():
|
|
155
|
+
continue
|
|
156
|
+
extensions.append({
|
|
157
|
+
"name": child.name,
|
|
158
|
+
"output_dir": str(child),
|
|
159
|
+
"files_count": sum(1 for item in child.rglob("*") if item.is_file()),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
file_count = sum(1 for item in output_dir.rglob("*") if item.is_file())
|
|
163
|
+
LOGGER.info("Dumped %d extension files (%d extensions) to %s", file_count, len(extensions), output_dir)
|
|
164
|
+
return {
|
|
165
|
+
"output_dir": str(output_dir),
|
|
166
|
+
"connection_mode": connection_mode,
|
|
167
|
+
"connection_value": connection_value,
|
|
168
|
+
"all_extensions": True,
|
|
169
|
+
"extensions_count": len(extensions),
|
|
170
|
+
"extensions": extensions,
|
|
171
|
+
"files_count": file_count,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
105
175
|
def dump_cf_to_xml(
|
|
106
176
|
platform_dir: Path,
|
|
107
177
|
cf_path: Path,
|
|
@@ -35,7 +35,7 @@ from . import release_helper
|
|
|
35
35
|
from .bridge import OneCBridgeServer
|
|
36
36
|
from .credentials import get_store as _credentials_store
|
|
37
37
|
from .rag import RagService
|
|
38
|
-
from .rag.dump import dump_cf_to_xml, dump_infobase_to_xml
|
|
38
|
+
from .rag.dump import dump_all_extensions_to_xml, dump_cf_to_xml, dump_infobase_to_xml
|
|
39
39
|
from .window_control import active_1c_window_geometry as _active_windows_1c_window_geometry
|
|
40
40
|
from .window_control import maximize_test_client_window as _maximize_external_window
|
|
41
41
|
from .recorder import RecordingSession
|
|
@@ -1767,6 +1767,11 @@ def _dump_source_name(prefix: str, runtime_name: str, runtime_version: str, used
|
|
|
1767
1767
|
return name
|
|
1768
1768
|
|
|
1769
1769
|
|
|
1770
|
+
def _is_ignored_runtime_extension_name(name: str) -> bool:
|
|
1771
|
+
"""Return True for service/fresh patch extensions that should not be added to RAG snapshots."""
|
|
1772
|
+
return str(name or "").casefold().startswith("ef_")
|
|
1773
|
+
|
|
1774
|
+
|
|
1770
1775
|
def _rag_match_needs_dump(match: dict[str, Any], target_version: str) -> bool:
|
|
1771
1776
|
if not match.get("matched"):
|
|
1772
1777
|
return True
|
|
@@ -1868,6 +1873,9 @@ async def rag_session_autoconfigure(
|
|
|
1868
1873
|
dump_connection_mode: Annotated[str, "auto | server | file-direct; required for dumping when current session uses a web URL"] = "auto",
|
|
1869
1874
|
dump_connection_value: Annotated[str, "Direct server/infobase (/S) or file infobase path (/F) used only for source dumping"] = "",
|
|
1870
1875
|
file_infobase_path: Annotated[str, "Alias for dump_connection_value when dumping from a file infobase"] = "",
|
|
1876
|
+
dump_username: Annotated[str, "Optional infobase username for direct Designer dump; defaults to session username"] = "",
|
|
1877
|
+
dump_password: Annotated[str, "Optional infobase password for direct Designer dump; defaults to session password"] = "",
|
|
1878
|
+
dump_all_extensions: Annotated[bool, "Dump all installed extensions via Designer -AllExtensions when dump_if_missing is enabled"] = True,
|
|
1871
1879
|
cf_files: Annotated[str, "Optional JSON/comma list of .cf files to load into temporary file IBs and dump"] = "",
|
|
1872
1880
|
cfe_files: Annotated[str, "Optional JSON/comma list of .cfe files to load into temporary file IBs and dump as extensions"] = "",
|
|
1873
1881
|
dump_root: Annotated[str, "Optional persistent directory for generated RAG source dumps"] = "",
|
|
@@ -1886,6 +1894,9 @@ async def rag_session_autoconfigure(
|
|
|
1886
1894
|
Dumping requires a direct Designer connection: server infobase (`server/ib`) or
|
|
1887
1895
|
file infobase path. Web `/WS` URLs cannot be dumped; for web sessions pass
|
|
1888
1896
|
`dump_connection_value`, `file_infobase_path`, or `cf_files`/`cfe_files`.
|
|
1897
|
+
If the direct dump connection has different users than the web publication
|
|
1898
|
+
(for example multitenant/separated-data infobases), pass `dump_username` and
|
|
1899
|
+
`dump_password`; otherwise the current session credentials are reused.
|
|
1889
1900
|
Temporary infobases created from CF/CFE files are deleted after dumping by
|
|
1890
1901
|
default; generated XML source directories are kept under `dump_root` (or the
|
|
1891
1902
|
Answer42 temp rag-dumps directory) because the RAG index references them.
|
|
@@ -1898,6 +1909,9 @@ async def rag_session_autoconfigure(
|
|
|
1898
1909
|
"dump_connection_mode": dump_connection_mode,
|
|
1899
1910
|
"dump_connection_value": dump_connection_value,
|
|
1900
1911
|
"file_infobase_path": file_infobase_path,
|
|
1912
|
+
"dump_username": dump_username,
|
|
1913
|
+
"dump_password": "***" if dump_password else "",
|
|
1914
|
+
"dump_all_extensions": dump_all_extensions,
|
|
1901
1915
|
"cf_files": cf_files,
|
|
1902
1916
|
"cfe_files": cfe_files,
|
|
1903
1917
|
"dump_root": dump_root,
|
|
@@ -1919,6 +1933,7 @@ async def rag_session_autoconfigure(
|
|
|
1919
1933
|
config_name = str(config.get("configuration_name") or "")
|
|
1920
1934
|
config_version = str(config.get("configuration_version") or "")
|
|
1921
1935
|
installed_extensions = extensions_info.get("extensions", []) if isinstance(extensions_info, dict) else []
|
|
1936
|
+
extensions_from_designer = False
|
|
1922
1937
|
|
|
1923
1938
|
warnings: list[str] = []
|
|
1924
1939
|
dumps: list[dict[str, Any]] = []
|
|
@@ -1928,6 +1943,8 @@ async def rag_session_autoconfigure(
|
|
|
1928
1943
|
|
|
1929
1944
|
cf_paths = [Path(item).expanduser() for item in _json_list(cf_files)]
|
|
1930
1945
|
cfe_paths = [Path(item).expanduser() for item in _json_list(cfe_files)]
|
|
1946
|
+
effective_dump_username = dump_username if dump_username else sess.username
|
|
1947
|
+
effective_dump_password = dump_password if dump_password else sess.password
|
|
1931
1948
|
|
|
1932
1949
|
if dump_if_missing and cf_paths:
|
|
1933
1950
|
dump_base_dir = dump_base_dir or _rag_dump_root(session_id, dump_root)
|
|
@@ -1973,6 +1990,56 @@ async def rag_session_autoconfigure(
|
|
|
1973
1990
|
sources.append(generated)
|
|
1974
1991
|
extension_sources.append(generated)
|
|
1975
1992
|
|
|
1993
|
+
if dump_if_missing and dump_all_extensions:
|
|
1994
|
+
dump_mode, dump_value = _effective_dump_connection(sess, dump_connection_mode, dump_connection_value, file_infobase_path)
|
|
1995
|
+
dump_base_dir = dump_base_dir or _rag_dump_root(session_id, dump_root)
|
|
1996
|
+
all_ext_dir = dump_base_dir / "dump_all_extensions"
|
|
1997
|
+
all_ext_dump_info = await asyncio.to_thread(
|
|
1998
|
+
dump_all_extensions_to_xml,
|
|
1999
|
+
sess.platform_dir,
|
|
2000
|
+
dump_mode,
|
|
2001
|
+
dump_value,
|
|
2002
|
+
all_ext_dir,
|
|
2003
|
+
effective_dump_username,
|
|
2004
|
+
effective_dump_password,
|
|
2005
|
+
sess.display,
|
|
2006
|
+
)
|
|
2007
|
+
dumps.append(all_ext_dump_info | {"source_name": "dump_all_extensions", "source_kind": "extensions_container"})
|
|
2008
|
+
ignored_extensions: list[str] = []
|
|
2009
|
+
for ext_dump in all_ext_dump_info.get("extensions", []) or []:
|
|
2010
|
+
out_dir = Path(str(ext_dump.get("output_dir") or ""))
|
|
2011
|
+
ext_meta = _source_metadata(str(out_dir))
|
|
2012
|
+
ext_name = ext_meta.get("name") or str(ext_dump.get("name") or out_dir.name)
|
|
2013
|
+
ext_version = ext_meta.get("version") or ""
|
|
2014
|
+
if _is_ignored_runtime_extension_name(ext_name):
|
|
2015
|
+
ignored_extensions.append(ext_name)
|
|
2016
|
+
dumps.append(ext_dump | {
|
|
2017
|
+
"source_kind": "extension",
|
|
2018
|
+
"runtime_name": ext_name,
|
|
2019
|
+
"runtime_version": ext_version,
|
|
2020
|
+
"ignored": True,
|
|
2021
|
+
"ignore_reason": "EF_* service/fresh patch extension",
|
|
2022
|
+
})
|
|
2023
|
+
continue
|
|
2024
|
+
source_name = _dump_source_name("ext", ext_name, ext_version, used_generated_names)
|
|
2025
|
+
generated = _register_dumped_source(source_name, out_dir, "extension")
|
|
2026
|
+
generated["runtime_name"] = ext_name
|
|
2027
|
+
generated["runtime_version"] = ext_version
|
|
2028
|
+
generated_sources.append(generated)
|
|
2029
|
+
sources.append(generated)
|
|
2030
|
+
extension_sources.append(generated)
|
|
2031
|
+
dumps.append(ext_dump | {
|
|
2032
|
+
"source_name": source_name,
|
|
2033
|
+
"source_kind": "extension",
|
|
2034
|
+
"runtime_name": ext_name,
|
|
2035
|
+
"runtime_version": ext_version,
|
|
2036
|
+
})
|
|
2037
|
+
if not any(str(ext.get("name") or "") == ext_name for ext in installed_extensions):
|
|
2038
|
+
installed_extensions.append({"name": ext_name, "version": ext_version, "source": "designer_all_extensions"})
|
|
2039
|
+
if ignored_extensions:
|
|
2040
|
+
warnings.append(f"Ignored {len(ignored_extensions)} EF_* service/fresh patch extensions from Designer dump.")
|
|
2041
|
+
extensions_from_designer = bool(all_ext_dump_info.get("extensions_count"))
|
|
2042
|
+
|
|
1976
2043
|
base_match = _best_rag_source_match(config_name, config_version, base_sources, None)
|
|
1977
2044
|
if _rag_match_needs_dump(base_match, config_version):
|
|
1978
2045
|
if not dump_if_missing:
|
|
@@ -1990,8 +2057,8 @@ async def rag_session_autoconfigure(
|
|
|
1990
2057
|
dump_mode,
|
|
1991
2058
|
dump_value,
|
|
1992
2059
|
out_dir,
|
|
1993
|
-
|
|
1994
|
-
|
|
2060
|
+
effective_dump_username,
|
|
2061
|
+
effective_dump_password,
|
|
1995
2062
|
None,
|
|
1996
2063
|
sess.display,
|
|
1997
2064
|
)
|
|
@@ -2013,6 +2080,15 @@ async def rag_session_autoconfigure(
|
|
|
2013
2080
|
extension_source_names: list[str] = []
|
|
2014
2081
|
extension_matches: list[dict[str, Any]] = []
|
|
2015
2082
|
used_extension_sources: set[str] = set()
|
|
2083
|
+
filtered_installed_extensions = []
|
|
2084
|
+
for extension in installed_extensions:
|
|
2085
|
+
ext_name = str(extension.get("name") or "")
|
|
2086
|
+
if _is_ignored_runtime_extension_name(ext_name):
|
|
2087
|
+
warnings.append(f"Ignored EF_* service/fresh patch extension {ext_name!r} from runtime extensions list.")
|
|
2088
|
+
continue
|
|
2089
|
+
filtered_installed_extensions.append(extension)
|
|
2090
|
+
|
|
2091
|
+
installed_extensions = filtered_installed_extensions
|
|
2016
2092
|
for extension in installed_extensions:
|
|
2017
2093
|
ext_name = str(extension.get("name") or "")
|
|
2018
2094
|
ext_version = str(extension.get("version") or "")
|
|
@@ -2030,8 +2106,8 @@ async def rag_session_autoconfigure(
|
|
|
2030
2106
|
dump_mode,
|
|
2031
2107
|
dump_value,
|
|
2032
2108
|
out_dir,
|
|
2033
|
-
|
|
2034
|
-
|
|
2109
|
+
effective_dump_username,
|
|
2110
|
+
effective_dump_password,
|
|
2035
2111
|
ext_name,
|
|
2036
2112
|
sess.display,
|
|
2037
2113
|
)
|
|
@@ -2057,8 +2133,8 @@ async def rag_session_autoconfigure(
|
|
|
2057
2133
|
extension_source_names.append(source_name)
|
|
2058
2134
|
used_extension_sources.add(source_name)
|
|
2059
2135
|
|
|
2060
|
-
effective_snapshot_name = snapshot_name or "+".join([_safe_snapshot_part(config_name), *(_safe_snapshot_part(ext.get("name", "")) for ext in installed_extensions)])
|
|
2061
|
-
if len(effective_snapshot_name) > 180:
|
|
2136
|
+
effective_snapshot_name = snapshot_name or "+".join([_safe_snapshot_part(config_name), *(_safe_snapshot_part(ext.get("name", "")) for ext in installed_extensions[:20])])
|
|
2137
|
+
if len(effective_snapshot_name) > 180 or len(installed_extensions) > 20:
|
|
2062
2138
|
digest = hashlib.sha1(effective_snapshot_name.encode("utf-8")).hexdigest()[:10]
|
|
2063
2139
|
effective_snapshot_name = f"{_safe_snapshot_part(config_name)}+extensions-{digest}"
|
|
2064
2140
|
|
|
@@ -2076,6 +2152,7 @@ async def rag_session_autoconfigure(
|
|
|
2076
2152
|
"warnings": warnings,
|
|
2077
2153
|
"runtime_configuration": {"name": config_name, "version": config_version, "match": _rag_match_public(base_match)},
|
|
2078
2154
|
"runtime_extensions": installed_extensions,
|
|
2155
|
+
"extensions_from_designer": extensions_from_designer,
|
|
2079
2156
|
"extension_matches": extension_matches,
|
|
2080
2157
|
"dumps": dumps,
|
|
2081
2158
|
"generated_sources": generated_sources,
|
|
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
|
|
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
|