chore: archive project history and preserve delivery controls
Keep current plans and release controls discoverable without losing historical evidence, and include the reviewed Windows start/stop workflow.
This commit is contained in:
+160
-58
@@ -1,63 +1,165 @@
|
||||
# Yuqei AI Contract Platform - one-click local startup.
|
||||
# Usage (repo root): powershell -ExecutionPolicy Bypass -File .\start-dev.ps1
|
||||
# Optional: add -WithReviewWorker to also start the contract review worker.
|
||||
# NOTE: keep this file ASCII-only. Windows PowerShell 5.1 misreads UTF-8
|
||||
# comments without BOM (GBK mojibake breaks the parser).
|
||||
|
||||
param(
|
||||
[switch]$WithReviewWorker
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
# Keep this script ASCII-only for Windows PowerShell 5.1.
|
||||
param([switch]$NoBrowser, [switch]$WithReviewWorker)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = $PSScriptRoot
|
||||
$python = Join-Path $root ".venv-project\Scripts\python.exe"
|
||||
$python = Join-Path $root '.venv-project\Scripts\python.exe'
|
||||
$apiDir = Join-Path $root 'yuqei-contract-platform\services\contract-api'
|
||||
$aiDir = Join-Path $root 'yuqei-ai-platform\services\ai-platform-api'
|
||||
$webDir = Join-Path $root 'yuqei-contract-platform\apps\contract-web'
|
||||
$logDir = Join-Path $root '.runtime\dev-logs'
|
||||
$stateFile = Join-Path $logDir 'processes.json'
|
||||
$records = @{}
|
||||
$savedEnvironment = @{}
|
||||
|
||||
if (-not (Test-Path $python)) {
|
||||
Write-Host "Python not found: $python" -ForegroundColor Red
|
||||
Write-Host "Rebuild the venv first: python -m venv .venv-project then .\.venv-project\Scripts\pip install -e yuqei-contract-platform\services\contract-api -e yuqei-ai-platform\services\ai-platform-api -e yuqei-ai-platform\services\worker -e yuqei-ai-sdk-python"
|
||||
exit 1
|
||||
function Set-ChildEnvironment([string]$Name, [string]$Value) {
|
||||
$savedEnvironment[$Name] = [Environment]::GetEnvironmentVariable($Name, 'Process')
|
||||
[Environment]::SetEnvironmentVariable($Name, $Value, 'Process')
|
||||
}
|
||||
|
||||
function Start-ServiceWindow($title, $workDir, $command) {
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit", "-Command",
|
||||
"`$Host.UI.RawUI.WindowTitle = '$title'; Set-Location '$workDir'; $command"
|
||||
function Get-Identity([int]$ProcessId) {
|
||||
$item = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
|
||||
if (-not $item) { return $null }
|
||||
return [pscustomobject]@{ Pid = [int]$item.ProcessId; ParentPid = [int]$item.ParentProcessId; CreatedUtc = $item.CreationDate.ToUniversalTime().ToString('o'); ExecutablePath = [string]$item.ExecutablePath; CommandLine = [string]$item.CommandLine }
|
||||
}
|
||||
function Find-ProjectOwner($Identity, [string]$Marker) {
|
||||
# Windows venv python.exe launches the base interpreter as a child.
|
||||
for ($level = 0; $Identity -and $level -lt 8; $level++) {
|
||||
if ($Identity.CommandLine.IndexOf($Marker, [StringComparison]::OrdinalIgnoreCase) -lt 0) { return $null }
|
||||
if ($Identity.CommandLine.IndexOf($root, [StringComparison]::OrdinalIgnoreCase) -ge 0) { return $Identity }
|
||||
$parent = Get-Identity $Identity.ParentPid
|
||||
if (-not $parent -or [datetime]::Parse($parent.CreatedUtc) -gt [datetime]::Parse($Identity.CreatedUtc)) { return $null }
|
||||
$Identity = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Test-Recorded($Record) {
|
||||
if (-not $Record) { return $false }
|
||||
$actual = Get-Identity $Record.Pid
|
||||
return ($actual -and $actual.CreatedUtc -eq $Record.CreatedUtc -and $actual.ExecutablePath -eq $Record.ExecutablePath -and $actual.CommandLine -eq $Record.CommandLine)
|
||||
}
|
||||
function Save-State {
|
||||
$entries = @($records.GetEnumerator() | ForEach-Object { [pscustomobject]@{ Name = $_.Key; Process = $_.Value } })
|
||||
[pscustomobject]@{ Root = $root; Services = $entries } | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $stateFile -Encoding UTF8
|
||||
}
|
||||
function Test-Endpoint($Service) {
|
||||
try {
|
||||
if ($Service.Port -eq 3000) {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri $Service.Url -TimeoutSec 8
|
||||
return ($response.StatusCode -eq 200 -and $response.Content -match 'login-auth-page')
|
||||
}
|
||||
$response = Invoke-RestMethod -Uri $Service.Url -TimeoutSec 5
|
||||
return ($response.status -eq 'ok' -and $response.service -eq $Service.Name)
|
||||
} catch { return $false }
|
||||
}
|
||||
function Find-Running($Service) {
|
||||
if ($Service.Port) {
|
||||
$listeners = @(Get-NetTCPConnection -State Listen -LocalPort $Service.Port -ErrorAction SilentlyContinue)
|
||||
if ($listeners.Count -eq 0) { return $false }
|
||||
foreach ($listener in $listeners) {
|
||||
$identity = Find-ProjectOwner (Get-Identity $listener.OwningProcess) $Service.Marker
|
||||
if (-not $identity) {
|
||||
throw "Port $($Service.Port) belongs to another process. Resolve the conflict before starting."
|
||||
}
|
||||
}
|
||||
if (-not (Test-Endpoint $Service)) { throw "$($Service.Name) occupies port $($Service.Port) but is not healthy. Check $logDir and run stop-dev.ps1 before retrying." }
|
||||
if (-not (Test-Recorded $records[$Service.Name])) { $records[$Service.Name] = $identity; Save-State }
|
||||
Write-Host "Already ready: $($Service.Name)"
|
||||
return $true
|
||||
}
|
||||
if (Test-Recorded $records[$Service.Name]) { Write-Host "Already running: $($Service.Name)"; return $true }
|
||||
$existing = @(Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and $_.CommandLine.IndexOf($root, [StringComparison]::OrdinalIgnoreCase) -ge 0 -and $_.CommandLine -match (' -m ' + [regex]::Escape($Service.Marker) + '(\s|$)') })
|
||||
if ($existing.Count -gt 0) {
|
||||
$records[$Service.Name] = Get-Identity $existing[0].ProcessId
|
||||
Save-State
|
||||
Write-Host "Already running: $($Service.Name)"
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
function Start-LocalService($Service) {
|
||||
$stdout = Join-Path $logDir "$($Service.Name).out.log"
|
||||
$stderr = Join-Path $logDir "$($Service.Name).err.log"
|
||||
$process = Start-Process -FilePath $Service.Exe -ArgumentList $Service.Arguments -WorkingDirectory $Service.Directory -WindowStyle Hidden -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
||||
Start-Sleep -Milliseconds 400
|
||||
$identity = Get-Identity $process.Id
|
||||
if (-not $identity) { throw "$($Service.Name) exited. Check $stderr" }
|
||||
$records[$Service.Name] = $identity
|
||||
Save-State
|
||||
Write-Host "Started: $($Service.Name) (PID $($process.Id))"
|
||||
}
|
||||
function Invoke-Migration([string]$Name, [string]$Directory) {
|
||||
Write-Host "Checking database migrations: $Name"
|
||||
$stdout = Join-Path $logDir "$Name-migration.out.log"
|
||||
$stderr = Join-Path $logDir "$Name-migration.err.log"
|
||||
$process = Start-Process -FilePath $python -ArgumentList @('-m', 'alembic', 'upgrade', 'head') -WorkingDirectory $Directory -WindowStyle Hidden -PassThru -Wait -RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
||||
if ($process.ExitCode -ne 0) { throw "$Name migration failed (exit $($process.ExitCode)). Check $stderr. Startup stopped." }
|
||||
}
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $python)) { throw "Python environment missing: $python" }
|
||||
& $python -c 'import sys; assert sys.version_info >= (3, 12); import uvicorn, alembic, dotenv' 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Project Python is unavailable. Repair .venv-project and install project dependencies first.' }
|
||||
$node = (Get-Command node -ErrorAction Stop).Source
|
||||
$next = Join-Path $webDir 'node_modules\next\dist\bin\next'
|
||||
if (-not (Test-Path -LiteralPath $next)) { $next = Join-Path $root 'node_modules\next\dist\bin\next' }
|
||||
if (-not (Test-Path -LiteralPath $next)) { throw 'Next.js dependencies are missing. Install workspace npm dependencies first.' }
|
||||
$webEnv = Join-Path $webDir '.env.local'
|
||||
if (-not (Test-Path -LiteralPath $webEnv)) { throw "Web configuration missing: $webEnv" }
|
||||
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||
if (Test-Path -LiteralPath $stateFile) {
|
||||
$state = Get-Content -Raw -LiteralPath $stateFile | ConvertFrom-Json
|
||||
if ($state.Root -ne $root) { throw "Process state belongs to another directory: $stateFile" }
|
||||
foreach ($entry in $state.Services) { $records[$entry.Name] = $entry.Process }
|
||||
}
|
||||
# Read only proxy secrets, without printing them or replacing service .env settings.
|
||||
$readSecrets = "import json,sys; from dotenv import dotenv_values; e=dotenv_values(sys.argv[1]); print(json.dumps({k:e.get(k) for k in ('CONTRACT_API_PROXY_SECRET','AI_PLATFORM_PROXY_SECRET')}))"
|
||||
$secretJson = & $python -c $readSecrets $webEnv
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not read web proxy authentication configuration.' }
|
||||
$secrets = $secretJson | ConvertFrom-Json
|
||||
if (-not $secrets.CONTRACT_API_PROXY_SECRET -or -not $secrets.AI_PLATFORM_PROXY_SECRET) { throw 'CONTRACT_API_PROXY_SECRET and AI_PLATFORM_PROXY_SECRET are required in web .env.local.' }
|
||||
Set-ChildEnvironment 'CONTRACT_API_WEB_PROXY_SHARED_SECRET' $secrets.CONTRACT_API_PROXY_SECRET
|
||||
Set-ChildEnvironment 'CONTRACT_API_AI_PLATFORM_SERVICE_KEY' $secrets.AI_PLATFORM_PROXY_SECRET
|
||||
Set-ChildEnvironment 'AI_PLATFORM_SERVICE_SHARED_SECRET' $secrets.AI_PLATFORM_PROXY_SECRET
|
||||
Set-ChildEnvironment 'CONTRACT_API_DRAFTING_WORKER_ENABLED' 'true'
|
||||
Set-ChildEnvironment 'PYTHONUNBUFFERED' '1'
|
||||
$bypassProxy = @($env:NO_PROXY -split ','; 'localhost'; '127.0.0.1'; '::1') | Where-Object { $_ } | Select-Object -Unique
|
||||
Set-ChildEnvironment 'NO_PROXY' ($bypassProxy -join ',')
|
||||
Set-ChildEnvironment 'PYTHONPATH' ((Join-Path $apiDir 'src'), (Join-Path $aiDir 'src'), (Join-Path $root 'yuqei-ai-sdk-python\src') -join ';')
|
||||
$services = @(
|
||||
@{ Name = 'ai-platform-api'; Exe = $python; Directory = $aiDir; Marker = 'yuqei_ai_platform_api.main:app'; Port = 8101; Url = 'http://127.0.0.1:8101/health'; Arguments = '-m uvicorn yuqei_ai_platform_api.main:app --app-dir src --host 127.0.0.1 --port 8101' },
|
||||
@{ Name = 'contract-api'; Exe = $python; Directory = $apiDir; Marker = 'yuqei_contract_api.main:app'; Port = 8100; Url = 'http://127.0.0.1:8100/health'; Arguments = '-m uvicorn yuqei_contract_api.main:app --app-dir src --host 127.0.0.1 --port 8100' },
|
||||
@{ Name = 'contract-web'; Exe = $node; Directory = $webDir; Marker = 'next'; Port = 3000; Url = 'http://127.0.0.1:3000/login'; Arguments = ('"' + $next + '" dev --hostname 127.0.0.1 --port 3000') },
|
||||
@{ Name = 'contract-review-worker'; Exe = $python; Directory = $apiDir; Marker = 'yuqei_contract_api.review_worker'; Port = 0; Arguments = '-m yuqei_contract_api.review_worker' },
|
||||
@{ Name = 'drafting-continuation-worker'; Exe = $python; Directory = $apiDir; Marker = 'yuqei_contract_api.drafting_continuation_worker'; Port = 0; Arguments = '-m yuqei_contract_api.drafting_continuation_worker' }
|
||||
)
|
||||
$running = @{}
|
||||
foreach ($service in $services) { $running[$service.Name] = Find-Running $service }
|
||||
# Both schemas must be current before any new API or worker starts.
|
||||
Invoke-Migration 'ai-platform-api' $aiDir
|
||||
Invoke-Migration 'contract-api' $apiDir
|
||||
foreach ($service in $services) { if (-not $running[$service.Name]) { Start-LocalService $service } }
|
||||
foreach ($service in $services) {
|
||||
if ($service.Port) {
|
||||
$deadline = (Get-Date).AddSeconds(120)
|
||||
while (-not (Test-Endpoint $service)) {
|
||||
if (-not (Test-Recorded $records[$service.Name])) { throw "$($service.Name) exited. Check $logDir." }
|
||||
if ((Get-Date) -gt $deadline) { throw "$($service.Name) did not become ready. Check $logDir." }
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
} elseif (-not (Test-Recorded $records[$service.Name])) { throw "$($Service.Name) exited. Check $logDir." }
|
||||
}
|
||||
Write-Host 'Ready: http://localhost:3000/login' -ForegroundColor Green
|
||||
Write-Host 'APIs: http://127.0.0.1:8100/health and http://127.0.0.1:8101/health'
|
||||
Write-Host "Review and drafting workers are running. Logs: $logDir"
|
||||
Write-Host 'Stop: powershell -ExecutionPolicy Bypass -File .\stop-dev.ps1'
|
||||
if (-not $NoBrowser) {
|
||||
$chrome = Get-Command chrome.exe -ErrorAction SilentlyContinue
|
||||
$chromePath = if ($chrome) { $chrome.Source } else { Join-Path $env:ProgramFiles 'Google\Chrome\Application\chrome.exe' }
|
||||
if (Test-Path -LiteralPath $chromePath) { Start-Process -FilePath $chromePath -ArgumentList 'http://localhost:3000/login' }
|
||||
else { Start-Process 'http://localhost:3000/login' }
|
||||
}
|
||||
} catch {
|
||||
Write-Host ('Startup failed: ' + $_.Exception.Message) -ForegroundColor Red
|
||||
Write-Host 'Any services already started remain recorded. Inspect logs or run stop-dev.ps1 before retrying.'
|
||||
exit 1
|
||||
} finally {
|
||||
foreach ($name in $savedEnvironment.Keys) { [Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name], 'Process') }
|
||||
}
|
||||
|
||||
# 0) Database migration (idempotent; skips applied migrations; never blocks startup)
|
||||
Write-Host "Running database migrations (alembic upgrade head)..." -ForegroundColor Cyan
|
||||
$apiDir = Join-Path $root "yuqei-contract-platform\services\contract-api"
|
||||
Push-Location $apiDir
|
||||
& $python -m alembic upgrade head
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Database migrations up to date." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Migration check did not complete (services will still start; re-run manually when the DB is reachable)." -ForegroundColor Yellow
|
||||
}
|
||||
Pop-Location
|
||||
|
||||
# 1) Contract API (port 8100)
|
||||
Start-ServiceWindow "contract-api :8100" (Join-Path $root "yuqei-contract-platform\services\contract-api") `
|
||||
"& '$python' -m uvicorn yuqei_contract_api.main:app --app-dir src --reload --port 8100"
|
||||
|
||||
# 2) AI Platform API (port 8101)
|
||||
Start-ServiceWindow "ai-platform-api :8101" (Join-Path $root "yuqei-ai-platform\services\ai-platform-api") `
|
||||
"& '$python' -m uvicorn yuqei_ai_platform_api.main:app --app-dir src --reload --port 8101"
|
||||
|
||||
# 3) Contract Web (port 3000)
|
||||
Start-ServiceWindow "contract-web :3000" (Join-Path $root "yuqei-contract-platform\apps\contract-web") `
|
||||
"npm run dev"
|
||||
|
||||
# 4) Optional: contract review worker
|
||||
if ($WithReviewWorker) {
|
||||
$workerDir = Join-Path $root "yuqei-contract-platform\services\contract-api"
|
||||
Start-ServiceWindow "contract-review-worker" $workerDir `
|
||||
"`$env:PYTHONPATH='src;..\..\..\yuqei-ai-sdk-python\src'; & '$python' -m yuqei_contract_api.review_worker"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Started: Contract API(8100) / AI Platform(8101) / Web(3000)" -ForegroundColor Green
|
||||
if ($WithReviewWorker) { Write-Host "Started: contract review worker" -ForegroundColor Green }
|
||||
Start-Sleep -Seconds 8
|
||||
Start-Process "http://localhost:3000"
|
||||
|
||||
Reference in New Issue
Block a user