Handle UNC binary directories in AI structure flow
This commit is contained in:
@@ -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 ""))
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user