param( [int]$Port = 8010, [int]$ReadyTimeoutSeconds = 60 ) $ErrorActionPreference = "Stop" $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $runtimeRoot = Join-Path $projectRoot ".runtime" $logRoot = Join-Path $projectRoot "logs\runtime" New-Item -ItemType Directory -Force -Path $runtimeRoot, $logRoot | Out-Null function Get-OwnedRuntimeProcess { param([string]$PidFile) if (-not (Test-Path -LiteralPath $PidFile)) { return $null } $parts = (Get-Content -LiteralPath $PidFile -Raw).Trim().Split("|") if ($parts.Count -ne 2) { return $null } $processId = 0 $startTicks = 0L if ( -not [int]::TryParse($parts[0], [ref]$processId) -or -not [long]::TryParse($parts[1], [ref]$startTicks) ) { return $null } $process = Get-Process -Id $processId -ErrorAction SilentlyContinue if ( $null -eq $process -or $process.StartTime.ToUniversalTime().Ticks -ne $startTicks ) { return $null } return $process } $pidFiles = @{ Api = Join-Path $runtimeRoot "api.pid" FeishuEvents = Join-Path $runtimeRoot "feishu-events.pid" } foreach ($entry in $pidFiles.GetEnumerator()) { if ($null -ne (Get-OwnedRuntimeProcess -PidFile $entry.Value)) { throw "$($entry.Key) is already running. Stop it before starting another instance." } if (Test-Path -LiteralPath $entry.Value) { Remove-Item -LiteralPath $entry.Value -Force } } $pythonOutput = & conda run -n company-ai-platform python -c "import sys; print(sys.executable)" if ($LASTEXITCODE -ne 0) { throw "Unable to locate Python in the company-ai-platform Conda environment." } $pythonExe = ( $pythonOutput | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Last 1 ) if (-not [string]::IsNullOrWhiteSpace($pythonExe)) { $pythonExe = $pythonExe.Trim() } if ( [string]::IsNullOrWhiteSpace($pythonExe) -or -not (Test-Path -LiteralPath $pythonExe) ) { $condaEnvironmentOutput = & conda env list --json if ($LASTEXITCODE -ne 0) { throw "Unable to list Conda environments." } $condaEnvironmentList = ( ($condaEnvironmentOutput -join [Environment]::NewLine) | ConvertFrom-Json ) $matchingEnvironmentRoots = @( $condaEnvironmentList.envs | Where-Object { (Split-Path -Leaf $_) -eq "company-ai-platform" } ) if ($matchingEnvironmentRoots.Count -ne 1) { throw "Unable to resolve a unique company-ai-platform Conda environment." } $pythonExe = Join-Path $matchingEnvironmentRoots[0] "python.exe" } if (-not (Test-Path -LiteralPath $pythonExe -PathType Leaf)) { throw "The company-ai-platform Python executable was not found." } # Process environment variables take precedence over .env. $env:SCHEDULER_ENABLED = "true" $env:TASK_QUEUE_ENABLED = "false" $env:FEISHU_EVENT_TRANSPORT = "long_connection" $preflightOutput = & $pythonExe -m app.tools.runtime_preflight if ($LASTEXITCODE -ne 0) { throw "Runtime preflight failed. Review the configuration and migrate the platform database." } Write-Host ($preflightOutput | Select-Object -Last 1) $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" function Start-HiddenRuntimeProcess { param( [string]$Name, [string[]]$Arguments, [string]$PidFile ) $stdoutPath = Join-Path $logRoot "$Name-$timestamp.stdout.log" $stderrPath = Join-Path $logRoot "$Name-$timestamp.stderr.log" $process = Start-Process ` -FilePath $pythonExe ` -ArgumentList $Arguments ` -WorkingDirectory $projectRoot ` -WindowStyle Hidden ` -RedirectStandardOutput $stdoutPath ` -RedirectStandardError $stderrPath ` -PassThru $identity = "$($process.Id)|$($process.StartTime.ToUniversalTime().Ticks)" Set-Content -LiteralPath $PidFile -Value $identity -NoNewline return @{ Process = $process Stdout = $stdoutPath Stderr = $stderrPath } } $started = @() try { $api = Start-HiddenRuntimeProcess ` -Name "api" ` -Arguments @( "-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", "$Port" ) ` -PidFile $pidFiles.Api $started += $api $feishuEvents = Start-HiddenRuntimeProcess ` -Name "feishu-events" ` -Arguments @("-m", "app.modules.feishu.long_connection") ` -PidFile $pidFiles.FeishuEvents $started += $feishuEvents $deadline = (Get-Date).AddSeconds($ReadyTimeoutSeconds) $readyUrl = "http://127.0.0.1:$Port/api/v1/health/ready" while ((Get-Date) -lt $deadline) { foreach ($item in $started) { if ($item.Process.HasExited) { throw "A runtime process exited before readiness succeeded." } } try { $response = Invoke-WebRequest -Uri $readyUrl -TimeoutSec 3 -UseBasicParsing if ($response.StatusCode -eq 200) { Write-Host "Runtime is ready at http://127.0.0.1:$Port" Write-Host "Logs: $logRoot" return } } catch { # Readiness can be unavailable briefly while both processes initialize. } Start-Sleep -Seconds 1 } throw "Runtime did not become ready within $ReadyTimeoutSeconds seconds." } catch { foreach ($item in $started) { if (-not $item.Process.HasExited) { Stop-Process -Id $item.Process.Id -Force -ErrorAction SilentlyContinue } } Remove-Item -LiteralPath $pidFiles.Api, $pidFiles.FeishuEvents ` -Force -ErrorAction SilentlyContinue throw }