answer42 0.4.89__tar.gz → 0.4.91__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 (64) hide show
  1. {answer42-0.4.89 → answer42-0.4.91}/PKG-INFO +1 -1
  2. {answer42-0.4.89 → answer42-0.4.91}/pyproject.toml +1 -1
  3. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/server.py +190 -66
  4. {answer42-0.4.89 → answer42-0.4.91}/.gitignore +0 -0
  5. {answer42-0.4.89 → answer42-0.4.91}/LICENSE +0 -0
  6. {answer42-0.4.89 → answer42-0.4.91}/README.md +0 -0
  7. {answer42-0.4.89 → answer42-0.4.91}/credentials.example.json +0 -0
  8. {answer42-0.4.89 → answer42-0.4.91}/docs/agent-installation.md +0 -0
  9. {answer42-0.4.89 → answer42-0.4.91}/docs/architecture.md +0 -0
  10. {answer42-0.4.89 → answer42-0.4.91}/docs/assets/answer42-logo.png +0 -0
  11. {answer42-0.4.89 → answer42-0.4.91}/docs/assets/platform42-logo.svg +0 -0
  12. {answer42-0.4.89 → answer42-0.4.91}/docs/installation.md +0 -0
  13. {answer42-0.4.89 → answer42-0.4.91}/scripts/build_cf.py +0 -0
  14. {answer42-0.4.89 → answer42-0.4.91}/scripts/build_pages.py +0 -0
  15. {answer42-0.4.89 → answer42-0.4.91}/scripts/e2e_http_smoke.py +0 -0
  16. {answer42-0.4.89 → answer42-0.4.91}/scripts/e2e_stable.py +0 -0
  17. {answer42-0.4.89 → answer42-0.4.91}/scripts/openclaw_mcp_autoreload.py +0 -0
  18. {answer42-0.4.89 → answer42-0.4.91}/scripts/rag_cli.py +0 -0
  19. {answer42-0.4.89 → answer42-0.4.91}/src/cf/ConfigDumpInfo.xml +0 -0
  20. {answer42-0.4.89 → answer42-0.4.91}/src/cf/Configuration.xml +0 -0
  21. {answer42-0.4.89 → answer42-0.4.91}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
  22. {answer42-0.4.89 → answer42-0.4.91}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl" +0 -0
  23. {answer42-0.4.89 → answer42-0.4.91}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
  24. {answer42-0.4.89 → answer42-0.4.91}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
  25. {answer42-0.4.89 → answer42-0.4.91}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
  26. {answer42-0.4.89 → answer42-0.4.91}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
  27. {answer42-0.4.89 → answer42-0.4.91}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  28. {answer42-0.4.89 → answer42-0.4.91}/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
  29. {answer42-0.4.89 → answer42-0.4.91}/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
  30. {answer42-0.4.89 → answer42-0.4.91}/src/client_cf/ConfigDumpInfo.xml +0 -0
  31. {answer42-0.4.89 → answer42-0.4.91}/src/client_cf/Configuration.xml +0 -0
  32. {answer42-0.4.89 → answer42-0.4.91}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  33. {answer42-0.4.89 → answer42-0.4.91}/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
  34. {answer42-0.4.89 → answer42-0.4.91}/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
  35. {answer42-0.4.89 → answer42-0.4.91}/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
  36. {answer42-0.4.89 → answer42-0.4.91}/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
  37. {answer42-0.4.89 → answer42-0.4.91}/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
  38. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/__init__.py +0 -0
  39. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
  40. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
  41. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/assets/__init__.py +0 -0
  42. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/assets/skills/answer42/SKILL.md +0 -0
  43. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
  44. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/assets/skills/answer42-universal-report/SKILL.md +0 -0
  45. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/auth.py +0 -0
  46. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/bridge.py +0 -0
  47. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/credentials.py +0 -0
  48. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/os_support.py +0 -0
  49. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/platform.py +0 -0
  50. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/protocol.py +0 -0
  51. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/__init__.py +0 -0
  52. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/detect.py +0 -0
  53. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/dump.py +0 -0
  54. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/model.py +0 -0
  55. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/parsers.py +0 -0
  56. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/service.py +0 -0
  57. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/store.py +0 -0
  58. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/summary_backend.py +0 -0
  59. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/rag/summary_vector.py +0 -0
  60. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/recorder.py +0 -0
  61. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/release_helper.py +0 -0
  62. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/runtime.py +0 -0
  63. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/skill_installer.py +0 -0
  64. {answer42-0.4.89 → answer42-0.4.91}/src/mcp_1c/window_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: answer42
3
- Version: 0.4.89
3
+ Version: 0.4.91
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.4.89"
3
+ version = "0.4.91"
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"
@@ -16,6 +16,7 @@ import sys
16
16
  import time
17
17
  import urllib.request
18
18
  import uuid
19
+ from collections.abc import Sequence
19
20
  from contextlib import contextmanager, suppress
20
21
  from dataclasses import dataclass, field
21
22
  from datetime import UTC, datetime
@@ -8118,6 +8119,62 @@ async def _add_universal_report_filter(
8118
8119
  return True
8119
8120
 
8120
8121
 
8122
+ def _active_window_matches_self_opened_form(
8123
+ active_window: Any,
8124
+ *,
8125
+ title_markers: Sequence[str] = (),
8126
+ form_names: Sequence[str] = (),
8127
+ ) -> bool:
8128
+ if not isinstance(active_window, dict):
8129
+ return False
8130
+ normalized_form_names = {str(name or "").casefold() for name in form_names if str(name or "").strip()}
8131
+ form_name = str(active_window.get("form_name") or "").casefold()
8132
+ if form_name and form_name in normalized_form_names:
8133
+ return True
8134
+ diagnostics = active_window.get("client_diagnostics") if isinstance(active_window.get("client_diagnostics"), dict) else {}
8135
+ titles = [
8136
+ active_window.get("title"),
8137
+ active_window.get("window_title"),
8138
+ diagnostics.get("active_window_title"),
8139
+ diagnostics.get("modal_title"),
8140
+ ]
8141
+ normalized_markers = [_normalize_match_text(marker) for marker in title_markers if str(marker or "").strip()]
8142
+ return any(
8143
+ marker and marker in _normalize_match_text(str(title or ""))
8144
+ for marker in normalized_markers
8145
+ for title in titles
8146
+ )
8147
+
8148
+
8149
+ async def _close_self_opened_form(
8150
+ session_id: str,
8151
+ warnings: list[str],
8152
+ *,
8153
+ title_markers: Sequence[str] = (),
8154
+ form_names: Sequence[str] = (),
8155
+ description: str = "form",
8156
+ ) -> bool:
8157
+ """Close a form opened by a high-level helper without touching unrelated user UI."""
8158
+ try:
8159
+ active = await _bridge_call_recorded(session_id, "active_window", {})
8160
+ except Exception as exc:
8161
+ warnings.append(f"Self-opened {description} cleanup active-window probe failed: {exc}")
8162
+ return False
8163
+ if not _active_window_matches_self_opened_form(
8164
+ active,
8165
+ title_markers=title_markers,
8166
+ form_names=form_names,
8167
+ ):
8168
+ return False
8169
+ try:
8170
+ await _bridge_call_recorded(session_id, "close_form", {})
8171
+ warnings.append(f"Self-opened {description} form was closed after tool completion")
8172
+ return True
8173
+ except Exception as exc:
8174
+ warnings.append(f"Self-opened {description} form cleanup failed: {exc}")
8175
+ return False
8176
+
8177
+
8121
8178
  async def _close_message_box_from_command_diagnostics(
8122
8179
  session_id: str,
8123
8180
  command_result: Any,
@@ -8411,8 +8468,11 @@ async def _metadata_rows_from_universal_report(
8411
8468
  """
8412
8469
  warnings: list[str] = []
8413
8470
  navigation_link = "e1cib/app/Отчет.УниверсальныйОтчет"
8471
+ cleanup_enabled = False
8472
+ cleanup_allowed = True
8414
8473
  try:
8415
8474
  open_result = await _bridge_call_recorded(session_id, "open_navigation_link", {"navigation_link": navigation_link})
8475
+ cleanup_enabled = True
8416
8476
  await _close_message_box_from_command_diagnostics(session_id, open_result, warnings)
8417
8477
  source_configured = await _ensure_universal_report_metadata_source(
8418
8478
  session_id,
@@ -8430,6 +8490,7 @@ async def _metadata_rows_from_universal_report(
8430
8490
  form_warnings = await _form_universal_report_metadata_source(session_id)
8431
8491
  warnings.extend(form_warnings)
8432
8492
  if any("form action is still running or blocked" in warning for warning in form_warnings):
8493
+ cleanup_allowed = False
8433
8494
  return [], warnings
8434
8495
  ready, readiness_warnings, readiness = await _wait_tabular_document_ready(
8435
8496
  session_id,
@@ -8469,6 +8530,7 @@ async def _metadata_rows_from_universal_report(
8469
8530
  "Universal report MXL save is still running or blocked the interface;"
8470
8531
  f" save_result={_truncate_value(selected_result)}"
8471
8532
  )
8533
+ cleanup_allowed = False
8472
8534
  return [], [*warnings, *read_warnings]
8473
8535
  save_diagnostics = selected_result
8474
8536
  selected = selected_result.get("selected") if isinstance(selected_result, dict) else None
@@ -8491,7 +8553,7 @@ async def _metadata_rows_from_universal_report(
8491
8553
  "Universal report MXL became parseable"
8492
8554
  f"; rows={readiness.get('rows')}; columns={readiness.get('columns')}; path={last_saved_path}"
8493
8555
  )
8494
- return rows[:max_rows], warnings
8556
+ return rows if max_rows <= 0 else rows[:max_rows], warnings
8495
8557
 
8496
8558
  report_path = _runtime_output_file("metadata_objects", session_id, ".xlsx")
8497
8559
  try:
@@ -8512,6 +8574,7 @@ async def _metadata_rows_from_universal_report(
8512
8574
  "Universal report XLSX save is still running or blocked the interface;"
8513
8575
  f" save_result={_truncate_value(selected_result)}"
8514
8576
  )
8577
+ cleanup_allowed = False
8515
8578
  return [], [*warnings, *read_warnings]
8516
8579
  save_diagnostics = selected_result
8517
8580
  selected = selected_result.get("selected") if isinstance(selected_result, dict) else None
@@ -8541,9 +8604,20 @@ async def _metadata_rows_from_universal_report(
8541
8604
  "Universal report XLSX became parseable"
8542
8605
  f"; rows={readiness.get('rows')}; columns={readiness.get('columns')}; path={last_saved_path}"
8543
8606
  )
8544
- return rows[:max_rows], warnings
8607
+ return rows if max_rows <= 0 else rows[:max_rows], warnings
8545
8608
  except Exception as exc:
8546
8609
  return [], [f"Universal report metadata read failed: {exc}"]
8610
+ finally:
8611
+ current_task = asyncio.current_task()
8612
+ is_cancelling = bool(current_task and current_task.cancelling())
8613
+ if cleanup_enabled and cleanup_allowed and not is_cancelling:
8614
+ await _close_self_opened_form(
8615
+ session_id,
8616
+ warnings,
8617
+ title_markers=("Универсальный отчет", "Идентификаторы объектов метаданных"),
8618
+ form_names=("ОбщаяФорма.ФормаОтчета",),
8619
+ description="Universal report metadata source",
8620
+ )
8547
8621
 
8548
8622
 
8549
8623
  async def _metadata_catalog_rows_from_output(session_id: str, max_rows: int) -> tuple[list[dict[str, str]], list[str]]:
@@ -8908,17 +8982,21 @@ async def _metadata_rows_from_target_catalog_list(
8908
8982
  ) -> tuple[list[dict[str, str]], list[str]]:
8909
8983
  warnings: list[str] = []
8910
8984
  navigation_link = "e1cib/list/Справочник.ИдентификаторыОбъектовМетаданных"
8911
- open_result = await _dangerous_bridge_call_recorded(
8912
- session_id,
8913
- "open_navigation_link",
8914
- {"navigation_link": navigation_link},
8915
- timeout=15,
8916
- danger="metadata_target_catalog_list_open_may_block_thin_client",
8917
- )
8918
- if isinstance(open_result, dict) and open_result.get("started_long_action"):
8919
- warnings.append("Target metadata catalog list open watchdog timeout")
8920
- return [], warnings
8985
+ cleanup_enabled = False
8986
+ cleanup_allowed = True
8921
8987
  try:
8988
+ open_result = await _dangerous_bridge_call_recorded(
8989
+ session_id,
8990
+ "open_navigation_link",
8991
+ {"navigation_link": navigation_link},
8992
+ timeout=15,
8993
+ danger="metadata_target_catalog_list_open_may_block_thin_client",
8994
+ )
8995
+ cleanup_enabled = True
8996
+ if isinstance(open_result, dict) and open_result.get("started_long_action"):
8997
+ cleanup_allowed = False
8998
+ warnings.append("Target metadata catalog list open watchdog timeout")
8999
+ return [], warnings
8922
9000
  result = await _bridge_call_recorded(
8923
9001
  session_id,
8924
9002
  "table_rows",
@@ -8927,6 +9005,17 @@ async def _metadata_rows_from_target_catalog_list(
8927
9005
  )
8928
9006
  except Exception as exc:
8929
9007
  return [], [f"Target metadata catalog list read failed: {exc}"]
9008
+ finally:
9009
+ current_task = asyncio.current_task()
9010
+ is_cancelling = bool(current_task and current_task.cancelling())
9011
+ if cleanup_enabled and cleanup_allowed and not is_cancelling:
9012
+ await _close_self_opened_form(
9013
+ session_id,
9014
+ warnings,
9015
+ title_markers=("Идентификаторы объектов метаданных",),
9016
+ form_names=("Справочник.ИдентификаторыОбъектовМетаданных.ФормаСписка",),
9017
+ description="metadata identifiers catalog list",
9018
+ )
8930
9019
  raw_rows = result.get("rows", []) if isinstance(result, dict) else []
8931
9020
  parsed_rows: list[dict[str, str]] = []
8932
9021
  for row in raw_rows:
@@ -9006,7 +9095,7 @@ async def _load_metadata_objects_from_catalog(session_id: str, limit: int, query
9006
9095
  del query, max_results
9007
9096
  navigation_link = "e1cib/app/Отчет.УниверсальныйОтчет"
9008
9097
  requested_limit = max(1, int(limit or 1))
9009
- parsed_rows, warnings = await _metadata_rows_from_universal_report(session_id, requested_limit)
9098
+ parsed_rows, warnings = await _metadata_rows_from_universal_report(session_id, 0)
9010
9099
  if not parsed_rows:
9011
9100
  warnings.append("Universal report returned no metadata rows for cache build")
9012
9101
  data = _metadata_catalog_data_from_rows(
@@ -9076,7 +9165,7 @@ async def _metadata_objects_from_catalog_impl(
9076
9165
  @mcp.tool()
9077
9166
  async def metadata_objects_from_catalog(
9078
9167
  query: Annotated[str, "Substring to search in presentation, full name, type, name or synonym"] = "",
9079
- limit: Annotated[int, "Maximum rows to parse from the saved Universal report when cache is refreshed"] = 10000,
9168
+ limit: Annotated[int, "Deprecated compatibility parameter; cache refresh always reads the full saved Universal report. Use max_results to limit returned matches"] = 10000,
9080
9169
  max_results: Annotated[int, "Maximum matched objects to return"] = 200,
9081
9170
  refresh_cache: Annotated[bool, "Force rebuilding the Universal report snapshot instead of using the session cache"] = False,
9082
9171
  include_groups: Annotated[bool, "Include hierarchy group rows in results"] = False,
@@ -9224,64 +9313,78 @@ async def configuration_name_version_from_subsystems(
9224
9313
  `Это основная конфигурация`, are visible even when they are not displayed
9225
9314
  in the default form table.
9226
9315
  """
9227
- navigation_link = "e1cib/list/РегистрСведений.ВерсииПодсистем"
9228
- await _bridge_call_recorded(session_id, "open_navigation_link", {"navigation_link": navigation_link})
9229
-
9230
9316
  warnings: list[str] = []
9231
9317
  main: dict[str, str] = {}
9232
9318
  cells: list[list[str]] = []
9233
9319
  source_method = "sorted_edge_row"
9234
9320
  current_row: dict[str, str] = {}
9235
9321
  sort_attempts: list[dict[str, Any]] = []
9322
+ navigation_link = "e1cib/list/РегистрСведений.ВерсииПодсистем"
9323
+ cleanup_enabled = False
9236
9324
 
9237
- sort_result = await _bridge_call_recorded(session_id, "dynamic_list_sort_by_column", {
9238
- "name": "Список",
9239
- "title": "",
9240
- "column_title": "Это основная конфигурация",
9241
- })
9242
- ordered = bool(sort_result.get("ordered")) if isinstance(sort_result, dict) else False
9243
- for direction, label in (("first", "first_after_sort"), ("last", "last_after_sort")):
9244
- move_result = await _bridge_call_recorded(session_id, "table_move_row", {
9325
+ try:
9326
+ await _bridge_call_recorded(session_id, "open_navigation_link", {"navigation_link": navigation_link})
9327
+ cleanup_enabled = True
9328
+
9329
+ sort_result = await _bridge_call_recorded(session_id, "dynamic_list_sort_by_column", {
9245
9330
  "name": "Список",
9246
9331
  "title": "",
9247
- "direction": direction,
9248
- "steps": 1,
9249
- })
9250
- row_result = await _bridge_call_recorded(session_id, "table_current_row", {"name": "Список", "title": ""})
9251
- current_row = _table_row_pairs(row_result)
9252
- main = _configuration_from_sorted_first_row(current_row)
9253
- sort_attempts.append({
9254
- "attempt": len(sort_attempts) + 1,
9255
- "ordered": ordered,
9256
- "move_direction": direction,
9257
- "move_result": move_result if isinstance(move_result, dict) else {},
9258
- "read_method": label,
9259
- "row_is_main": bool(main),
9260
- "row_main_marker": _row_value(current_row, "Это основная конфигурация", "ЭтоОсновнаяКонфигурация"),
9332
+ "column_title": "Это основная конфигурация",
9261
9333
  })
9262
- if main:
9263
- break
9334
+ ordered = bool(sort_result.get("ordered")) if isinstance(sort_result, dict) else False
9335
+ for direction, label in (("first", "first_after_sort"), ("last", "last_after_sort")):
9336
+ move_result = await _bridge_call_recorded(session_id, "table_move_row", {
9337
+ "name": "Список",
9338
+ "title": "",
9339
+ "direction": direction,
9340
+ "steps": 1,
9341
+ })
9342
+ row_result = await _bridge_call_recorded(session_id, "table_current_row", {"name": "Список", "title": ""})
9343
+ current_row = _table_row_pairs(row_result)
9344
+ main = _configuration_from_sorted_first_row(current_row)
9345
+ sort_attempts.append({
9346
+ "attempt": len(sort_attempts) + 1,
9347
+ "ordered": ordered,
9348
+ "move_direction": direction,
9349
+ "move_result": move_result if isinstance(move_result, dict) else {},
9350
+ "read_method": label,
9351
+ "row_is_main": bool(main),
9352
+ "row_main_marker": _row_value(current_row, "Это основная конфигурация", "ЭтоОсновнаяКонфигурация"),
9353
+ })
9354
+ if main:
9355
+ break
9264
9356
 
9265
- if not main:
9266
- warnings.append("Sorted edge list rows did not expose the main configuration; falling back to MXL list output")
9267
- source_method = "list_output_mxl"
9268
- output_result = await _bridge_call_recorded(session_id, "dynamic_list_output", {
9269
- "button": "",
9270
- "wait_seconds": 3,
9271
- "include_text": False,
9272
- })
9273
- tabular_docs = output_result.get("tabular_documents", {}) if isinstance(output_result, dict) else {}
9274
- items = tabular_docs.get("items", []) if isinstance(tabular_docs, dict) else []
9275
- if items:
9276
- first_item = items[0]
9277
- doc_name = first_item.get("name", "") if isinstance(first_item, dict) else ""
9278
- cells = await _save_output_tabular_document_to_mxl(
9357
+ if not main:
9358
+ warnings.append("Sorted edge list rows did not expose the main configuration; falling back to MXL list output")
9359
+ source_method = "list_output_mxl"
9360
+ output_result = await _bridge_call_recorded(session_id, "dynamic_list_output", {
9361
+ "button": "",
9362
+ "wait_seconds": 3,
9363
+ "include_text": False,
9364
+ })
9365
+ tabular_docs = output_result.get("tabular_documents", {}) if isinstance(output_result, dict) else {}
9366
+ items = tabular_docs.get("items", []) if isinstance(tabular_docs, dict) else []
9367
+ if items:
9368
+ first_item = items[0]
9369
+ doc_name = first_item.get("name", "") if isinstance(first_item, dict) else ""
9370
+ cells = await _save_output_tabular_document_to_mxl(
9371
+ session_id,
9372
+ doc_name,
9373
+ "configuration_name_version_from_subsystems",
9374
+ warnings,
9375
+ )
9376
+ main = _find_main_configuration_in_tabular_cells(cells)
9377
+ finally:
9378
+ current_task = asyncio.current_task()
9379
+ is_cancelling = bool(current_task and current_task.cancelling())
9380
+ if cleanup_enabled and not is_cancelling:
9381
+ await _close_self_opened_form(
9279
9382
  session_id,
9280
- doc_name,
9281
- "configuration_name_version_from_subsystems",
9282
9383
  warnings,
9384
+ title_markers=("Версии подсистем",),
9385
+ form_names=("РегистрСведений.ВерсииПодсистем.ФормаСписка",),
9386
+ description="subsystem versions list",
9283
9387
  )
9284
- main = _find_main_configuration_in_tabular_cells(cells)
9285
9388
 
9286
9389
  return {
9287
9390
  "found": bool(main),
@@ -9376,15 +9479,35 @@ async def installed_extensions_from_catalog(
9376
9479
  returned as extensions.
9377
9480
  """
9378
9481
  navigation_link = "e1cib/list/Справочник.ВерсииРасширений"
9379
- await _bridge_call_recorded(session_id, "open_navigation_link", {"navigation_link": navigation_link})
9380
- rows_result = await _bridge_call_recorded(session_id, "table_rows", {"name": "Список", "title": "", "limit": 500})
9381
- rows = rows_result.get("rows", []) if isinstance(rows_result, dict) else []
9482
+ warnings: list[str] = []
9483
+ cleanup_enabled = False
9484
+ parsed_rows: list[dict[str, str]] = []
9485
+ actual_rows: list[dict[str, str]] = []
9486
+ selected_row: dict[str, str] = {}
9487
+ extensions: list[dict[str, str]] = []
9382
9488
 
9383
- parsed_rows = [_table_row_pairs(row) for row in rows]
9384
- actual_rows = [row for row in parsed_rows if _is_not_deleted_catalog_row(row)]
9385
- selected_row = max(actual_rows, key=_extension_catalog_row_sort_key) if actual_rows else {}
9386
- description = _row_value(selected_row, "Описание метаданных", "ОписаниеМетаданных")
9387
- extensions = _parse_extension_metadata_description(description)
9489
+ try:
9490
+ await _bridge_call_recorded(session_id, "open_navigation_link", {"navigation_link": navigation_link})
9491
+ cleanup_enabled = True
9492
+ rows_result = await _bridge_call_recorded(session_id, "table_rows", {"name": "Список", "title": "", "limit": 500})
9493
+ rows = rows_result.get("rows", []) if isinstance(rows_result, dict) else []
9494
+
9495
+ parsed_rows = [_table_row_pairs(row) for row in rows]
9496
+ actual_rows = [row for row in parsed_rows if _is_not_deleted_catalog_row(row)]
9497
+ selected_row = max(actual_rows, key=_extension_catalog_row_sort_key) if actual_rows else {}
9498
+ description = _row_value(selected_row, "Описание метаданных", "ОписаниеМетаданных")
9499
+ extensions = _parse_extension_metadata_description(description)
9500
+ finally:
9501
+ current_task = asyncio.current_task()
9502
+ is_cancelling = bool(current_task and current_task.cancelling())
9503
+ if cleanup_enabled and not is_cancelling:
9504
+ await _close_self_opened_form(
9505
+ session_id,
9506
+ warnings,
9507
+ title_markers=("Версии расширений",),
9508
+ form_names=("Справочник.ВерсииРасширений.ФормаСписка",),
9509
+ description="extension versions list",
9510
+ )
9388
9511
 
9389
9512
  return {
9390
9513
  "found": bool(selected_row),
@@ -9397,6 +9520,7 @@ async def installed_extensions_from_catalog(
9397
9520
  "non_deleted_rows_count": len(actual_rows),
9398
9521
  "navigation_link": navigation_link,
9399
9522
  "source": "Справочник.ВерсииРасширений",
9523
+ "warnings": warnings,
9400
9524
  }
9401
9525
 
9402
9526
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes