Improve AI structure agent path diagnostics
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 04:40:35 +03:00
parent e12332d3c6
commit b3689b1d9e
4 changed files with 92 additions and 5 deletions
@@ -57,6 +57,8 @@ def render_html5_ai_structure_agent_panel(project_id: str, *, agent_info: dict[s
details.append(("Хост", str(info["host"])))
if info.get("version"):
details.append(("Версия", str(info["version"])))
if info.get("network_roots"):
details.append(("Доступные сетевые корни", str(info["network_roots"])))
detail_html = "".join(
f'<article class="access-card"><strong>{escape(label)}</strong><small>{escape(value)}</small></article>'
for label, value in details
@@ -250,7 +252,7 @@ def _agent_status_title(status: str) -> str:
def _agent_status_advice(status: str, agent_id: str) -> str:
if status == "online":
return f"Для бинарных .cf/.cfe будет использован Windows Agent {agent_id}. Можно запускать подготовку."
return f"Для бинарных .cf/.cfe будет использован Windows Agent {agent_id}. Перед запуском проверьте, что входной UNC-путь лежит внутри одного из доступных сетевых корней агента."
if status == "offline":
return f"Windows Agent {agent_id} выбран, но сейчас не отвечает. Обновите или запустите агент, затем повторите."
return "Для прямого разбора .cf/.cfe нужен выбранный Windows Agent. Укажите его в настройках проекта."
@@ -67,9 +67,11 @@ async def html5_ai_structure_run(
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)
direct_binary_match = _normalize_binary_match(_detect_binary_input(input_path))
direct_binary_file = input_path.strip().casefold().endswith((".cf", ".cfe"))
local_input = work_dir / "input" if is_unc_path(input_path) and not direct_binary_file 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):
if is_unc_path(input_path) and not direct_binary_file:
copy_smb_tree_to_local(
source=input_path,
target=local_input,
@@ -77,7 +79,7 @@ async def html5_ai_structure_run(
password=password,
domain=domain or None,
)
binary_match = _detect_binary_tree(local_input) or _normalize_binary_match(_detect_binary_input(input_path))
binary_match = _detect_binary_tree(local_input) or direct_binary_match
if binary_match is not None:
if start_binary_job is None or save_run_state is None:
return render_html5_ai_structure_error("Сервис подготовки CF/CFE через Windows Agent не подключен.")
@@ -191,6 +191,7 @@ from api_server.snapshot_module_service import (
module_sources_for_object as _snapshot_module_sources_for_object,
snapshot_bsl_completion_items as _snapshot_bsl_completion_items,
)
from api_server.smb_paths import is_unc_path
from api_server.time_utils import current_timestamp as _current_timestamp
from impact_engine import object_impact, routine_impact
from incremental_indexer import rebuild_changed_file
@@ -582,6 +583,34 @@ def _agent_id_for_source(settings: "ProjectSettingsRequest", source: "ImportSour
return str(agent.get("agent_id") or "").strip()
def _normalize_unc_for_compare(value: str) -> str:
text = str(value or "").strip().replace("/", "\\")
while "\\\\" in text:
text = text.replace("\\\\", "\\")
if text.startswith("\\") and not text.startswith("\\\\"):
text = "\\" + text
if text.startswith("\\\\"):
return text.rstrip("\\").casefold()
return text.casefold()
def _unc_path_matches_root(path: str, root: str) -> bool:
normalized_path = _normalize_unc_for_compare(path)
normalized_root = _normalize_unc_for_compare(root)
if not normalized_path.startswith("\\\\") or not normalized_root.startswith("\\\\"):
return False
return normalized_path == normalized_root or normalized_path.startswith(normalized_root + "\\")
def _ai_structure_agent_root_mismatch_detail(agent_id: str, input_path: str, roots: list[str]) -> str:
visible_roots = ", ".join(roots) if roots else "не указаны"
return (
f"Windows Agent {agent_id} сейчас не может открыть путь {input_path}. "
f"Для этого агента доступны только корни: {visible_roots}. "
"Укажите путь внутри доступного корня или переключите проект на другой Windows Agent."
)
async def _start_ai_structure_agent_job(
*,
project_id: str,
@@ -625,6 +654,14 @@ async def _start_ai_structure_agent_job(
detail += f" Последний heartbeat: {last_seen}."
detail += " Запустите агент и повторите."
raise HTTPException(status_code=409, detail=detail)
network_roots = [str(item).strip() for item in getattr(agent_status, "network_roots", []) if str(item).strip()]
if is_unc_path(input_path) and network_roots and not any(
_unc_path_matches_root(input_path, root) for root in network_roots
):
raise HTTPException(
status_code=400,
detail=_ai_structure_agent_root_mismatch_detail(agent_id, input_path, network_roots),
)
agent = settings.agent if isinstance(settings.agent, dict) else {}
metadata: dict[str, Any] = {
@@ -718,6 +755,7 @@ def _ai_structure_agent_info(project_id: str) -> dict[str, str]:
"last_seen_at": str(status.last_seen_at or ""),
"host": str(status.host or ""),
"version": str(status.version or ""),
"network_roots": ", ".join(str(item) for item in getattr(status, "network_roots", []) if str(item).strip()),
}
+46 -1
View File
@@ -2081,7 +2081,15 @@ def test_html5_ai_structure_page_shows_agent_status_panel():
json={"name": "AI Page", "structure_source": "CF_FILE", "agent": {"cf_agent_id": agent_id}},
)
assert settings.status_code == 200
heartbeat = client.post("/agent/heartbeat", json={"agent_id": agent_id, "host": "test-host", "version": "0.2.31"})
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")
@@ -2091,6 +2099,7 @@ def test_html5_ai_structure_page_shows_agent_status_panel():
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():
@@ -2109,6 +2118,42 @@ def test_html5_ai_structure_page_shows_missing_agent_hint():
assert "Укажите его в настройках проекта" in page.text
def test_html5_ai_structure_reports_unc_root_mismatch_for_online_agent(tmp_path: Path):
cf_input = r"\\192.168.220.200\mst\1c\MARKA\CODEX\CF\demo.cf"
client = TestClient(app)
project_id = f"ai-root-mismatch-{uuid4()}"
agent_id = f"win-agent-{uuid4()}"
settings = client.post(
f"/projects/{project_id}/settings",
json={"name": "AI Root Mismatch", "structure_source": "CF_FILE", "agent": {"cf_agent_id": agent_id}},
)
assert settings.status_code == 200
heartbeat = client.post(
"/agent/heartbeat",
json={
"agent_id": agent_id,
"host": "test-host",
"network_roots": [r"\\192.168.220.220\mst"],
},
)
assert heartbeat.status_code == 200
queued = client.post(
f"/html5/projects/{project_id}/ai-structure/run",
data={
"project_id": project_id,
"input_path": cf_input,
"output_path": str(tmp_path / "out"),
"smb_username": "m",
"smb_password": "secret",
},
)
assert queued.status_code == 200
assert "сейчас не может открыть путь" in queued.text
assert r"\\192.168.220.220\mst" in queued.text
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):
first = tmp_path / "first"
second = tmp_path / "second"