Switch AI structure HTML5 flow to XML exports
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 15:18:51 +03:00
parent 58afd3932e
commit c9f3c12c3f
3 changed files with 77 additions and 56 deletions
@@ -31,7 +31,6 @@ def render_html5_ai_structure_page(
</aside>
<section class="panel setup-main">
<div class="panel-title">Подготовка структуры</div>
{render_html5_ai_structure_agent_panel(project_id, agent_info=agent_info)}
{render_html5_ai_structure_source_hint()}
{render_html5_ai_structure_form(project_id, saved_credentials=saved_credentials)}
<div data-html5-ai-structure-path-check>{render_html5_ai_structure_path_check(None)}</div>
@@ -100,8 +99,8 @@ def render_html5_ai_structure_source_hint() -> str:
</article>
</div>
<ul class="access-warnings">
<li>Если на входе уже XML-выгрузка, Windows Agent не нужен: сервер сам строит NormalizedProject, SIR и пакет для Codex.</li>
<li>Agent нужен только для бинарных <code>.cf</code>/<code>.cfe</code>, когда сначала надо получить XML-структуру из платформы 1С.</li>
<li>Для этой страницы используется только XML-выгрузка 1С. Сервер сам строит NormalizedProject, SIR и пакет для Codex.</li>
<li>Перед запуском можно проверить структуру выгрузки: главную папку, расширения и первые найденные файлы объектов и модулей.</li>
</ul>
</section>
"""
@@ -121,7 +120,7 @@ def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[s
hx-indicator="[data-ai-structure-progress]"
>
<label class="ai-structure-field ai-structure-field-wide">
<span>Папка с cf/cfe или выгрузкой</span>
<span>Папка с XML-выгрузкой 1С</span>
<input name="input_path" value="\\\\192.168.220.200\\mst\\1c\\MARKA\\CODEX\\CF" />
</label>
<label class="ai-structure-field ai-structure-field-wide">
@@ -155,7 +154,7 @@ def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[s
hx-include="closest form"
hx-target="[data-html5-ai-structure-path-check]"
hx-swap="innerHTML"
>Проверить путь у агента</button>
>Проверить структуру выгрузки</button>
<button class="primary ai-structure-submit" type="submit">Подготовить для ИИ</button>
</form>
<section class="ai-structure-progress" data-ai-structure-progress hidden aria-live="polite">
@@ -170,19 +169,19 @@ def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[s
<div><dt>Осталось примерно</dt><dd data-ai-structure-eta>считаем</dd></div>
<div><dt>Стадия</dt><dd data-ai-structure-stage>Запуск запроса</dd></div>
</dl>
<p class="muted padded">Окно не зависло: сервер копирует сетевые файлы, строит normalized/SIR модель и пишет Codex-пакет. Большие cf/cfe или SMB-папки могут выполняться несколько минут.</p>
<p class="muted padded">Окно не зависло: сервер копирует сетевые файлы, строит normalized/SIR модель и пишет Codex-пакет. Большие XML-выгрузки и SMB-папки могут выполняться несколько минут.</p>
</section>
"""
def render_html5_ai_structure_path_check(result: dict | None) -> str:
if result is None:
return '<p class="muted padded">Сначала можно проверить, видит ли Windows Agent входной путь и файл .cf/.cfe.</p>'
return '<p class="muted padded">Сначала можно проверить, что сервер видит XML-выгрузку, папку <code>Конфигурация</code>, расширения и первые найденные файлы.</p>'
status = str(result.get("status") or "info")
title_map = {
"ok": "Путь доступен",
"error": "Путь недоступен",
"info": "Проверка пути",
"ok": "Структура найдена",
"error": "Структура недоступна",
"info": "Проверка структуры",
}
title = title_map.get(status, "Проверка пути")
message = str(result.get("message") or "")
@@ -269,7 +268,7 @@ def render_html5_ai_structure_error(message: str) -> str:
<ul class="access-warnings">
<li>{escape(message)}</li>
</ul>
<p class="muted padded">Проверьте, что входная и выходная папки доступны именно серверу SFERA/API. Если файлы лежат на рабочем ПК, сначала положите их в общую папку или выполните экспорт через агент.</p>
<p class="muted padded">Проверьте, что входная и выходная папки доступны именно серверу SFERA/API. Для этого сценария ожидается XML-выгрузка 1С с папкой <code>Конфигурация</code> и, при необходимости, отдельными папками расширений.</p>
</section>
"""
@@ -347,23 +346,6 @@ def _artifact_text(value: object) -> str:
return mapping.get(str(value or ""), str(value or ""))
def _agent_status_title(status: str) -> str:
mapping = {
"online": "агент онлайн",
"offline": "агент офлайн",
"not_configured": "агент не настроен",
}
return mapping.get(status, status)
def _agent_status_advice(status: str, agent_id: str) -> str:
if status == "online":
return f"Для бинарных .cf/.cfe будет использован Windows Agent {agent_id}. Перед запуском проверьте, что входной UNC-путь лежит внутри одного из доступных сетевых корней агента."
if status == "offline":
return f"Windows Agent {agent_id} выбран, но сейчас не отвечает. Обновите или запустите агент, затем повторите."
return "Для прямого разбора .cf/.cfe нужен выбранный Windows Agent. Укажите его в настройках проекта."
def _layout_kind_text(value: str) -> str:
mapping = {
"configuration_with_extensions": "Конфигурация + отдельные папки расширений",
@@ -53,7 +53,7 @@ async def html5_ai_structure_run(
input_path = form_value(form, "input_path")
output_path = form_value(form, "output_path")
if not input_path:
return render_html5_ai_structure_error("Заполните входную папку с .cf/.cfe или выгрузкой 1С.")
return render_html5_ai_structure_error("Заполните входную папку с XML-выгрузкой 1С.")
if not output_path:
return render_html5_ai_structure_error("Заполните папку результата.")
saved = load_credentials(project_id) if load_credentials else None
@@ -179,8 +179,13 @@ async def html5_ai_structure_check_path(
password = form_value(form, "smb_password") or (saved or {}).get("password", "")
domain = form_value(form, "smb_domain") or (saved or {}).get("domain", "")
if _detect_binary_input(input_path):
result = await check_path(project_id=project_id, input_path=input_path)
return render_html5_ai_structure_path_check(result)
return render_html5_ai_structure_path_check(
{
"status": "error",
"message": "Для этой страницы поддерживается только XML-выгрузка 1С. Бинарные .cf/.cfe здесь не используются.",
"details": ["Подготовьте папку с `Конфигурация` и, при необходимости, с отдельными папками расширений."],
}
)
try:
result = _inspect_ai_structure_input(
raw_input_path=input_path,
@@ -431,6 +436,18 @@ def _inspect_ai_structure_input(
xml_count = sum(1 for path in files if path.suffix.casefold() == ".xml")
mdo_count = sum(1 for path in files if path.suffix.casefold() == ".mdo")
bsl_count = sum(1 for path in files if path.suffix.casefold() == ".bsl")
object_files = _preview_relative_paths(
local_root,
sorted(
[path for path in files if path.suffix.casefold() in {".mdo", ".xml"}],
key=lambda path: (
0 if any(part.casefold() in {"configuration", "конфигурация"} for part in path.parts) else 1,
str(path).casefold(),
),
),
limit=5,
)
module_files = _preview_relative_paths(local_root, [path for path in files if path.suffix.casefold() == ".bsl"], limit=5)
details = [
f"Тип раскладки: {_layout_kind_text(layout['kind'])}",
f"Главная папка: {layout['main_configuration_root']}",
@@ -439,6 +456,10 @@ def _inspect_ai_structure_input(
f"Файлов MDO: {mdo_count}",
f"Файлов BSL: {bsl_count}",
]
if object_files:
details.append(f"Первые файлы объектов: {', '.join(object_files)}")
if module_files:
details.append(f"Первые файлы модулей: {', '.join(module_files)}")
if copied_from_unc:
details.append("Проверка выполнена сервером после чтения UNC-пути по SMB.")
if parseable:
@@ -493,3 +514,13 @@ def _layout_kind_text(value: str) -> str:
"file": "Отдельный входной файл",
}
return mapping.get(value, value)
def _preview_relative_paths(root: Path, files: list[Path], *, limit: int) -> list[str]:
preview: list[str] = []
for path in files[:limit]:
if root.is_file():
preview.append(path.name)
else:
preview.append(path.relative_to(root).as_posix())
return preview
+33 -25
View File
@@ -1770,6 +1770,7 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
"Ожидаемый формат выгрузки 1С",
"Конфигурация",
"Без лишнего сырья",
"Для этой страницы используется только XML-выгрузка 1С",
"smb_username",
"smb_password",
"data-ai-structure-progress",
@@ -2240,51 +2241,36 @@ def test_html5_ai_structure_reports_offline_agent_with_last_seen(tmp_path: Path)
assert "Последний heartbeat" in queued.text
def test_html5_ai_structure_page_shows_agent_status_panel():
def test_html5_ai_structure_page_shows_xml_only_hint():
client = TestClient(app)
project_id = f"ai-page-{uuid4()}"
agent_id = f"win-agent-{uuid4()}"
settings = client.post(
f"/projects/{project_id}/settings",
json={"name": "AI Page", "structure_source": "CF_FILE", "agent": {"cf_agent_id": agent_id}},
json={"name": "AI Page", "structure_source": "XML_DUMP"},
)
assert settings.status_code == 200
heartbeat = client.post(
"/agent/heartbeat",
json={
"agent_id": agent_id,
"host": "test-host",
"version": "0.2.31",
"network_roots": [r"\\192.168.220.220\mst"],
},
)
assert heartbeat.status_code == 200
page = client.get(f"/html5/projects/{project_id}/ai-structure")
assert page.status_code == 200
assert "Агент для CF/CFE" in page.text
assert "агент онлайн" in page.text
assert agent_id in page.text
assert "test-host" in page.text
assert "0.2.31" in page.text
assert r"\\192.168.220.220\mst" in page.text
assert "Для этой страницы используется только XML-выгрузка 1С" in page.text
assert "Агент для CF/CFE" not in page.text
def test_html5_ai_structure_page_shows_missing_agent_hint():
def test_html5_ai_structure_page_shows_xml_layout_hint():
client = TestClient(app)
project_id = f"ai-page-missing-{uuid4()}"
settings = client.post(
f"/projects/{project_id}/settings",
json={"name": "AI Page Missing", "structure_source": "CF_FILE"},
json={"name": "AI Page Missing", "structure_source": "XML_DUMP"},
)
assert settings.status_code == 200
page = client.get(f"/html5/projects/{project_id}/ai-structure")
assert page.status_code == 200
assert "агент не настроен" in page.text
assert "Укажите его в настройках проекта" in page.text
assert "Папка с XML-выгрузкой 1С" in page.text
assert "Проверить структуру выгрузки" in page.text
def test_html5_ai_structure_check_path_button_present():
@@ -2292,12 +2278,12 @@ def test_html5_ai_structure_check_path_button_present():
project_id = f"ai-path-button-{uuid4()}"
settings = client.post(
f"/projects/{project_id}/settings",
json={"name": "AI Path Button", "structure_source": "CF_FILE"},
json={"name": "AI Path Button", "structure_source": "XML_DUMP"},
)
assert settings.status_code == 200
page = client.get(f"/html5/projects/{project_id}/ai-structure")
assert page.status_code == 200
assert "Проверить путь у агента" in page.text
assert "Проверить структуру выгрузки" in page.text
assert "/ai-structure/check-path" in page.text
@@ -2389,6 +2375,28 @@ def test_html5_ai_structure_check_path_reports_xml_export_layout(tmp_path: Path)
assert "Конфигурация + отдельные папки расширений" in checked.text
assert "Главная папка: Конфигурация" in checked.text
assert "Папки расширений: CRM" in checked.text
assert "Первые файлы объектов: Конфигурация/metadata.xml, CRM/РасширениеCRM.mdo" in checked.text
def test_html5_ai_structure_check_path_rejects_binary_input_for_xml_flow(tmp_path: Path):
cf_input = tmp_path / "demo.cf"
cf_input.write_bytes(b"binary-cf")
client = TestClient(app)
project_id = f"ai-xml-binary-{uuid4()}"
settings = client.post(
f"/projects/{project_id}/settings",
json={"name": "AI XML Only", "structure_source": "XML_DUMP"},
)
assert settings.status_code == 200
checked = client.post(
f"/html5/projects/{project_id}/ai-structure/check-path",
data={"input_path": str(cf_input)},
)
assert checked.status_code == 200
assert "поддерживается только XML-выгрузка 1С" in checked.text
assert "Конфигурация" in checked.text
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):