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 df3c0dd..1a6dbeb 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 @@ -44,6 +44,7 @@ async def html5_ai_structure_run( prepare: Callable[..., dict[str, Any]], work_root: Path, start_binary_job: Callable[..., Any] | None = None, + stage_binary_input: Callable[..., Any] | None = None, save_run_state: Callable[[str, dict[str, Any]], None] | None = None, load_credentials: Callable[[str], SmbCredentials | None] | None = None, save_credentials: Callable[[str, SmbCredentials], None] | None = None, @@ -85,11 +86,26 @@ async def html5_ai_structure_run( 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 не подключен.") + binary_input_path = input_path + if is_unc_path(input_path) and local_input.exists() and stage_binary_input is not None: + try: + binary_input_path = await stage_binary_input( + project_id=project_id, + effective_project_id=effective_project_id, + local_input=local_input, + username=username, + password=password, + domain=domain or None, + ) + except HTTPException as error: + return render_html5_ai_structure_error(str(error.detail)) + except RuntimeError as error: + return render_html5_ai_structure_error(str(error)) try: job = await start_binary_job( project_id=project_id, effective_project_id=effective_project_id, - input_path=input_path, + input_path=binary_input_path, detected_binary_relative_path=binary_match.get("relative_path"), detected_binary_relative_paths=binary_match.get("binary_relative_paths"), ) diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index e3e3dfb..60ce3ca 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -193,7 +193,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.smb_paths import copy_local_tree_to_smb, 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 @@ -657,6 +657,39 @@ async def _queue_ai_structure_agent_step( ) +async def _stage_ai_structure_binary_input_for_agent( + *, + project_id: str, + effective_project_id: str, + local_input: Path, + username: str, + password: str, + domain: str | None = None, +) -> str: + settings = _project_settings_or_404(project_id) + agent_id = _agent_id_for_source(settings, ImportSourceKind.CF_FILE) + if not agent_id: + raise HTTPException(status_code=400, detail="В настройках проекта не выбран Windows Agent для CF/CFE.") + agent_status = _agent_status_with_liveness(_agent_statuses.get(agent_id, AgentStatus(agent_id=agent_id))) + roots = [str(item).strip() for item in getattr(agent_status, "network_roots", []) if str(item).strip()] + if not roots: + raise HTTPException( + status_code=400, + detail=f"У Windows Agent {agent_id} не настроены доступные сетевые корни для staging CF/CFE.", + ) + if not local_input.exists(): + raise HTTPException(status_code=404, detail=f"Сервер не нашел локальную копию входных файлов: {local_input}") + stage_root = ntpath.join(roots[0], "SFERA", "ai-structure-staging", f"{effective_project_id}-{uuid4().hex}") + copy_local_tree_to_smb( + source=local_input, + target=stage_root, + username=username, + password=password, + domain=domain or None, + ) + return stage_root + + async def _start_ai_structure_agent_job( *, project_id: str, @@ -2117,6 +2150,7 @@ async def html5_project_ai_structure_run(project_id: str, request: Request) -> R prepare=_prepare_ai_structure, work_root=_storage.root / "ai_structure_work", start_binary_job=_start_ai_structure_agent_job, + stage_binary_input=_stage_ai_structure_binary_input_for_agent, save_run_state=_save_ai_structure_agent_run, load_credentials=_load_ai_structure_smb_credentials, save_credentials=_save_ai_structure_smb_credentials, diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index 7d7aabb..83021a1 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -2091,6 +2091,10 @@ def test_html5_ai_structure_routes_unc_directory_with_cf_through_windows_agent(m started.update(kwargs) return FakeJob() + async def fake_stage_binary_input(**kwargs): + started["staged_from"] = kwargs["local_input"] + return r"\\192.168.220.220\mst\SFERA\ai-structure-staging\unc-demo" + saved_runs: dict[str, dict[str, object]] = {} monkeypatch.setattr(controller, "copy_smb_tree_to_local", fake_copy_smb_tree_to_local) @@ -2108,15 +2112,17 @@ def test_html5_ai_structure_routes_unc_directory_with_cf_through_windows_agent(m prepare=lambda **_: {}, work_root=tmp_path / "work", start_binary_job=fake_start_binary_job, + stage_binary_input=fake_stage_binary_input, 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["input_path"] == r"\\192.168.220.220\mst\SFERA\ai-structure-staging\unc-demo" assert started["detected_binary_relative_path"] == "base.cf" assert started["detected_binary_relative_paths"] == ["base.cf"] + assert started["staged_from"] == copied_targets[0][1] assert "agent-import-test" in saved_runs