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.
Files changed (59) hide show
  1. {answer42-0.3.0 → answer42-0.3.2}/PKG-INFO +1 -1
  2. {answer42-0.3.0 → answer42-0.3.2}/pyproject.toml +1 -1
  3. {answer42-0.3.0 → answer42-0.3.2}/scripts/e2e_stable.py +25 -10
  4. {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
  5. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
  6. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
  7. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/skills/answer42/SKILL.md +5 -3
  8. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +5 -2
  9. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/dump.py +70 -0
  10. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/server.py +84 -7
  11. {answer42-0.3.0 → answer42-0.3.2}/.gitignore +0 -0
  12. {answer42-0.3.0 → answer42-0.3.2}/LICENSE +0 -0
  13. {answer42-0.3.0 → answer42-0.3.2}/README.md +0 -0
  14. {answer42-0.3.0 → answer42-0.3.2}/credentials.example.json +0 -0
  15. {answer42-0.3.0 → answer42-0.3.2}/docs/agent-installation.md +0 -0
  16. {answer42-0.3.0 → answer42-0.3.2}/docs/architecture.md +0 -0
  17. {answer42-0.3.0 → answer42-0.3.2}/docs/assets/answer42-logo.png +0 -0
  18. {answer42-0.3.0 → answer42-0.3.2}/docs/assets/platform42-logo.svg +0 -0
  19. {answer42-0.3.0 → answer42-0.3.2}/docs/installation.md +0 -0
  20. {answer42-0.3.0 → answer42-0.3.2}/scripts/build_cf.py +0 -0
  21. {answer42-0.3.0 → answer42-0.3.2}/scripts/build_pages.py +0 -0
  22. {answer42-0.3.0 → answer42-0.3.2}/scripts/openclaw_mcp_autoreload.py +0 -0
  23. {answer42-0.3.0 → answer42-0.3.2}/scripts/rag_cli.py +0 -0
  24. {answer42-0.3.0 → answer42-0.3.2}/src/cf/ConfigDumpInfo.xml +0 -0
  25. {answer42-0.3.0 → answer42-0.3.2}/src/cf/Configuration.xml +0 -0
  26. {answer42-0.3.0 → answer42-0.3.2}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
  27. {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
  28. {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
  29. {answer42-0.3.0 → answer42-0.3.2}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
  30. {answer42-0.3.0 → answer42-0.3.2}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
  31. {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
  32. {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
  33. {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
  34. {answer42-0.3.0 → answer42-0.3.2}/src/client_cf/ConfigDumpInfo.xml +0 -0
  35. {answer42-0.3.0 → answer42-0.3.2}/src/client_cf/Configuration.xml +0 -0
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/__init__.py +0 -0
  43. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/assets/__init__.py +0 -0
  44. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/bridge.py +0 -0
  45. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/credentials.py +0 -0
  46. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/os_support.py +0 -0
  47. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/platform.py +0 -0
  48. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/protocol.py +0 -0
  49. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/__init__.py +0 -0
  50. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/detect.py +0 -0
  51. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/model.py +0 -0
  52. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/parsers.py +0 -0
  53. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/service.py +0 -0
  54. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/rag/store.py +0 -0
  55. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/recorder.py +0 -0
  56. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/release_helper.py +0 -0
  57. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/runtime.py +0 -0
  58. {answer42-0.3.0 → answer42-0.3.2}/src/mcp_1c/skill_installer.py +0 -0
  59. {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.0
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.0"
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 call_ui_ok(session, "click_button", {"name": "OK", "session_id": sid})
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, "dynamic_list_set_order", {"column_title": "Наименование", "name": "Список", "session_id": sid})
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-dialog.png"))
380
- await call_ui_ok(session, "click_button", {"name": "OK", "session_id": sid})
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, "dynamic_list_open_form_settings", {"session_id": sid})
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, "dynamic_list_available_fields", {"session_id": sid})
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, "dynamic_list_apply_settings", {
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
- Поле.ОжидатьФормированияВыпадающегоСписка(60);
3741
+ Поле.ОжидатьФормированияВыпадающегоСписка(3);
3742
3742
  Представления = Поле.ПолучитьПредставлениеСпискаВыбора();
3743
3743
  Если Представления.Количество() > 0 Тогда
3744
3744
  ИндексВыбора = ИндексРелевантногоВариантаСпискаВыбораMCP(Представления, Значение);
@@ -3777,7 +3777,7 @@
3777
3777
  Если ОткрытВыпадающийСписокПоляMCP(Поле) Тогда
3778
3778
  СписокСформирован = Ложь;
3779
3779
  Попытка
3780
- СписокСформирован = Поле.ОжидатьФормированияВыпадающегоСписка(60);
3780
+ СписокСформирован = Поле.ОжидатьФормированияВыпадающегоСписка(3);
3781
3781
  Исключение
3782
3782
  КонецПопытки;
3783
3783
  Представления = Поле.ПолучитьПредставлениеСпискаВыбора();
@@ -4872,7 +4872,10 @@
4872
4872
 
4873
4873
  &НаКлиенте
4874
4874
  Функция ИмяЭлементаНастроекСпискаMCP(Раздел)
4875
- Возврат "КомпоновщикНастроекПользовательскиеНастройкиЭлемент" + Формат(НомерЭлементаНастроекСпискаMCP(Раздел), "ЧГ=");
4875
+ // Не используем Формат(..., "ЧГ="): для 0 он возвращает пустую строку,
4876
+ // из-за чего filter-раздел искался как "...ЭлементДоступныеПоляТаблица"
4877
+ // вместо реального имени "...Элемент0ДоступныеПоляТаблица".
4878
+ Возврат "КомпоновщикНастроекПользовательскиеНастройкиЭлемент" + Строка(НомерЭлементаНастроекСпискаMCP(Раздел));
4876
4879
  КонецФункции
4877
4880
 
4878
4881
  &НаКлиенте
@@ -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
- - `execute_window_command` — только для явных навигационных ссылок `e1cib/...`. Не передавай туда псевдокоманды/команды формы вроде `ФормаНайти`, `СледующаяСтрока`, `СписокВывестиСписок`; для них нужны отдельные first-class MCP tools.
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
- - Открывай формы/списки через `execute_window_command` с `e1cib/list/...` или `e1cib/app/...`, когда доступно.
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` — низкоуровневая диагностика. Не прокидывай имена команд в `execute_window_command`.
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`) — выгружает конфигурацию и расширения через Designer `DumpConfigToFiles`.
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
- sess.username,
1994
- sess.password,
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
- sess.username,
2034
- sess.password,
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