diff --git a/services/api-server/src/api_server/html5_ai_structure.py b/services/api-server/src/api_server/html5_ai_structure.py index 84684c6..15e8d0a 100644 --- a/services/api-server/src/api_server/html5_ai_structure.py +++ b/services/api-server/src/api_server/html5_ai_structure.py @@ -33,6 +33,7 @@ def render_html5_ai_structure_page(
Пути должны быть доступны серверу SFERA/API. Для docker-test используйте папку, смонтированную или доступную внутри контейнера; локальные диски Windows и закрытые SMB-папки без учетных данных сервер не увидит.
Сначала можно проверить, видит ли Windows Agent входной путь и файл .cf/.cfe.
' + status = str(result.get("status") or "info") + title_map = { + "ok": "Путь доступен", + "error": "Путь недоступен", + "info": "Проверка пути", + } + title = title_map.get(status, "Проверка пути") + message = str(result.get("message") or "") + details = list(result.get("details") or []) + return f""" +Укажите входную и выходную папку. Файлы будут созданы сервером в указанном каталоге.
' diff --git a/services/api-server/src/api_server/html5_ai_structure_controller.py b/services/api-server/src/api_server/html5_ai_structure_controller.py index 81ff1e5..6c645ae 100644 --- a/services/api-server/src/api_server/html5_ai_structure_controller.py +++ b/services/api-server/src/api_server/html5_ai_structure_controller.py @@ -10,6 +10,7 @@ from fastapi import HTTPException from api_server.html5_ai_structure import ( render_html5_ai_structure_error, render_html5_ai_structure_job, + render_html5_ai_structure_path_check, render_html5_ai_structure_page, render_html5_ai_structure_result, ) @@ -144,6 +145,19 @@ async def html5_ai_structure_run( return render_html5_ai_structure_result(result) +async def html5_ai_structure_check_path( + *, + project_id: str, + form: dict[str, list[str]], + check_path: Callable[..., dict[str, Any]], +) -> str: + input_path = form_value(form, "input_path") + if not input_path: + return render_html5_ai_structure_path_check({"status": "error", "message": "Сначала укажите входной путь."}) + result = await check_path(project_id=project_id, input_path=input_path) + return render_html5_ai_structure_path_check(result) + + def html5_ai_structure_job( *, project_id: str, diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index 509f9bf..05287db 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -66,6 +66,7 @@ from api_server.html5_access_controller import ( ) from api_server.ai_structure_service import prepare_ai_structure as _prepare_ai_structure from api_server.html5_ai_structure_controller import ( + html5_ai_structure_check_path as _html5_ai_structure_check_path, html5_ai_structure_job as _html5_ai_structure_job, html5_ai_structure_page as _html5_ai_structure_page, html5_ai_structure_run as _html5_ai_structure_run, @@ -708,6 +709,99 @@ async def _start_ai_structure_agent_job( ) +async def _check_ai_structure_agent_path(*, project_id: str, input_path: str) -> dict[str, Any]: + normalized_input = str(input_path or "").strip() + if not normalized_input: + return {"status": "error", "message": "Путь не указан."} + settings = _project_settings_or_404(project_id) + agent_id = _agent_id_for_source(settings, ImportSourceKind.CF_FILE) + if not agent_id: + return { + "status": "error", + "message": "В настройках проекта не выбран Windows Agent для CF/CFE.", + } + agent_status = _agent_status_with_liveness(_agent_statuses.get(agent_id, AgentStatus(agent_id=agent_id))) + if agent_status.status != "online": + details = [] + if agent_status.last_seen_at: + details.append(f"Последний heartbeat: {agent_status.last_seen_at}") + return { + "status": "error", + "message": f"Windows Agent {agent_id} сейчас офлайн.", + "details": details, + } + network_roots = [str(item).strip() for item in getattr(agent_status, "network_roots", []) if str(item).strip()] + if is_unc_path(normalized_input) and network_roots and not any( + _unc_path_matches_root(normalized_input, root) for root in network_roots + ): + return { + "status": "error", + "message": _ai_structure_agent_root_mismatch_detail(agent_id, normalized_input, network_roots), + } + if not is_unc_path(normalized_input): + return { + "status": "info", + "message": "Это не UNC-путь. Проверка у Windows Agent нужна только для сетевых путей или путей на машине агента.", + "details": [f"Текущий путь: {normalized_input}"], + } + + browse_path = normalized_input + target_name = "" + lowered = normalized_input.casefold() + if lowered.endswith(".cf") or lowered.endswith(".cfe"): + browse_path = ntpath.dirname(normalized_input) or normalized_input + target_name = ntpath.basename(normalized_input) + + request = await create_agent_browse_request(AgentBrowseRequestCreate(agent_id=agent_id, path=browse_path)) + for _ in range(20): + current = _agent_browse_requests.get(request.request_id) + if current is None: + break + if current.status not in {AgentBrowseRequestStatus.QUEUED, AgentBrowseRequestStatus.RUNNING}: + request = current + break + await asyncio.sleep(0.5) + else: + return { + "status": "info", + "message": f"Windows Agent {agent_id} еще проверяет путь {browse_path}. Повторите через пару секунд.", + } + + if request.status == AgentBrowseRequestStatus.FAILED: + details = [str(request.error)] if request.error else [] + return { + "status": "error", + "message": f"Windows Agent {agent_id} не смог открыть путь {browse_path}.", + "details": details, + } + if request.status != AgentBrowseRequestStatus.SUCCEEDED: + return { + "status": "info", + "message": f"Проверка пути {browse_path} еще не завершилась.", + } + + entries = list(request.entries or []) + if target_name: + names = {str(item.name) for item in entries} + if target_name not in names: + return { + "status": "error", + "message": f"Папка {browse_path} доступна, но файл {target_name} в ней не найден.", + "details": [f"Найдено элементов: {len(entries)}"], + } + return { + "status": "ok", + "message": f"Windows Agent {agent_id} видит файл {target_name} в папке {browse_path}.", + "details": [f"Элементов в папке: {len(entries)}"], + } + names = [str(item.name) for item in entries[:8]] + return { + "status": "ok", + "message": f"Windows Agent {agent_id} видит путь {browse_path}.", + "details": [f"Элементов найдено: {len(entries)}"] + ([f"Первые элементы: {', '.join(names)}"] if names else []), + } + + def _ai_structure_binary_files( raw_input_path: str, detected_binary_relative_path: str | None = None, @@ -1869,6 +1963,18 @@ async def html5_project_ai_structure_run(project_id: str, request: Request) -> R ) +@app.post("/html5/projects/{project_id}/ai-structure/check-path") +async def html5_project_ai_structure_check_path(project_id: str, request: Request) -> Response: + form = await _html5_form_data(request) + return _html5_response( + await _html5_ai_structure_check_path( + project_id=project_id, + form=form, + check_path=_check_ai_structure_agent_path, + ) + ) + + @app.get("/html5/projects/{project_id}/ai-structure/jobs/{job_id}") async def html5_project_ai_structure_job(project_id: str, job_id: str) -> Response: return _html5_response( diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index 746ddf6..df945d5 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -2118,6 +2118,20 @@ def test_html5_ai_structure_page_shows_missing_agent_hint(): assert "Укажите его в настройках проекта" in page.text +def test_html5_ai_structure_check_path_button_present(): + client = TestClient(app) + project_id = f"ai-path-button-{uuid4()}" + settings = client.post( + f"/projects/{project_id}/settings", + json={"name": "AI Path Button", "structure_source": "CF_FILE"}, + ) + assert settings.status_code == 200 + page = client.get(f"/html5/projects/{project_id}/ai-structure") + assert page.status_code == 200 + assert "Проверить путь у агента" in page.text + assert "/ai-structure/check-path" 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) @@ -2154,6 +2168,34 @@ def test_html5_ai_structure_reports_unc_root_mismatch_for_online_agent(tmp_path: assert r"\\192.168.220.220\mst" in queued.text +def test_html5_ai_structure_check_path_reports_root_mismatch(): + client = TestClient(app) + project_id = f"ai-path-check-{uuid4()}" + agent_id = f"win-agent-{uuid4()}" + settings = client.post( + f"/projects/{project_id}/settings", + json={"name": "AI Path Check", "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 + + checked = client.post( + f"/html5/projects/{project_id}/ai-structure/check-path", + data={"input_path": r"\\192.168.220.200\mst\1c\MARKA\CODEX\CF\demo.cf"}, + ) + assert checked.status_code == 200 + assert "Путь недоступен" in checked.text + assert "доступны только корни" in checked.text + + def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path): first = tmp_path / "first" second = tmp_path / "second"