Improve AI structure agent path diagnostics
This commit is contained in:
@@ -57,6 +57,8 @@ def render_html5_ai_structure_agent_panel(project_id: str, *, agent_info: dict[s
|
|||||||
details.append(("Хост", str(info["host"])))
|
details.append(("Хост", str(info["host"])))
|
||||||
if info.get("version"):
|
if info.get("version"):
|
||||||
details.append(("Версия", str(info["version"])))
|
details.append(("Версия", str(info["version"])))
|
||||||
|
if info.get("network_roots"):
|
||||||
|
details.append(("Доступные сетевые корни", str(info["network_roots"])))
|
||||||
detail_html = "".join(
|
detail_html = "".join(
|
||||||
f'<article class="access-card"><strong>{escape(label)}</strong><small>{escape(value)}</small></article>'
|
f'<article class="access-card"><strong>{escape(label)}</strong><small>{escape(value)}</small></article>'
|
||||||
for label, value in details
|
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:
|
def _agent_status_advice(status: str, agent_id: str) -> str:
|
||||||
if status == "online":
|
if status == "online":
|
||||||
return f"Для бинарных .cf/.cfe будет использован Windows Agent {agent_id}. Можно запускать подготовку."
|
return f"Для бинарных .cf/.cfe будет использован Windows Agent {agent_id}. Перед запуском проверьте, что входной UNC-путь лежит внутри одного из доступных сетевых корней агента."
|
||||||
if status == "offline":
|
if status == "offline":
|
||||||
return f"Windows Agent {agent_id} выбран, но сейчас не отвечает. Обновите или запустите агент, затем повторите."
|
return f"Windows Agent {agent_id} выбран, но сейчас не отвечает. Обновите или запустите агент, затем повторите."
|
||||||
return "Для прямого разбора .cf/.cfe нужен выбранный Windows Agent. Укажите его в настройках проекта."
|
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}"
|
work_dir = work_root / f"{effective_project_id}-{uuid4().hex}"
|
||||||
try:
|
try:
|
||||||
work_dir.mkdir(parents=True, exist_ok=True)
|
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)
|
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(
|
copy_smb_tree_to_local(
|
||||||
source=input_path,
|
source=input_path,
|
||||||
target=local_input,
|
target=local_input,
|
||||||
@@ -77,7 +79,7 @@ async def html5_ai_structure_run(
|
|||||||
password=password,
|
password=password,
|
||||||
domain=domain or None,
|
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 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 не подключен.")
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ from api_server.snapshot_module_service import (
|
|||||||
module_sources_for_object as _snapshot_module_sources_for_object,
|
module_sources_for_object as _snapshot_module_sources_for_object,
|
||||||
snapshot_bsl_completion_items as _snapshot_bsl_completion_items,
|
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 api_server.time_utils import current_timestamp as _current_timestamp
|
||||||
from impact_engine import object_impact, routine_impact
|
from impact_engine import object_impact, routine_impact
|
||||||
from incremental_indexer import rebuild_changed_file
|
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()
|
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(
|
async def _start_ai_structure_agent_job(
|
||||||
*,
|
*,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
@@ -625,6 +654,14 @@ async def _start_ai_structure_agent_job(
|
|||||||
detail += f" Последний heartbeat: {last_seen}."
|
detail += f" Последний heartbeat: {last_seen}."
|
||||||
detail += " Запустите агент и повторите."
|
detail += " Запустите агент и повторите."
|
||||||
raise HTTPException(status_code=409, detail=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 {}
|
agent = settings.agent if isinstance(settings.agent, dict) else {}
|
||||||
metadata: dict[str, Any] = {
|
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 ""),
|
"last_seen_at": str(status.last_seen_at or ""),
|
||||||
"host": str(status.host or ""),
|
"host": str(status.host or ""),
|
||||||
"version": str(status.version or ""),
|
"version": str(status.version or ""),
|
||||||
|
"network_roots": ", ".join(str(item) for item in getattr(status, "network_roots", []) if str(item).strip()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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}},
|
json={"name": "AI Page", "structure_source": "CF_FILE", "agent": {"cf_agent_id": agent_id}},
|
||||||
)
|
)
|
||||||
assert settings.status_code == 200
|
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
|
assert heartbeat.status_code == 200
|
||||||
|
|
||||||
page = client.get(f"/html5/projects/{project_id}/ai-structure")
|
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 agent_id in page.text
|
||||||
assert "test-host" in page.text
|
assert "test-host" in page.text
|
||||||
assert "0.2.31" 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():
|
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
|
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):
|
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