Add XML export preflight for AI structure
This commit is contained in:
@@ -168,11 +168,35 @@ async def html5_ai_structure_check_path(
|
|||||||
project_id: str,
|
project_id: str,
|
||||||
form: dict[str, list[str]],
|
form: dict[str, list[str]],
|
||||||
check_path: Callable[..., dict[str, Any]],
|
check_path: Callable[..., dict[str, Any]],
|
||||||
|
work_root: Path,
|
||||||
|
load_credentials: Callable[[str], SmbCredentials | None] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
input_path = form_value(form, "input_path")
|
input_path = form_value(form, "input_path")
|
||||||
if not input_path:
|
if not input_path:
|
||||||
return render_html5_ai_structure_path_check({"status": "error", "message": "Сначала укажите входной путь."})
|
return render_html5_ai_structure_path_check({"status": "error", "message": "Сначала укажите входной путь."})
|
||||||
result = await check_path(project_id=project_id, input_path=input_path)
|
saved = load_credentials(project_id) if load_credentials else None
|
||||||
|
username = form_value(form, "smb_username") or (saved or {}).get("username", "")
|
||||||
|
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)
|
||||||
|
try:
|
||||||
|
result = _inspect_ai_structure_input(
|
||||||
|
raw_input_path=input_path,
|
||||||
|
work_root=work_root,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
domain=domain,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as error:
|
||||||
|
result = {"status": "error", "message": str(error)}
|
||||||
|
except PermissionError as error:
|
||||||
|
result = {"status": "error", "message": f"Нет доступа к папке: {error}"}
|
||||||
|
except OSError as error:
|
||||||
|
result = {"status": "error", "message": f"Ошибка файловой системы: {error}"}
|
||||||
|
except RuntimeError as error:
|
||||||
|
result = {"status": "error", "message": str(error)}
|
||||||
return render_html5_ai_structure_path_check(result)
|
return render_html5_ai_structure_path_check(result)
|
||||||
|
|
||||||
|
|
||||||
@@ -367,3 +391,105 @@ def _compose_ai_structure_source_root(target_root: Path, source_roots: list[Path
|
|||||||
|
|
||||||
def _enum_text(value: object) -> str:
|
def _enum_text(value: object) -> str:
|
||||||
return str(getattr(value, "value", value or ""))
|
return str(getattr(value, "value", value or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _inspect_ai_structure_input(
|
||||||
|
*,
|
||||||
|
raw_input_path: str,
|
||||||
|
work_root: Path,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
domain: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
input_path = str(raw_input_path or "").strip()
|
||||||
|
temp_root = work_root / f"inspect-{uuid4().hex}"
|
||||||
|
local_root = Path(input_path)
|
||||||
|
copied_from_unc = False
|
||||||
|
try:
|
||||||
|
if is_unc_path(input_path):
|
||||||
|
if not username or not password:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Для проверки сетевой XML-выгрузки укажите логин и пароль SMB.",
|
||||||
|
}
|
||||||
|
temp_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
local_root = temp_root / "input"
|
||||||
|
copy_smb_tree_to_local(
|
||||||
|
source=input_path,
|
||||||
|
target=local_root,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
domain=domain or None,
|
||||||
|
)
|
||||||
|
copied_from_unc = True
|
||||||
|
if not local_root.exists():
|
||||||
|
raise FileNotFoundError(f"Входная папка не найдена: {input_path}")
|
||||||
|
files = [local_root] if local_root.is_file() else sorted(path for path in local_root.rglob("*") if path.is_file())
|
||||||
|
parseable = [path for path in files if path.suffix.casefold() in {".xml", ".mdo", ".bsl"}]
|
||||||
|
binaries = [path for path in files if path.suffix.casefold() in {".cf", ".cfe"}]
|
||||||
|
layout = _inspect_source_layout(local_root)
|
||||||
|
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")
|
||||||
|
details = [
|
||||||
|
f"Тип раскладки: {_layout_kind_text(layout['kind'])}",
|
||||||
|
f"Главная папка: {layout['main_configuration_root']}",
|
||||||
|
f"Папки расширений: {', '.join(layout['extension_roots']) or 'нет'}",
|
||||||
|
f"Файлов XML: {xml_count}",
|
||||||
|
f"Файлов MDO: {mdo_count}",
|
||||||
|
f"Файлов BSL: {bsl_count}",
|
||||||
|
]
|
||||||
|
if copied_from_unc:
|
||||||
|
details.append("Проверка выполнена сервером после чтения UNC-пути по SMB.")
|
||||||
|
if parseable:
|
||||||
|
warnings: list[str] = []
|
||||||
|
if layout["kind"] == "flat_or_mixed":
|
||||||
|
warnings.append("Папка `Конфигурация` не найдена. Сервер все равно попытается собрать проект по имеющимся XML/MDO/BSL.")
|
||||||
|
if binaries:
|
||||||
|
warnings.append("Во входной папке есть и бинарные .cf/.cfe, и XML-выгрузка. Для server-side подготовки будут использованы XML/MDO/BSL-файлы.")
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Сервер видит выгрузку 1С и может готовить пакет для Codex без Windows Agent.",
|
||||||
|
"details": details + warnings,
|
||||||
|
}
|
||||||
|
if binaries:
|
||||||
|
return {
|
||||||
|
"status": "info",
|
||||||
|
"message": "Во входной папке найдены только бинарные .cf/.cfe. Для них потребуется Windows Agent или runtime 1С.",
|
||||||
|
"details": details + [f"Бинарных файлов: {len(binaries)}"],
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Во входной папке не найдены XML/MDO/BSL-файлы выгрузки 1С.",
|
||||||
|
"details": details,
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
if copied_from_unc and temp_root.exists():
|
||||||
|
remove_tree(temp_root, expected_parent=work_root)
|
||||||
|
|
||||||
|
|
||||||
|
def _inspect_source_layout(root: Path) -> dict[str, Any]:
|
||||||
|
if root.is_file():
|
||||||
|
return {"kind": "file", "main_configuration_root": root.name, "extension_roots": []}
|
||||||
|
children = [path for path in sorted(root.iterdir()) if path.is_dir()]
|
||||||
|
config_dir = next((path for path in children if path.name.casefold() in {"configuration", "конфигурация"}), None)
|
||||||
|
extension_roots = [
|
||||||
|
path.name
|
||||||
|
for path in children
|
||||||
|
if path != config_dir and any(item.suffix.casefold() in {".xml", ".mdo", ".bsl"} for item in path.rglob("*") if item.is_file())
|
||||||
|
]
|
||||||
|
kind = "configuration_with_extensions" if config_dir else "flat_or_mixed"
|
||||||
|
return {
|
||||||
|
"kind": kind,
|
||||||
|
"main_configuration_root": config_dir.name if config_dir else root.name,
|
||||||
|
"extension_roots": extension_roots,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _layout_kind_text(value: str) -> str:
|
||||||
|
mapping = {
|
||||||
|
"configuration_with_extensions": "Конфигурация + отдельные папки расширений",
|
||||||
|
"flat_or_mixed": "Плоская или смешанная выгрузка",
|
||||||
|
"file": "Отдельный входной файл",
|
||||||
|
}
|
||||||
|
return mapping.get(value, value)
|
||||||
|
|||||||
@@ -2167,6 +2167,8 @@ async def html5_project_ai_structure_check_path(project_id: str, request: Reques
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
form=form,
|
form=form,
|
||||||
check_path=_check_ai_structure_agent_path,
|
check_path=_check_ai_structure_agent_path,
|
||||||
|
work_root=_storage.root / "ai_structure_work",
|
||||||
|
load_credentials=_load_ai_structure_smb_credentials,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2365,6 +2365,32 @@ def test_html5_ai_structure_check_path_reports_root_mismatch():
|
|||||||
assert "доступны только корни" in checked.text
|
assert "доступны только корни" in checked.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_html5_ai_structure_check_path_reports_xml_export_layout(tmp_path: Path):
|
||||||
|
source = tmp_path / "xml-export"
|
||||||
|
(source / "Конфигурация").mkdir(parents=True)
|
||||||
|
(source / "CRM").mkdir(parents=True)
|
||||||
|
(source / "Конфигурация" / "metadata.xml").write_text("<Configuration />", encoding="utf-8")
|
||||||
|
(source / "CRM" / "РасширениеCRM.mdo").write_text("<ConfigurationExtension />", encoding="utf-8")
|
||||||
|
client = TestClient(app)
|
||||||
|
project_id = f"ai-xml-check-{uuid4()}"
|
||||||
|
|
||||||
|
settings = client.post(
|
||||||
|
f"/projects/{project_id}/settings",
|
||||||
|
json={"name": "AI XML Check", "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(source)},
|
||||||
|
)
|
||||||
|
assert checked.status_code == 200
|
||||||
|
assert "Сервер видит выгрузку 1С" in checked.text
|
||||||
|
assert "Конфигурация + отдельные папки расширений" in checked.text
|
||||||
|
assert "Главная папка: Конфигурация" in checked.text
|
||||||
|
assert "Папки расширений: CRM" 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):
|
||||||
first = tmp_path / "first"
|
first = tmp_path / "first"
|
||||||
second = tmp_path / "second"
|
second = tmp_path / "second"
|
||||||
|
|||||||
Reference in New Issue
Block a user