Handle UNC binary directories in AI structure flow
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 00:54:43 +03:00
parent de8b0eb795
commit 51d52ccf04
3 changed files with 134 additions and 34 deletions
@@ -62,12 +62,30 @@ async def html5_ai_structure_run(
if should_save and save_credentials and username and password: if should_save and save_credentials and username and password:
save_credentials(project_id, {"username": username, "password": password, "domain": domain}) save_credentials(project_id, {"username": username, "password": password, "domain": domain})
binary_source = _detect_binary_input(input_path) work_dir = work_root / f"{effective_project_id}-{uuid4().hex}"
if binary_source is not None: try:
work_dir.mkdir(parents=True, exist_ok=True)
local_input = work_dir / "input" if is_unc_path(input_path) else Path(input_path)
local_output = work_dir / "output" if is_unc_path(output_path) else Path(output_path)
if is_unc_path(input_path):
copy_smb_tree_to_local(
source=input_path,
target=local_input,
username=username,
password=password,
domain=domain or None,
)
binary_match = _detect_binary_tree(local_input) or _normalize_binary_match(_detect_binary_input(input_path))
if binary_match is not None:
if start_binary_job is None or save_run_state is None: if start_binary_job is None or save_run_state is None:
return render_html5_ai_structure_error("Сервис подготовки CF/CFE через Windows Agent не подключен.") return render_html5_ai_structure_error("Сервис подготовки CF/CFE через Windows Agent не подключен.")
try: try:
job = await start_binary_job(project_id=project_id, effective_project_id=effective_project_id, input_path=Path(input_path)) job = await start_binary_job(
project_id=project_id,
effective_project_id=effective_project_id,
input_path=input_path,
detected_binary_relative_path=binary_match.get("relative_path"),
)
except HTTPException as error: except HTTPException as error:
return render_html5_ai_structure_error(str(error.detail)) return render_html5_ai_structure_error(str(error.detail))
save_run_state( save_run_state(
@@ -92,20 +110,6 @@ async def html5_ai_structure_run(
message="Разбор CF/CFE запущен через Windows Agent", message="Разбор CF/CFE запущен через Windows Agent",
logs=getattr(job, "logs", []), logs=getattr(job, "logs", []),
) )
work_dir = work_root / f"{effective_project_id}-{uuid4().hex}"
try:
work_dir.mkdir(parents=True, exist_ok=True)
local_input = work_dir / "input" if is_unc_path(input_path) else Path(input_path)
local_output = work_dir / "output" if is_unc_path(output_path) else Path(output_path)
if is_unc_path(input_path):
copy_smb_tree_to_local(
source=input_path,
target=local_input,
username=username,
password=password,
domain=domain or None,
)
result = prepare( result = prepare(
project_id=effective_project_id, project_id=effective_project_id,
input_path=local_input, input_path=local_input,
@@ -244,5 +248,30 @@ def _detect_binary_input(raw_input_path: str) -> str | None:
return binary_files[0].suffix.casefold() return binary_files[0].suffix.casefold()
def _detect_binary_tree(input_path: Path) -> dict[str, str] | None:
if not input_path.exists():
return None
suffixes = {".cf", ".cfe"}
if input_path.is_file() and input_path.suffix.casefold() in suffixes:
return {"suffix": input_path.suffix.casefold(), "relative_path": input_path.name}
if not input_path.is_dir():
return None
files = sorted(path for path in input_path.rglob("*") if path.is_file())
parseable_files = any(path.suffix.casefold() in {".xml", ".mdo", ".bsl"} for path in files)
binary_files = [path for path in files if path.suffix.casefold() in suffixes]
if parseable_files or not binary_files:
return None
first = binary_files[0]
return {"suffix": first.suffix.casefold(), "relative_path": first.relative_to(input_path).as_posix()}
def _normalize_binary_match(value: str | dict[str, str] | None) -> dict[str, str] | None:
if value is None:
return None
if isinstance(value, dict):
return value
return {"suffix": value, "relative_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 ""))
+17 -3
View File
@@ -4,6 +4,7 @@ import asyncio
import base64 import base64
import hashlib import hashlib
import json import json
import ntpath
import os import os
import re import re
import shutil import shutil
@@ -581,9 +582,15 @@ def _agent_id_for_source(settings: "ProjectSettingsRequest", source: "ImportSour
return str(agent.get("agent_id") or "").strip() return str(agent.get("agent_id") or "").strip()
async def _start_ai_structure_agent_job(*, project_id: str, effective_project_id: str, input_path: Path) -> AgentImportJob: async def _start_ai_structure_agent_job(
*,
project_id: str,
effective_project_id: str,
input_path: str,
detected_binary_relative_path: str | None = None,
) -> AgentImportJob:
settings = _project_settings_or_404(project_id) settings = _project_settings_or_404(project_id)
binary_files = _ai_structure_binary_files(input_path) binary_files = _ai_structure_binary_files(input_path, detected_binary_relative_path=detected_binary_relative_path)
if not binary_files: if not binary_files:
raise HTTPException(status_code=400, detail="Во входном пути не найдены файлы .cf или .cfe.") raise HTTPException(status_code=400, detail="Во входном пути не найдены файлы .cf или .cfe.")
@@ -641,7 +648,14 @@ async def _start_ai_structure_agent_job(*, project_id: str, effective_project_id
) )
def _ai_structure_binary_files(input_path: Path) -> list[Path]: def _ai_structure_binary_files(raw_input_path: str, detected_binary_relative_path: str | None = None) -> list[Path]:
lowered = raw_input_path.strip().casefold()
if lowered.endswith(".cf") or lowered.endswith(".cfe"):
return [Path(raw_input_path)]
if detected_binary_relative_path:
windows_path = ntpath.join(raw_input_path, detected_binary_relative_path.replace("/", "\\"))
return [Path(windows_path)]
input_path = Path(raw_input_path)
if input_path.is_file() and input_path.suffix.casefold() in {".cf", ".cfe"}: if input_path.is_file() and input_path.suffix.casefold() in {".cf", ".cfe"}:
return [input_path] return [input_path]
if not input_path.exists() or not input_path.is_dir(): if not input_path.exists() or not input_path.is_dir():
+57
View File
@@ -1,3 +1,4 @@
import asyncio
from pathlib import Path from pathlib import Path
import json import json
import re import re
@@ -1961,6 +1962,62 @@ def test_html5_ai_structure_routes_binary_cfe_through_windows_agent(tmp_path: Pa
assert (output / f"codex-1c-context-{project_id}" / "AGENTS.md").exists() assert (output / f"codex-1c-context-{project_id}" / "AGENTS.md").exists()
def test_html5_ai_structure_routes_unc_directory_with_cf_through_windows_agent(monkeypatch, tmp_path: Path):
from api_server import html5_ai_structure_controller as controller
copied_root = tmp_path / "copied-unc"
copied_root.mkdir()
(copied_root / "base.cf").write_bytes(b"binary-cf")
copied_targets: list[tuple[str, Path]] = []
def fake_copy_smb_tree_to_local(*, source: str, target: Path, username: str, password: str, domain: str | None = None) -> None:
copied_targets.append((source, target))
target.mkdir(parents=True, exist_ok=True)
for item in copied_root.iterdir():
if item.is_file():
(target / item.name).write_bytes(item.read_bytes())
class FakeJob:
job_id = "agent-import-test"
status = "QUEUED"
source = "CF_FILE"
logs = ["queued"]
started: dict[str, object] = {}
async def fake_start_binary_job(**kwargs):
started.update(kwargs)
return FakeJob()
saved_runs: dict[str, dict[str, object]] = {}
monkeypatch.setattr(controller, "copy_smb_tree_to_local", fake_copy_smb_tree_to_local)
html = asyncio.run(
controller.html5_ai_structure_run(
project_id="unc-demo",
form={
"project_id": ["unc-demo"],
"input_path": [r"\\192.168.220.200\mst\1c\MARKA\CODEX\CF"],
"output_path": [r"\\192.168.220.200\mst\1c\MARKA\CODEX\CODEX"],
"smb_username": ["m"],
"smb_password": ["secret"],
},
prepare=lambda **_: {},
work_root=tmp_path / "work",
start_binary_job=fake_start_binary_job,
save_run_state=lambda job_id, payload: saved_runs.setdefault(job_id, payload),
)
)
assert "Windows Agent" in html
assert copied_targets
assert started["input_path"] == r"\\192.168.220.200\mst\1c\MARKA\CODEX\CF"
assert started["detected_binary_relative_path"] == "base.cf"
assert "agent-import-test" in saved_runs
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"