Route HTML5 AI structure CF/CFE through Windows Agent
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 00:39:02 +03:00
parent 23800dea71
commit b85bff6e06
4 changed files with 391 additions and 2 deletions
+137 -1
View File
@@ -65,6 +65,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_job as _html5_ai_structure_job,
html5_ai_structure_page as _html5_ai_structure_page,
html5_ai_structure_run as _html5_ai_structure_run,
)
@@ -318,6 +319,51 @@ def _save_ai_structure_smb_credentials(project_id: str, credentials: dict[str, s
},
)
def _load_ai_structure_agent_run(job_id: str) -> dict[str, Any] | None:
try:
payload = _storage.read_document("ai_structure_agent_runs", job_id)
except FileNotFoundError:
return None
password_value = str(payload.get("password_b64") or "")
try:
password = base64.b64decode(password_value.encode("ascii")).decode("utf-8") if password_value else ""
except (ValueError, UnicodeDecodeError):
password = ""
result = payload.get("result")
return {
"project_id": str(payload.get("project_id") or ""),
"effective_project_id": str(payload.get("effective_project_id") or ""),
"input_path": str(payload.get("input_path") or ""),
"output_path": str(payload.get("output_path") or ""),
"display_input_path": str(payload.get("display_input_path") or ""),
"display_output_path": str(payload.get("display_output_path") or ""),
"username": str(payload.get("username") or ""),
"domain": str(payload.get("domain") or ""),
"password": password,
"result": result if isinstance(result, dict) else None,
}
def _save_ai_structure_agent_run(job_id: str, payload: dict[str, Any]) -> None:
password = str(payload.get("password") or "")
_storage.write_document(
"ai_structure_agent_runs",
job_id,
{
"project_id": str(payload.get("project_id") or ""),
"effective_project_id": str(payload.get("effective_project_id") or ""),
"input_path": str(payload.get("input_path") or ""),
"output_path": str(payload.get("output_path") or ""),
"display_input_path": str(payload.get("display_input_path") or ""),
"display_output_path": str(payload.get("display_output_path") or ""),
"username": str(payload.get("username") or ""),
"domain": str(payload.get("domain") or ""),
"password_b64": base64.b64encode(password.encode("utf-8")).decode("ascii") if password else "",
"result": payload.get("result") if isinstance(payload.get("result"), dict) else None,
},
)
_ACCESS_TARGET_KINDS = {
NodeKind.CATALOG,
NodeKind.DOCUMENT,
@@ -535,6 +581,78 @@ def _agent_id_for_source(settings: "ProjectSettingsRequest", source: "ImportSour
return str(agent.get("agent_id") or "").strip()
async def _start_ai_structure_agent_job(*, project_id: str, effective_project_id: str, input_path: Path) -> AgentImportJob:
settings = _project_settings_or_404(project_id)
binary_files = _ai_structure_binary_files(input_path)
if not binary_files:
raise HTTPException(status_code=400, detail="Во входном пути не найдены файлы .cf или .cfe.")
source = ImportSourceKind.CF_FILE if any(path.suffix.casefold() == ".cf" for path in binary_files) else ImportSourceKind.CFE_FILE
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)))
if agent_status.status != "online":
raise HTTPException(status_code=409, detail=f"Windows Agent {agent_id} сейчас офлайн. Запустите агент и повторите.")
agent = settings.agent if isinstance(settings.agent, dict) else {}
metadata: dict[str, Any] = {
"platform_version": settings.platform_version or None,
"compatibility_mode": settings.compatibility_mode or None,
}
local_path: str | None = None
if source == ImportSourceKind.CF_FILE:
one_c_server = _agent_string_value(agent, "one_c_server") or _agent_string_value(agent, "published_1c_server") or _agent_string_value(agent, "published_server_url")
one_c_infobase = _agent_string_value(agent, "one_c_infobase") or _agent_string_value(agent, "published_infobase")
if one_c_server.startswith(("http://", "https://")):
one_c_server = urlsplit(one_c_server).hostname or one_c_server
if not one_c_server or not one_c_infobase:
raise HTTPException(
status_code=400,
detail="Для разбора .cf нужен сервер 1С и имя информационной базы в настройках проекта. Сейчас они не заполнены.",
)
metadata.update(
{
"one_c_server": one_c_server,
"one_c_infobase": one_c_infobase,
"one_c_user": _agent_string_value(agent, "one_c_user") or None,
"one_c_password": _agent_string_value(agent, "one_c_password") or None,
"include_extensions": True,
}
)
else:
cfe_files = [path for path in binary_files if path.suffix.casefold() == ".cfe"]
if len(cfe_files) != 1:
raise HTTPException(
status_code=400,
detail="Для прямого разбора расширения укажите один конкретный файл .cfe, а не папку с несколькими расширениями.",
)
cfe_file = cfe_files[0]
local_path = str(cfe_file)
metadata["one_c_extension"] = cfe_file.stem
return await create_agent_import_job(
effective_project_id,
source,
AgentImportJobRequest(
agent_id=agent_id,
source=source,
local_path=local_path,
mode=ImportMode.FULL_REPLACE,
metadata=metadata,
),
)
def _ai_structure_binary_files(input_path: Path) -> list[Path]:
if input_path.is_file() and input_path.suffix.casefold() in {".cf", ".cfe"}:
return [input_path]
if not input_path.exists() or not input_path.is_dir():
return []
return sorted(path for path in input_path.rglob("*") if path.is_file() and path.suffix.casefold() in {".cf", ".cfe"})
def _cancel_stale_extension_install_jobs(project_id: str, selected_agent_id: str) -> None:
now = _current_timestamp()
for job in list(_agent_import_jobs.values()):
@@ -1631,17 +1749,35 @@ async def html5_project_ai_structure(project_id: str) -> Response:
async def html5_project_ai_structure_run(project_id: str, request: Request) -> Response:
form = await _html5_form_data(request)
return _html5_response(
_html5_ai_structure_run(
await _html5_ai_structure_run(
project_id=project_id,
form=form,
prepare=_prepare_ai_structure,
work_root=_storage.root / "ai_structure_work",
start_binary_job=_start_ai_structure_agent_job,
save_run_state=_save_ai_structure_agent_run,
load_credentials=_load_ai_structure_smb_credentials,
save_credentials=_save_ai_structure_smb_credentials,
)
)
@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(
_html5_ai_structure_job(
project_id=project_id,
job_id=job_id,
prepare=_prepare_ai_structure,
work_root=_storage.root / "ai_structure_work",
load_run_state=_load_ai_structure_agent_run,
save_run_state=_save_ai_structure_agent_run,
load_job=lambda current_job_id: _agent_import_jobs.get(current_job_id),
current_project_source_root=_current_project_source_root,
)
)
@app.get("/html5/projects/{project_id}/access/profiles/{profile_name}/plan")
async def html5_project_access_profile_plan(project_id: str, profile_name: str) -> Response:
return _html5_response(