Add AI structure agent path check
This commit is contained in:
@@ -33,6 +33,7 @@ def render_html5_ai_structure_page(
|
||||
<div class="panel-title">Подготовка структуры</div>
|
||||
{render_html5_ai_structure_agent_panel(project_id, agent_info=agent_info)}
|
||||
{render_html5_ai_structure_form(project_id, saved_credentials=saved_credentials)}
|
||||
<div data-html5-ai-structure-path-check>{render_html5_ai_structure_path_check(None)}</div>
|
||||
<p class="object-summary">Пути должны быть доступны серверу SFERA/API. Для docker-test используйте папку, смонтированную или доступную внутри контейнера; локальные диски Windows и закрытые SMB-папки без учетных данных сервер не увидит.</p>
|
||||
<div data-html5-ai-structure-result>{render_html5_ai_structure_result(result)}</div>
|
||||
</section>
|
||||
@@ -117,6 +118,14 @@ def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[s
|
||||
<input name="save_smb_credentials" type="checkbox" value="1" checked />
|
||||
<span>Сохранить</span>
|
||||
</label>
|
||||
<button
|
||||
class="button ai-structure-submit"
|
||||
type="button"
|
||||
hx-post="/html5/projects/{quote(project_id)}/ai-structure/check-path"
|
||||
hx-include="closest form"
|
||||
hx-target="[data-html5-ai-structure-path-check]"
|
||||
hx-swap="innerHTML"
|
||||
>Проверить путь у агента</button>
|
||||
<button class="primary ai-structure-submit" type="submit">Подготовить для ИИ</button>
|
||||
</form>
|
||||
<section class="ai-structure-progress" data-ai-structure-progress hidden aria-live="polite">
|
||||
@@ -136,6 +145,32 @@ def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[s
|
||||
"""
|
||||
|
||||
|
||||
def render_html5_ai_structure_path_check(result: dict | None) -> str:
|
||||
if result is None:
|
||||
return '<p class="muted padded">Сначала можно проверить, видит ли Windows Agent входной путь и файл .cf/.cfe.</p>'
|
||||
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"""
|
||||
<section class="ai-structure-result" data-html5-ai-structure-status="{escape(status)}">
|
||||
<div class="access-plan-head">
|
||||
<span class="status-pill">{escape(title.lower())}</span>
|
||||
<strong>{escape(title)}</strong>
|
||||
</div>
|
||||
<ul class="access-warnings">
|
||||
<li>{escape(message)}</li>
|
||||
{''.join(f'<li>{escape(str(item))}</li>' for item in details)}
|
||||
</ul>
|
||||
</section>
|
||||
"""
|
||||
|
||||
|
||||
def render_html5_ai_structure_result(result: dict | None) -> str:
|
||||
if result is None:
|
||||
return '<p class="muted padded">Укажите входную и выходную папку. Файлы будут созданы сервером в указанном каталоге.</p>'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user