Support direct CF and CFE inputs in AI structure flow
This commit is contained in:
@@ -694,6 +694,100 @@ function Export-CfOrCfeFromInfobase {
|
|||||||
return $exportRoot
|
return $exportRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Convert-LocalCfOrCfeToMetadataExport {
|
||||||
|
param([object]$Job, [string[]]$PlatformBins)
|
||||||
|
$payloadPath = [string]$Job.local_path
|
||||||
|
if ([string]::IsNullOrWhiteSpace($payloadPath)) {
|
||||||
|
throw "local_path is required for direct CF/CFE conversion."
|
||||||
|
}
|
||||||
|
if (!(Test-Path -LiteralPath $payloadPath)) {
|
||||||
|
throw "Local CF/CFE path not found on agent machine: $payloadPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$designerPath = Get-DesignerBinPath -Job $Job -PlatformBins $PlatformBins
|
||||||
|
$workRoot = Join-Path $env:TEMP "sfera-agent"
|
||||||
|
$exportRoot = Join-Path $workRoot "$($Job.job_id)-local-binary"
|
||||||
|
if (Test-Path -LiteralPath $exportRoot) { Remove-Item -LiteralPath $exportRoot -Recurse -Force }
|
||||||
|
New-Item -ItemType Directory -Force -Path $exportRoot | Out-Null
|
||||||
|
|
||||||
|
$builderInfobase = Join-Path $exportRoot "builder-infobase"
|
||||||
|
$createLog = Join-Path $exportRoot "create-builder-infobase.log"
|
||||||
|
Invoke-1CCommand `
|
||||||
|
-PlatformPath $designerPath `
|
||||||
|
-Arguments @("CREATEINFOBASE", "File=$builderInfobase;") `
|
||||||
|
-LogPath $createLog `
|
||||||
|
-JobId $Job.job_id `
|
||||||
|
-ActionTitle "1C CREATEINFOBASE for local CF/CFE conversion" `
|
||||||
|
-TimeoutSeconds 180
|
||||||
|
|
||||||
|
$builderArgs = @("/F", $builderInfobase)
|
||||||
|
$sourceKind = [string]$Job.source
|
||||||
|
$fileName = [System.IO.Path]::GetFileName($payloadPath)
|
||||||
|
$artifactsRoot = Join-Path $exportRoot "artifacts"
|
||||||
|
New-Item -ItemType Directory -Force -Path $artifactsRoot | Out-Null
|
||||||
|
Copy-Item -LiteralPath $payloadPath -Destination (Join-Path $artifactsRoot $fileName) -Force
|
||||||
|
|
||||||
|
if ($sourceKind -eq "CF_FILE") {
|
||||||
|
$loadLog = Join-Path $exportRoot "designer-loadcfg-local-cf.log"
|
||||||
|
Invoke-DesignerCommand `
|
||||||
|
-DesignerPath $designerPath `
|
||||||
|
-Arguments (@($builderArgs) + @("/LoadCfg", $payloadPath)) `
|
||||||
|
-LogPath $loadLog `
|
||||||
|
-JobId $Job.job_id `
|
||||||
|
-ActionTitle "1C LoadCfg local CF" `
|
||||||
|
-TimeoutSeconds 180
|
||||||
|
|
||||||
|
$metadataRoot = Join-Path $exportRoot "configuration"
|
||||||
|
New-Item -ItemType Directory -Force -Path $metadataRoot | Out-Null
|
||||||
|
$metadataLog = Join-Path $exportRoot "designer-dumpconfigtofiles-local-cf.log"
|
||||||
|
Invoke-DesignerCommand `
|
||||||
|
-DesignerPath $designerPath `
|
||||||
|
-Arguments (@($builderArgs) + @("/DumpConfigToFiles", $metadataRoot, "-Format", "Hierarchical")) `
|
||||||
|
-LogPath $metadataLog `
|
||||||
|
-JobId $Job.job_id `
|
||||||
|
-ActionTitle "1C DumpConfigToFiles from local CF" `
|
||||||
|
-TimeoutSeconds 180
|
||||||
|
Copy-Item -LiteralPath $payloadPath -Destination (Join-Path $metadataRoot $fileName) -Force
|
||||||
|
Send-JobLogs -JobId $Job.job_id -Logs @("Local .cf converted to metadata export for server-side parsing.")
|
||||||
|
return $exportRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sourceKind -eq "CFE_FILE") {
|
||||||
|
$extensionName = Get-JobMetadataValue -Job $Job -Key "one_c_extension"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($extensionName)) {
|
||||||
|
$extensionName = [System.IO.Path]::GetFileNameWithoutExtension($payloadPath)
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($extensionName)) {
|
||||||
|
throw "Extension name is required for local CFE conversion."
|
||||||
|
}
|
||||||
|
|
||||||
|
$loadLog = Join-Path $exportRoot "designer-loadcfg-local-cfe.log"
|
||||||
|
Invoke-DesignerCommand `
|
||||||
|
-DesignerPath $designerPath `
|
||||||
|
-Arguments (@($builderArgs) + @("/LoadCfg", $payloadPath, "-Extension", $extensionName, "/UpdateDBCfg")) `
|
||||||
|
-LogPath $loadLog `
|
||||||
|
-JobId $Job.job_id `
|
||||||
|
-ActionTitle "1C LoadCfg local CFE" `
|
||||||
|
-TimeoutSeconds 180
|
||||||
|
|
||||||
|
$metadataRoot = Join-Path $exportRoot "extension"
|
||||||
|
New-Item -ItemType Directory -Force -Path $metadataRoot | Out-Null
|
||||||
|
$metadataLog = Join-Path $exportRoot "designer-dumpconfigtofiles-local-cfe.log"
|
||||||
|
Invoke-DesignerCommand `
|
||||||
|
-DesignerPath $designerPath `
|
||||||
|
-Arguments (@($builderArgs) + @("/DumpConfigToFiles", $metadataRoot, "-Format", "Hierarchical", "-Extension", $extensionName)) `
|
||||||
|
-LogPath $metadataLog `
|
||||||
|
-JobId $Job.job_id `
|
||||||
|
-ActionTitle "1C DumpConfigToFiles from local CFE" `
|
||||||
|
-TimeoutSeconds 180
|
||||||
|
Copy-Item -LiteralPath $payloadPath -Destination (Join-Path $metadataRoot $fileName) -Force
|
||||||
|
Send-JobLogs -JobId $Job.job_id -Logs @("Local .cfe converted to metadata export for server-side parsing.")
|
||||||
|
return $exportRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Unsupported source for local CF/CFE conversion: $sourceKind"
|
||||||
|
}
|
||||||
|
|
||||||
function Install-SferaExtensionJob {
|
function Install-SferaExtensionJob {
|
||||||
param([object]$Job, [string[]]$PlatformBins)
|
param([object]$Job, [string[]]$PlatformBins)
|
||||||
$workRoot = Join-Path $env:TEMP "sfera-agent"
|
$workRoot = Join-Path $env:TEMP "sfera-agent"
|
||||||
@@ -1138,9 +1232,13 @@ while ($true) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$payloadPath = $job.local_path
|
$payloadPath = $job.local_path
|
||||||
if (($job.source -eq "CF_FILE") -or (($job.source -eq "CFE_FILE") -and [string]::IsNullOrWhiteSpace($payloadPath))) {
|
if (($job.source -eq "CF_FILE") -or ($job.source -eq "CFE_FILE")) {
|
||||||
|
if (![string]::IsNullOrWhiteSpace($payloadPath)) {
|
||||||
|
$payloadPath = Convert-LocalCfOrCfeToMetadataExport -Job $job -PlatformBins $platformBins
|
||||||
|
} else {
|
||||||
$payloadPath = Export-CfOrCfeFromInfobase -Job $job -PlatformBins $platformBins
|
$payloadPath = Export-CfOrCfeFromInfobase -Job $job -PlatformBins $platformBins
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ([string]::IsNullOrWhiteSpace($payloadPath)) {
|
if ([string]::IsNullOrWhiteSpace($payloadPath)) {
|
||||||
throw "Job does not contain local_path or enough 1C infobase settings for agent export."
|
throw "Job does not contain local_path or enough 1C infobase settings for agent export."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -587,7 +587,14 @@ async def _start_ai_structure_agent_job(*, project_id: str, effective_project_id
|
|||||||
if not binary_files:
|
if not binary_files:
|
||||||
raise HTTPException(status_code=400, detail="Во входном пути не найдены файлы .cf или .cfe.")
|
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
|
cf_files = [path for path in binary_files if path.suffix.casefold() == ".cf"]
|
||||||
|
cfe_files = [path for path in binary_files if path.suffix.casefold() == ".cfe"]
|
||||||
|
if cf_files and cfe_files:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Во входной папке одновременно лежат .cf и .cfe. Укажите конкретный файл, который нужно подготовить для ИИ.",
|
||||||
|
)
|
||||||
|
source = ImportSourceKind.CF_FILE if cf_files else ImportSourceKind.CFE_FILE
|
||||||
agent_id = _agent_id_for_source(settings, ImportSourceKind.CF_FILE)
|
agent_id = _agent_id_for_source(settings, ImportSourceKind.CF_FILE)
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
raise HTTPException(status_code=400, detail="В настройках проекта не выбран Windows Agent для CF/CFE.")
|
raise HTTPException(status_code=400, detail="В настройках проекта не выбран Windows Agent для CF/CFE.")
|
||||||
@@ -603,26 +610,14 @@ async def _start_ai_structure_agent_job(*, project_id: str, effective_project_id
|
|||||||
local_path: str | None = None
|
local_path: str | None = None
|
||||||
|
|
||||||
if source == ImportSourceKind.CF_FILE:
|
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")
|
if len(cf_files) != 1:
|
||||||
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(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Для разбора .cf нужен сервер 1С и имя информационной базы в настройках проекта. Сейчас они не заполнены.",
|
detail="Для прямого разбора .cf укажите один конкретный файл .cf, а не папку с несколькими конфигурациями.",
|
||||||
)
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
local_path = str(cf_files[0])
|
||||||
|
metadata["input_mode"] = "cf_file"
|
||||||
else:
|
else:
|
||||||
cfe_files = [path for path in binary_files if path.suffix.casefold() == ".cfe"]
|
|
||||||
if len(cfe_files) != 1:
|
if len(cfe_files) != 1:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
@@ -631,6 +626,7 @@ async def _start_ai_structure_agent_job(*, project_id: str, effective_project_id
|
|||||||
cfe_file = cfe_files[0]
|
cfe_file = cfe_files[0]
|
||||||
local_path = str(cfe_file)
|
local_path = str(cfe_file)
|
||||||
metadata["one_c_extension"] = cfe_file.stem
|
metadata["one_c_extension"] = cfe_file.stem
|
||||||
|
metadata["input_mode"] = "cfe_file"
|
||||||
|
|
||||||
return await create_agent_import_job(
|
return await create_agent_import_job(
|
||||||
effective_project_id,
|
effective_project_id,
|
||||||
|
|||||||
@@ -1862,6 +1862,7 @@ def test_html5_ai_structure_routes_binary_cf_through_windows_agent(tmp_path: Pat
|
|||||||
assert claimed.status_code == 200
|
assert claimed.status_code == 200
|
||||||
assert claimed.json()["job_id"] == job_id
|
assert claimed.json()["job_id"] == job_id
|
||||||
assert claimed.json()["source"] == "CF_FILE"
|
assert claimed.json()["source"] == "CF_FILE"
|
||||||
|
assert claimed.json()["local_path"] == str(cf_input)
|
||||||
|
|
||||||
completed = client.post(
|
completed = client.post(
|
||||||
f"/agent/jobs/{job_id}/result",
|
f"/agent/jobs/{job_id}/result",
|
||||||
@@ -1887,6 +1888,79 @@ def test_html5_ai_structure_routes_binary_cf_through_windows_agent(tmp_path: Pat
|
|||||||
assert (output / f"codex-1c-context-{project_id}" / "AGENTS.md").exists()
|
assert (output / f"codex-1c-context-{project_id}" / "AGENTS.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_html5_ai_structure_routes_binary_cfe_through_windows_agent(tmp_path: Path):
|
||||||
|
metadata_root = tmp_path / "extension"
|
||||||
|
metadata_root.mkdir()
|
||||||
|
(metadata_root / "metadata.xml").write_text(
|
||||||
|
"""
|
||||||
|
<Configuration>
|
||||||
|
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты" />
|
||||||
|
</Configuration>
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
cfe_input = tmp_path / "MyExtension.cfe"
|
||||||
|
cfe_input.write_bytes(b"binary-cfe")
|
||||||
|
output = tmp_path / "ai-out-cfe"
|
||||||
|
client = TestClient(app)
|
||||||
|
project_id = f"ai-agent-cfe-{uuid4()}"
|
||||||
|
agent_id = f"win-agent-{uuid4()}"
|
||||||
|
|
||||||
|
indexed = client.post("/projects/index", json={"path": str(metadata_root), "project_id": project_id})
|
||||||
|
assert indexed.status_code == 200
|
||||||
|
settings = client.post(
|
||||||
|
f"/projects/{project_id}/settings",
|
||||||
|
json={
|
||||||
|
"name": "AI Agent Extension Demo",
|
||||||
|
"structure_source": "CFE_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"})
|
||||||
|
assert heartbeat.status_code == 200
|
||||||
|
|
||||||
|
queued = client.post(
|
||||||
|
f"/html5/projects/{project_id}/ai-structure/run",
|
||||||
|
data={"project_id": project_id, "input_path": str(cfe_input), "output_path": str(output)},
|
||||||
|
)
|
||||||
|
assert queued.status_code == 200
|
||||||
|
match = re.search(r"/html5/projects/[^/]+/ai-structure/jobs/([A-Za-z0-9-]+)", queued.text)
|
||||||
|
assert match is not None
|
||||||
|
job_id = match.group(1)
|
||||||
|
|
||||||
|
claimed = client.get("/agent/jobs/next", params={"agent_id": agent_id})
|
||||||
|
assert claimed.status_code == 200
|
||||||
|
assert claimed.json()["job_id"] == job_id
|
||||||
|
assert claimed.json()["source"] == "CFE_FILE"
|
||||||
|
assert claimed.json()["local_path"] == str(cfe_input)
|
||||||
|
assert claimed.json()["metadata"]["one_c_extension"] == "MyExtension"
|
||||||
|
|
||||||
|
completed = client.post(
|
||||||
|
f"/agent/jobs/{job_id}/result",
|
||||||
|
json={
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"server_path": str(metadata_root),
|
||||||
|
"logs": ["Выгрузка расширения завершена."],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert completed.status_code == 200
|
||||||
|
|
||||||
|
deadline = time.monotonic() + 10
|
||||||
|
fragment = ""
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
polled = client.get(f"/html5/projects/{project_id}/ai-structure/jobs/{job_id}")
|
||||||
|
assert polled.status_code == 200
|
||||||
|
fragment = polled.text
|
||||||
|
if "готово" in fragment:
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
assert "готово" in fragment
|
||||||
|
assert (output / f"codex-1c-context-{project_id}" / "AGENTS.md").exists()
|
||||||
|
|
||||||
|
|
||||||
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