Switch AI structure HTML5 flow to XML exports
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user