feat(feishu): 添加飞书入站事件inbox和混合数据库协调功能 - 实现飞书入站事件持久化inbox机制,支持状态管理、租约锁定和重试退避 - 添加混合数据库基线协调工具,确保平台PostgreSQL结构安全对齐 - 增加运行组件心跳检测和readiness就绪检查机制 - 实现app_ticket事件的安全轮换和验证处理 - 添加生产环境运行编排和fail-closed安全机制 - 支持webhook快速确认和长连接独立进程处理 - 完善个人数据擦除时的待处理事件清理功能 ```
179 lines
5.6 KiB
PowerShell
179 lines
5.6 KiB
PowerShell
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
|
|
}
|