Files
Edu/scripts/start-all.ps1
SpecialX 9cedf0c437 feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等
- Tailwind v4 + @theme inline,移除 tailwind.config.js
- React 19 use() + Suspense 流式渲染,首屏骨架秒出
- 三级错误边界:Route → Section → Widget 层层兜底
- 错误上报:useErrorReport → sendBeacon → /api/log mock 端点
- 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围
- 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99%
- notify 统一 Toast 封装,禁止业务直接 import sonner
- PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套)

验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
2026-07-17 16:10:05 +08:00

482 lines
22 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
Edu v2.1 一键启动脚本
.DESCRIPTION
启动顺序:
1. Docker 基础设施MySQL/Redis/Kafka/ClickHouse/Neo4j/ES/observability
2. Apollo RouterDocker 容器GraphQL 联邦网关)
3. 应用服务(本地 pnpm dev / uvicorn / go run
应用服务列表v2.1
- NestJS: iam, config-service, classes, core-edu, content, msg
- Python: data-ana, ai
- Go: api-gateway, push-gateway
- Next.js: portal-shell (前端)
.PARAMETER SkipInfra
跳过 Docker 基础设施启动(已确认 infra 在运行时使用)
.PARAMETER SkipApps
仅启动基础设施,不启动应用服务
.PARAMETER Force
自动杀掉占用端口的进程(不交互确认)
.PARAMETER Profile
Docker Compose profile 层级default | p3 | p4 | p5 | p6默认 p6=全部)
.EXAMPLE
.\scripts\start-all.ps1 # 启动全部
.\scripts\start-all.ps1 -SkipInfra # 仅启动应用服务
.\scripts\start-all.ps1 -SkipApps # 仅启动基础设施
.\scripts\start-all.ps1 -Force # 自动杀端口占用
#>
param(
[switch]$SkipInfra,
[switch]$SkipApps,
[switch]$SkipRouter,
[switch]$Force,
[string]$Profile = "p6"
)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Edu v2.1 Start All Services" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# ===== 0. 加载 .env 文件 =====
Write-Host "[0/7] Loading .env..." -ForegroundColor Yellow
$envFile = Join-Path $ProjectRoot ".env"
if (-not (Test-Path $envFile)) {
Write-Host " [FAIL] .env not found at $envFile" -ForegroundColor Red
exit 1
}
$envCount = 0
$envLines = Get-Content $envFile
foreach ($line in $envLines) {
$line = $line.Trim()
if (-not $line -or $line.StartsWith("#") -or -not $line.Contains("=")) { continue }
$idx = $line.IndexOf("=")
$key = $line.Substring(0, $idx).Trim()
$val = $line.Substring($idx + 1).Trim()
# 移除可能的引号
if ($val.StartsWith('"') -and $val.EndsWith('"')) { $val = $val.Substring(1, $val.Length - 2) }
if ($val.StartsWith("'") -and $val.EndsWith("'")) { $val = $val.Substring(1, $val.Length - 2) }
[Environment]::SetEnvironmentVariable($key, $val, "Process")
$envCount++
}
# 补充 .env 中未定义但 dev 模式需要的变量
if (-not $env:DEV_MODE) { $env:DEV_MODE = "true" }
if (-not $env:OTEL_EXPORTER_OTLP_ENDPOINT) { $env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318" }
if (-not $env:NEO4J_URI) { $env:NEO4J_URI = "bolt://localhost:7687" }
if (-not $env:NEO4J_USER) { $env:NEO4J_USER = "neo4j" }
if (-not $env:CLICKHOUSE_URL) { $env:CLICKHOUSE_URL = "http://localhost:8123" }
if (-not $env:ES_URL) { $env:ES_URL = "http://localhost:9200" }
if (-not $env:ROUTER_AUTH_SECRET) { $env:ROUTER_AUTH_SECRET = "dev-router-secret" }
Write-Host " [OK] Loaded $envCount env vars from .env (DEV_MODE=$($env:DEV_MODE))" -ForegroundColor Green
Write-Host ""
# ===== 1. 启动 Docker 基础设施 =====
# 混合部署仅启动基础设施容器MySQL/Redis/Kafka/ClickHouse/Neo4j/ES/Debezium/observability
# Temporal 暂不启动auto-setup 镜像需调整配置,非关键路径)
# 应用服务config-service/apollo-router/portal-shell本地运行避免 Docker 构建失败
if (-not $SkipInfra) {
Write-Host "[1/7] Starting Docker infrastructure..." -ForegroundColor Yellow
Push-Location "$ProjectRoot\infra"
# docker compose 把进度输出到 stderr临时放宽 ErrorActionPreference 避免误判
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
# 启用 p3+p5 profile显式启动基础设施服务跳过 temporal 和需构建镜像的 app
$composeCmd = "docker compose -f docker-compose.yml --profile p3 --profile p5 up -d mysql redis kafka zookeeper clickhouse neo4j elasticsearch debezium-connect"
Write-Host " $composeCmd" -ForegroundColor DarkGray
Invoke-Expression $composeCmd 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host " [WARN] Bulk start had issues, retrying individually..." -ForegroundColor Yellow
docker compose -f docker-compose.yml up -d mysql redis 2>&1 | Out-Null
docker compose -f docker-compose.yml --profile p3 up -d kafka zookeeper clickhouse neo4j debezium-connect 2>&1 | Out-Null
docker compose -f docker-compose.yml --profile p5 up -d elasticsearch 2>&1 | Out-Null
}
# 可观测性栈(独立 profile
docker compose -f docker-compose.yml --profile observability up -d 2>&1 | Out-Null
Pop-Location
$ErrorActionPreference = $prevEAP
Write-Host " Waiting 20s for containers to initialize..." -ForegroundColor Gray
Start-Sleep -Seconds 20
Write-Host " [OK] Docker infrastructure started" -ForegroundColor Green
} catch {
Pop-Location
$ErrorActionPreference = $prevEAP
Write-Host " [FAIL] $_" -ForegroundColor Red
exit 1
}
} else {
Write-Host "[1/7] Skipping Docker infrastructure (-SkipInfra)" -ForegroundColor Gray
}
Write-Host ""
# ===== 2. 基础设施健康检查 =====
Write-Host "[2/7] Checking infrastructure health..." -ForegroundColor Yellow
$infraServices = @(
@{Name="MySQL"; Container="edu-mysql"},
@{Name="Redis"; Container="edu-redis"},
@{Name="Kafka"; Container="edu-kafka"},
@{Name="Zookeeper"; Container="edu-zookeeper"},
@{Name="ClickHouse"; Container="edu-clickhouse"},
@{Name="Neo4j"; Container="edu-neo4j"},
@{Name="Elasticsearch"; Container="edu-es"},
@{Name="Debezium"; Container="edu-debezium"},
@{Name="Jaeger"; Container="edu-jaeger"},
@{Name="Prometheus"; Container="edu-prometheus"},
@{Name="Grafana"; Container="edu-grafana"}
)
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$infraOkCount = 0
foreach ($svc in $infraServices) {
$running = docker inspect -f '{{.State.Running}}' $svc.Container 2>$null
if ($running -ne "true") {
Write-Host " [SKIP] $($svc.Name) not running (may be optional)" -ForegroundColor Gray
continue
}
$health = docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' $svc.Container 2>$null
if ($health -eq "healthy" -or $health -eq "") {
Write-Host " [OK] $($svc.Name)" -ForegroundColor Green
$infraOkCount++
} elseif ($health -eq "starting") {
Write-Host " [WAIT] $($svc.Name) still starting..." -ForegroundColor Yellow
} else {
Write-Host " [WARN] $($svc.Name) health=$health" -ForegroundColor Yellow
}
}
$ErrorActionPreference = $prevEAP
Write-Host " $infraOkCount/$($infraServices.Count) infrastructure services OK" -ForegroundColor Green
Write-Host ""
if ($SkipApps) {
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Infrastructure only (-SkipApps). Done." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
exit 0
}
# ===== 3. 端口冲突检查 =====
Write-Host "[3/7] Checking port conflicts..." -ForegroundColor Yellow
$portMap = @{
3001="classes"; 3002="iam"; 3004="core-edu"; 3005="content"
3006="data-ana"; 3007="msg"; 3008="ai"; 3011="config-service"
8080="api-gateway"; 8081="push-gateway"; 4010="portal-shell"
}
$conflicts = @()
foreach ($port in $portMap.Keys | Sort-Object) {
$conn = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
if ($conn) {
$svcName = $portMap[$port]
$procId = $conn[0].OwningProcess
$procName = ""
try { $procName = (Get-Process -Id $procId -ErrorAction Stop).ProcessName } catch {}
Write-Host " [WARN] Port $port ($svcName) occupied by PID $procId ($procName)" -ForegroundColor Yellow
$conflicts += [PSCustomObject]@{Port=$port; Service=$svcName; PID=$procId; Process=$procName}
}
}
if ($conflicts.Count -gt 0) {
Write-Host " $($conflicts.Count) port(s) in use." -ForegroundColor Yellow
$shouldKill = $false
if ($Force) {
$shouldKill = $true
} else {
$answer = Read-Host " Kill existing processes and continue? (y/N)"
if ($answer -eq "y" -or $answer -eq "Y") { $shouldKill = $true }
}
if ($shouldKill) {
foreach ($c in $conflicts) {
try {
Stop-Process -Id $c.PID -Force -ErrorAction Stop
Write-Host " [OK] Killed PID $($c.PID) on port $($c.Port)" -ForegroundColor Green
Start-Sleep -Milliseconds 500
} catch {
Write-Host " [WARN] Cannot kill PID $($c.PID): $($_.Exception.Message)" -ForegroundColor Yellow
}
}
Start-Sleep -Seconds 2
} else {
Write-Host " Aborting." -ForegroundColor Red
exit 1
}
} else {
Write-Host " [OK] All app ports are free" -ForegroundColor Green
}
Write-Host ""
# ===== 4. 确保 shared-ts 已编译 =====
Write-Host "[4/7] Ensuring shared-ts is built..." -ForegroundColor Yellow
$sharedTsPath = Join-Path $ProjectRoot "packages\shared-ts\dist"
if (-not (Test-Path $sharedTsPath)) {
Write-Host " Building @edu/shared-ts..." -ForegroundColor Gray
pnpm --filter @edu/shared-ts build 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] shared-ts built" -ForegroundColor Green
} else {
Write-Host " [FAIL] shared-ts build failed" -ForegroundColor Red
exit 1
}
} else {
Write-Host " [OK] shared-ts already built" -ForegroundColor Green
}
Write-Host ""
# ===== 5. 清理 NestJS 增量缓存 =====
Write-Host "[5/7] Cleaning NestJS tsbuildinfo cache..." -ForegroundColor Yellow
$nestjsDirs = @("iam", "config-service", "classes", "core-edu", "content", "msg")
foreach ($dir in $nestjsDirs) {
$svcPath = Join-Path $ProjectRoot "services\$dir"
if (Test-Path $svcPath) {
Get-ChildItem -Path $svcPath -Filter "*.tsbuildinfo" -Recurse -ErrorAction SilentlyContinue |
Remove-Item -Force -ErrorAction SilentlyContinue
}
}
Write-Host " [OK] Cache cleaned" -ForegroundColor Green
Write-Host ""
# ===== 6. 启动应用服务 =====
Write-Host "[6/7] Starting application services (v2.1)..." -ForegroundColor Yellow
# Python 服务环境变量
$pyEnv = @{
CLICKHOUSE_HOST = "localhost"
CLICKHOUSE_PORT = "8123"
CLICKHOUSE_USER = "default"
CLICKHOUSE_PASSWORD = "clickhouse"
CLICKHOUSE_DATABASE = "edu_analytics"
CLICKHOUSE_URL = "http://localhost:8123"
KAFKA_BROKERS = "localhost:9092"
OTEL_ENDPOINT = "http://localhost:4318"
OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"
DEV_MODE = "true"
NEO4J_URI = "bolt://localhost:7687"
NEO4J_USER = "neo4j"
NEO4J_PASSWORD = "changeme"
ES_URL = "http://localhost:9200"
}
# Go 服务环境变量默认值(在当前进程设置,子进程会继承)
if (-not $env:CLASSES_SERVICE_URL) { $env:CLASSES_SERVICE_URL = "http://localhost:3001" }
if (-not $env:IAM_SERVICE_URL) { $env:IAM_SERVICE_URL = "http://localhost:3002" }
if (-not $env:APOLLO_ROUTER_URL) { $env:APOLLO_ROUTER_URL = "http://localhost:3000" }
if (-not $env:CORE_EDU_SERVICE_URL) { $env:CORE_EDU_SERVICE_URL = "http://localhost:3004" }
if (-not $env:CONTENT_SERVICE_URL) { $env:CONTENT_SERVICE_URL = "http://localhost:3005" }
if (-not $env:DATA_ANA_SERVICE_URL) { $env:DATA_ANA_SERVICE_URL = "http://localhost:3006" }
if (-not $env:MSG_SERVICE_URL) { $env:MSG_SERVICE_URL = "http://localhost:3007" }
if (-not $env:AI_SERVICE_URL) { $env:AI_SERVICE_URL = "http://localhost:3008" }
if (-not $env:CORS_ORIGINS) { $env:CORS_ORIGINS = "http://localhost:4000,http://localhost:4010" }
if (-not $env:JWKS_URL) { $env:JWKS_URL = "http://localhost:3002/v1/iam/.well-known/jwks.json" }
if (-not $env:PUSH_INTERNAL_TOKEN) { $env:PUSH_INTERNAL_TOKEN = "edu-internal-token" }
$services = @(
@{Title="edu-app-iam"; Cmd="pnpm"; Args=@("--filter","@edu/iam-service","exec","nest","start"); Dir="$ProjectRoot"},
@{Title="edu-app-config-service"; Cmd="pnpm"; Args=@("--filter","@edu/config-service","exec","nest","start"); Dir="$ProjectRoot"},
@{Title="edu-app-classes"; Cmd="pnpm"; Args=@("--filter","@edu/classes-service","exec","nest","start"); Dir="$ProjectRoot"},
@{Title="edu-app-core-edu"; Cmd="pnpm"; Args=@("--filter","@edu/core-edu-service","exec","nest","start"); Dir="$ProjectRoot"},
@{Title="edu-app-content"; Cmd="pnpm"; Args=@("--filter","@edu/content-service","exec","nest","start"); Dir="$ProjectRoot"},
@{Title="edu-app-msg"; Cmd="pnpm"; Args=@("--filter","@edu/msg-service","exec","nest","start"); Dir="$ProjectRoot"},
@{Title="edu-app-data-ana"; Cmd="uv"; Args=@("run","uvicorn","data_ana.main:app","--app-dir","src","--host","0.0.0.0","--port","3006","--reload"); Dir="$ProjectRoot\services\data-ana"; PyEnv=$true},
@{Title="edu-app-ai"; Cmd="uv"; Args=@("run","uvicorn","ai.main:app","--app-dir","src","--host","0.0.0.0","--port","3008","--reload"); Dir="$ProjectRoot\services\ai"; PyEnv=$true},
@{Title="edu-app-api-gateway"; Cmd="go"; Args=@("run","."); Dir="$ProjectRoot\services\api-gateway"; GoEnv=$true},
@{Title="edu-app-push-gateway"; Cmd="go"; Args=@("run","."); Dir="$ProjectRoot\services\push-gateway"; GoEnv=$true},
@{Title="edu-app-portal-shell"; Cmd="pnpm"; Args=@("--filter","@edu/portal-shell","dev"); Dir="$ProjectRoot"}
)
$startedCount = 0
foreach ($svc in $services) {
$psCmd = "Set-Location '$($svc.Dir)'; "
# Python 服务额外设置环境变量
if ($svc.PyEnv) {
foreach ($kv in $pyEnv.GetEnumerator()) {
$psCmd += "`$env:$($kv.Key)='$($kv.Value)'; "
}
}
# Go 服务确保 PATH 包含 Go bin
if ($svc.GoEnv) {
$psCmd += "`$env:Path = 'C:\Program Files\Go\bin;' + `$env:Path; "
}
$cmdStr = "$($svc.Cmd) $($svc.Args -join ' ')"
$psCmd += "$cmdStr; Write-Host ''; Write-Host 'Service stopped. Press any key to close...' -ForegroundColor Yellow; `$null = `$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')"
Start-Process -FilePath "powershell" -ArgumentList "-NoExit","-Command",$psCmd -WindowStyle Normal | Out-Null
Write-Host " [START] $($svc.Title)" -ForegroundColor Green
$startedCount++
Start-Sleep -Milliseconds 800
}
Write-Host ""
Write-Host " $startedCount services launched. Waiting 60s for init..." -ForegroundColor Yellow
Start-Sleep -Seconds 60
# ===== 7. 健康检查 =====
Write-Host "[7/7] Health check..." -ForegroundColor Yellow
Write-Host ""
$healthServices = @(
@{Name="iam"; Url="http://localhost:3002/healthz"; Critical=$true},
@{Name="config-service"; Url="http://localhost:3011/healthz"; Critical=$true},
@{Name="classes"; Url="http://localhost:3001/healthz"; Critical=$false},
@{Name="core-edu"; Url="http://localhost:3004/healthz"; Critical=$false},
@{Name="content"; Url="http://localhost:3005/healthz"; Critical=$false},
@{Name="data-ana"; Url="http://localhost:3006/healthz"; Critical=$false},
@{Name="msg"; Url="http://localhost:3007/healthz"; Critical=$false},
@{Name="ai"; Url="http://localhost:3008/healthz"; Critical=$false},
@{Name="api-gateway"; Url="http://localhost:8080/healthz"; Critical=$false},
@{Name="push-gateway"; Url="http://localhost:8081/healthz"; Critical=$false},
@{Name="portal-shell"; Url="http://localhost:4010/api/health"; Critical=$true}
)
$okCount = 0
$failCount = 0
$criticalFail = 0
$failedServices = @()
foreach ($svc in $healthServices) {
$retries = 0
$maxRetries = 5
$success = $false
while ($retries -lt $maxRetries -and -not $success) {
try {
$null = Invoke-RestMethod -Uri $svc.Url -Method Get -TimeoutSec 5 -ErrorAction Stop
$tag = if ($svc.Critical) { "CRITICAL" } else { "optional" }
Write-Host " [OK] $($svc.Name) ($tag)" -ForegroundColor Green
$success = $true
$okCount++
} catch {
$retries++
if ($retries -lt $maxRetries) { Start-Sleep -Seconds 4 }
}
}
if (-not $success) {
$tag = if ($svc.Critical) { "CRITICAL" } else { "optional" }
Write-Host " [FAIL] $($svc.Name) ($tag)" -ForegroundColor Red
$failCount++
$failedServices += $svc.Name
if ($svc.Critical) { $criticalFail++ }
}
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Result: [OK] $okCount / [FAIL] $failCount" -ForegroundColor Cyan
if ($criticalFail -gt 0) {
Write-Host " CRITICAL failures: $criticalFail" -ForegroundColor Red
}
Write-Host "========================================" -ForegroundColor Cyan
if ($failCount -gt 0) {
Write-Host ""
Write-Host "Failed: $($failedServices -join ', ')" -ForegroundColor Yellow
Write-Host ""
Write-Host "Troubleshooting:" -ForegroundColor Yellow
Write-Host " 1. Check service window for errors" -ForegroundColor White
Write-Host " 2. Health check: .\scripts\health-check.ps1" -ForegroundColor White
Write-Host " 3. Stop & retry: .\scripts\stop-all.ps1 -KillByPort then .\scripts\start-all.ps1 -SkipInfra -Force" -ForegroundColor White
Write-Host ""
}
# ===== 8. 启动 Apollo RouterDocker 容器) =====
# Apollo Router 需要所有 Federation 2 子图iam/core-edu/content/msg/config就绪后才能 compose supergraph
# 子图在 host 上运行router 容器通过 host.docker.internal 访问
if (-not $SkipRouter -and -not $SkipApps) {
Write-Host "[8/8] Starting Apollo Router (Docker)..." -ForegroundColor Yellow
$routerImage = "edu/apollo-router:dev"
$routerDir = Join-Path $ProjectRoot "infra\apollo-router"
# 检查镜像是否存在,不存在则提示
$imageExists = docker image inspect $routerImage 2>$null
if (-not $imageExists) {
Write-Host " [WARN] Image $routerImage not found. Building..." -ForegroundColor Yellow
Push-Location $routerDir
try {
docker build -t $routerImage . 2>&1 | Out-Host
if ($LASTEXITCODE -ne 0) {
Write-Host " [FAIL] Apollo Router image build failed" -ForegroundColor Red
Write-Host " Skip with -SkipRouter. Portal-shell will fall back to config-service direct connection." -ForegroundColor Yellow
Pop-Location
$SkipRouter = $true
} else {
Write-Host " [OK] Image built" -ForegroundColor Green
}
} catch {
Pop-Location
Write-Host " [FAIL] Build error: $_" -ForegroundColor Red
$SkipRouter = $true
}
if (-not $SkipRouter) { Pop-Location }
}
if (-not $SkipRouter) {
# 移除旧容器
docker rm -f edu-apollo-router 2>&1 | Out-Null
# 检查 3000 端口是否被占用
$port3000 = Get-NetTCPConnection -LocalPort 3000 -State Listen -ErrorAction SilentlyContinue
if ($port3000) {
Write-Host " [WARN] Port 3000 already in use, skipping router startup" -ForegroundColor Yellow
Write-Host " (use docker rm -f edu-apollo-router to clean up old container)" -ForegroundColor Gray
} else {
$proxyVar = $env:HTTP_PROXY
if (-not $proxyVar) { $proxyVar = "http://host.docker.internal:7897" }
$dockerArgs = @(
"run","-d","--name","edu-apollo-router",
"--add-host=host.docker.internal:host-gateway",
"-p","3000:3000","-p","8088:8088",
"-v","$routerDir\dev-supergraph.yaml:/dist/supergraph.yaml",
"-v","$routerDir\dev-entrypoint.sh:/dist/entrypoint.sh",
"-v","$routerDir\router.yaml:/dist/configuration.yaml",
"-e","ROUTER_AUTH_SECRET=dev-router-secret",
"-e","APOLLO_ELV2_LICENSE=accept",
"-e","HTTP_PROXY=$proxyVar",
"-e","HTTPS_PROXY=$proxyVar",
"-e","http_proxy=$proxyVar",
"-e","https_proxy=$proxyVar",
"-e","NO_PROXY=localhost,127.0.0.1,host.docker.internal",
$routerImage,
"/dist/entrypoint.sh"
)
Write-Host " Starting container..." -ForegroundColor Gray
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
& docker @dockerArgs 2>&1 | Out-Null
$ErrorActionPreference = $prevEAP
if ($LASTEXITCODE -eq 0) {
Write-Host " [START] edu-apollo-router (waiting 25s for supergraph compose)..." -ForegroundColor Green
Start-Sleep -Seconds 25
# 验证健康
try {
$null = Invoke-RestMethod -Uri "http://localhost:8088/health" -Method Get -TimeoutSec 5 -ErrorAction Stop
Write-Host " [OK] Apollo Router healthy (GraphQL: http://localhost:3000/graphql)" -ForegroundColor Green
} catch {
Write-Host " [WARN] Router not yet healthy, check: docker logs edu-apollo-router" -ForegroundColor Yellow
}
} else {
Write-Host " [FAIL] docker run failed" -ForegroundColor Red
Write-Host " Portal-shell will fall back to config-service direct connection (http://localhost:3011)" -ForegroundColor Yellow
}
}
}
} else {
if ($SkipRouter) {
Write-Host "[8/8] Skipping Apollo Router (-SkipRouter)" -ForegroundColor Gray
} else {
Write-Host "[8/8] Skipping Apollo Router (-SkipApps: no subgraphs running)" -ForegroundColor Gray
}
}
Write-Host ""
Write-Host "Access points:" -ForegroundColor Yellow
Write-Host " Portal Shell (前端): http://localhost:4010" -ForegroundColor White
Write-Host " Apollo Router (GraphQL): http://localhost:3000" -ForegroundColor White
Write-Host " API Gateway: http://localhost:8080" -ForegroundColor White
Write-Host " IAM Health: http://localhost:3002/healthz" -ForegroundColor White
Write-Host " Grafana: http://localhost:3030" -ForegroundColor White
Write-Host " Jaeger: http://localhost:16686" -ForegroundColor White
Write-Host ""