Switch AI structure HTML5 flow to XML exports
This commit is contained in:
@@ -31,7 +31,6 @@ def render_html5_ai_structure_page(
|
|||||||
</aside>
|
</aside>
|
||||||
<section class="panel setup-main">
|
<section class="panel setup-main">
|
||||||
<div class="panel-title">Подготовка структуры</div>
|
<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_source_hint()}
|
||||||
{render_html5_ai_structure_form(project_id, saved_credentials=saved_credentials)}
|
{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>
|
<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>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<ul class="access-warnings">
|
<ul class="access-warnings">
|
||||||
<li>Если на входе уже XML-выгрузка, Windows Agent не нужен: сервер сам строит NormalizedProject, SIR и пакет для Codex.</li>
|
<li>Для этой страницы используется только XML-выгрузка 1С. Сервер сам строит NormalizedProject, SIR и пакет для Codex.</li>
|
||||||
<li>Agent нужен только для бинарных <code>.cf</code>/<code>.cfe</code>, когда сначала надо получить XML-структуру из платформы 1С.</li>
|
<li>Перед запуском можно проверить структуру выгрузки: главную папку, расширения и первые найденные файлы объектов и модулей.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
@@ -121,7 +120,7 @@ def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[s
|
|||||||
hx-indicator="[data-ai-structure-progress]"
|
hx-indicator="[data-ai-structure-progress]"
|
||||||
>
|
>
|
||||||
<label class="ai-structure-field ai-structure-field-wide">
|
<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" />
|
<input name="input_path" value="\\\\192.168.220.200\\mst\\1c\\MARKA\\CODEX\\CF" />
|
||||||
</label>
|
</label>
|
||||||
<label class="ai-structure-field ai-structure-field-wide">
|
<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-include="closest form"
|
||||||
hx-target="[data-html5-ai-structure-path-check]"
|
hx-target="[data-html5-ai-structure-path-check]"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
>Проверить путь у агента</button>
|
>Проверить структуру выгрузки</button>
|
||||||
<button class="primary ai-structure-submit" type="submit">Подготовить для ИИ</button>
|
<button class="primary ai-structure-submit" type="submit">Подготовить для ИИ</button>
|
||||||
</form>
|
</form>
|
||||||
<section class="ai-structure-progress" data-ai-structure-progress hidden aria-live="polite">
|
<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-eta>считаем</dd></div>
|
||||||
<div><dt>Стадия</dt><dd data-ai-structure-stage>Запуск запроса</dd></div>
|
<div><dt>Стадия</dt><dd data-ai-structure-stage>Запуск запроса</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<p class="muted padded">Окно не зависло: сервер копирует сетевые файлы, строит normalized/SIR модель и пишет Codex-пакет. Большие cf/cfe или SMB-папки могут выполняться несколько минут.</p>
|
<p class="muted padded">Окно не зависло: сервер копирует сетевые файлы, строит normalized/SIR модель и пишет Codex-пакет. Большие XML-выгрузки и SMB-папки могут выполняться несколько минут.</p>
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_html5_ai_structure_path_check(result: dict | None) -> str:
|
def render_html5_ai_structure_path_check(result: dict | None) -> str:
|
||||||
if result is None:
|
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")
|
status = str(result.get("status") or "info")
|
||||||
title_map = {
|
title_map = {
|
||||||
"ok": "Путь доступен",
|
"ok": "Структура найдена",
|
||||||
"error": "Путь недоступен",
|
"error": "Структура недоступна",
|
||||||
"info": "Проверка пути",
|
"info": "Проверка структуры",
|
||||||
}
|
}
|
||||||
title = title_map.get(status, "Проверка пути")
|
title = title_map.get(status, "Проверка пути")
|
||||||
message = str(result.get("message") or "")
|
message = str(result.get("message") or "")
|
||||||
@@ -269,7 +268,7 @@ def render_html5_ai_structure_error(message: str) -> str:
|
|||||||
<ul class="access-warnings">
|
<ul class="access-warnings">
|
||||||
<li>{escape(message)}</li>
|
<li>{escape(message)}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="muted padded">Проверьте, что входная и выходная папки доступны именно серверу SFERA/API. Если файлы лежат на рабочем ПК, сначала положите их в общую папку или выполните экспорт через агент.</p>
|
<p class="muted padded">Проверьте, что входная и выходная папки доступны именно серверу SFERA/API. Для этого сценария ожидается XML-выгрузка 1С с папкой <code>Конфигурация</code> и, при необходимости, отдельными папками расширений.</p>
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -347,23 +346,6 @@ def _artifact_text(value: object) -> str:
|
|||||||
return mapping.get(str(value or ""), str(value or ""))
|
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:
|
def _layout_kind_text(value: str) -> str:
|
||||||
mapping = {
|
mapping = {
|
||||||
"configuration_with_extensions": "Конфигурация + отдельные папки расширений",
|
"configuration_with_extensions": "Конфигурация + отдельные папки расширений",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ async def html5_ai_structure_run(
|
|||||||
input_path = form_value(form, "input_path")
|
input_path = form_value(form, "input_path")
|
||||||
output_path = form_value(form, "output_path")
|
output_path = form_value(form, "output_path")
|
||||||
if not input_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:
|
if not output_path:
|
||||||
return render_html5_ai_structure_error("Заполните папку результата.")
|
return render_html5_ai_structure_error("Заполните папку результата.")
|
||||||
saved = load_credentials(project_id) if load_credentials else None
|
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", "")
|
password = form_value(form, "smb_password") or (saved or {}).get("password", "")
|
||||||
domain = form_value(form, "smb_domain") or (saved or {}).get("domain", "")
|
domain = form_value(form, "smb_domain") or (saved or {}).get("domain", "")
|
||||||
if _detect_binary_input(input_path):
|
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(
|
||||||
return render_html5_ai_structure_path_check(result)
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Для этой страницы поддерживается только XML-выгрузка 1С. Бинарные .cf/.cfe здесь не используются.",
|
||||||
|
"details": ["Подготовьте папку с `Конфигурация` и, при необходимости, с отдельными папками расширений."],
|
||||||
|
}
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
result = _inspect_ai_structure_input(
|
result = _inspect_ai_structure_input(
|
||||||
raw_input_path=input_path,
|
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")
|
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")
|
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")
|
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 = [
|
details = [
|
||||||
f"Тип раскладки: {_layout_kind_text(layout['kind'])}",
|
f"Тип раскладки: {_layout_kind_text(layout['kind'])}",
|
||||||
f"Главная папка: {layout['main_configuration_root']}",
|
f"Главная папка: {layout['main_configuration_root']}",
|
||||||
@@ -439,6 +456,10 @@ def _inspect_ai_structure_input(
|
|||||||
f"Файлов MDO: {mdo_count}",
|
f"Файлов MDO: {mdo_count}",
|
||||||
f"Файлов BSL: {bsl_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:
|
if copied_from_unc:
|
||||||
details.append("Проверка выполнена сервером после чтения UNC-пути по SMB.")
|
details.append("Проверка выполнена сервером после чтения UNC-пути по SMB.")
|
||||||
if parseable:
|
if parseable:
|
||||||
@@ -493,3 +514,13 @@ def _layout_kind_text(value: str) -> str:
|
|||||||
"file": "Отдельный входной файл",
|
"file": "Отдельный входной файл",
|
||||||
}
|
}
|
||||||
return mapping.get(value, value)
|
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
|
||||||
|
|||||||
@@ -1770,6 +1770,7 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
|
|||||||
"Ожидаемый формат выгрузки 1С",
|
"Ожидаемый формат выгрузки 1С",
|
||||||
"Конфигурация",
|
"Конфигурация",
|
||||||
"Без лишнего сырья",
|
"Без лишнего сырья",
|
||||||
|
"Для этой страницы используется только XML-выгрузка 1С",
|
||||||
"smb_username",
|
"smb_username",
|
||||||
"smb_password",
|
"smb_password",
|
||||||
"data-ai-structure-progress",
|
"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
|
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)
|
client = TestClient(app)
|
||||||
project_id = f"ai-page-{uuid4()}"
|
project_id = f"ai-page-{uuid4()}"
|
||||||
agent_id = f"win-agent-{uuid4()}"
|
|
||||||
|
|
||||||
settings = client.post(
|
settings = client.post(
|
||||||
f"/projects/{project_id}/settings",
|
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
|
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")
|
page = client.get(f"/html5/projects/{project_id}/ai-structure")
|
||||||
assert page.status_code == 200
|
assert page.status_code == 200
|
||||||
assert "Агент для CF/CFE" in page.text
|
assert "Для этой страницы используется только XML-выгрузка 1С" in page.text
|
||||||
assert "агент онлайн" in page.text
|
assert "Агент для CF/CFE" not 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
|
|
||||||
|
|
||||||
|
|
||||||
def test_html5_ai_structure_page_shows_missing_agent_hint():
|
def test_html5_ai_structure_page_shows_xml_layout_hint():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
project_id = f"ai-page-missing-{uuid4()}"
|
project_id = f"ai-page-missing-{uuid4()}"
|
||||||
|
|
||||||
settings = client.post(
|
settings = client.post(
|
||||||
f"/projects/{project_id}/settings",
|
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
|
assert settings.status_code == 200
|
||||||
|
|
||||||
page = client.get(f"/html5/projects/{project_id}/ai-structure")
|
page = client.get(f"/html5/projects/{project_id}/ai-structure")
|
||||||
assert page.status_code == 200
|
assert page.status_code == 200
|
||||||
assert "агент не настроен" in page.text
|
assert "Папка с XML-выгрузкой 1С" in page.text
|
||||||
assert "Укажите его в настройках проекта" in page.text
|
assert "Проверить структуру выгрузки" in page.text
|
||||||
|
|
||||||
|
|
||||||
def test_html5_ai_structure_check_path_button_present():
|
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()}"
|
project_id = f"ai-path-button-{uuid4()}"
|
||||||
settings = client.post(
|
settings = client.post(
|
||||||
f"/projects/{project_id}/settings",
|
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
|
assert settings.status_code == 200
|
||||||
page = client.get(f"/html5/projects/{project_id}/ai-structure")
|
page = client.get(f"/html5/projects/{project_id}/ai-structure")
|
||||||
assert page.status_code == 200
|
assert page.status_code == 200
|
||||||
assert "Проверить путь у агента" in page.text
|
assert "Проверить структуру выгрузки" in page.text
|
||||||
assert "/ai-structure/check-path" 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 "Главная папка: Конфигурация" in checked.text
|
assert "Главная папка: Конфигурация" in checked.text
|
||||||
assert "Папки расширений: CRM" 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):
|
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):
|
||||||
|
|||||||
Reference in New Issue
Block a user