fix: schedule timezone bug + missing notifications + celery beat startup
- Timezone: compute_next_run now correctly interprets cron in the schedule's configured timezone (e.g., "0 8 * * *" with Asia/Shanghai = 8AM Beijing, not UTC) - Notifications: agent_tasks now reuses pre-created execution records and calls notify_schedule_result on completion, so non-workflow agent schedules get DB notifications + Feishu webhook + Feishu app messages - Duplicate execution: execute_agent_task accepts optional execution_id to reuse the record created by schedule_service instead of creating a second one - Celery Beat: added to restart_backend_celery.ps1, stop_aiagent.ps1, and docker-compose.dev.yml; fixed repo-root path resolution in all PS1 scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
73
scripts/startup/restart_backend_celery.ps1
Normal file
73
scripts/startup/restart_backend_celery.ps1
Normal file
@@ -0,0 +1,73 @@
|
||||
# Restart backend API (uvicorn) + Celery worker + Celery Beat (scheduler)
|
||||
# Does not stop frontend/Redis.
|
||||
$ErrorActionPreference = "Continue"
|
||||
# Repo root = two levels up from scripts/startup/
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$RepoRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
||||
$Backend = Join-Path $RepoRoot "backend"
|
||||
$ApiPort = 8037
|
||||
|
||||
Write-Host "== Stop API + Celery worker + Celery Beat ==" -ForegroundColor Cyan
|
||||
|
||||
function Stop-ByCommandLine([string]$pattern, [string]$name) {
|
||||
$targets = Get-CimInstance Win32_Process | Where-Object {
|
||||
$_.CommandLine -and $_.CommandLine -match $pattern
|
||||
}
|
||||
if (-not $targets) {
|
||||
Write-Host "[SKIP] ${name}: no matching process" -ForegroundColor DarkGray
|
||||
return
|
||||
}
|
||||
foreach ($p in $targets) {
|
||||
try {
|
||||
Stop-Process -Id $p.ProcessId -Force
|
||||
Write-Host "[OK] stopped ${name} PID=$($p.ProcessId)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[WARN] failed to stop ${name} PID=$($p.ProcessId)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stop-ByCommandLine "uvicorn\s+app\.main:app" "backend-api"
|
||||
Stop-ByCommandLine "celery\s+-A\s+app\.core\.celery_app\s+worker" "celery-worker"
|
||||
Stop-ByCommandLine "celery\s+-A\s+app\.core\.celery_app\s+beat" "celery-beat"
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Write-Host "== Start API on $ApiPort + Celery worker + Celery Beat ==" -ForegroundColor Cyan
|
||||
|
||||
# 1. API (uvicorn)
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-Command",
|
||||
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m uvicorn app.main:app --host 0.0.0.0 --port $ApiPort"
|
||||
)
|
||||
|
||||
# 2. Celery worker
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-Command",
|
||||
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8"
|
||||
)
|
||||
|
||||
# 3. Celery Beat (scheduler — 每分钟检查到期的定时任务)
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-Command",
|
||||
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app beat --loglevel=info"
|
||||
)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[DONE] 已在新窗口启动 API + Celery Worker + Celery Beat" -ForegroundColor Green
|
||||
Write-Host "API: http://127.0.0.1:$ApiPort/docs" -ForegroundColor Cyan
|
||||
Write-Host "Celery Beat 负责每分钟检查定时任务,如果定时推送没触发,请确认 Beat 窗口正在运行" -ForegroundColor Yellow
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
Write-Host ""
|
||||
Write-Host "Port $ApiPort :" -ForegroundColor Cyan
|
||||
netstat -ano | findstr ":$ApiPort"
|
||||
91
scripts/startup/start_aiagent.ps1
Normal file
91
scripts/startup/start_aiagent.ps1
Normal file
@@ -0,0 +1,91 @@
|
||||
param(
|
||||
[int]$ApiPort = 8037,
|
||||
[int]$FallbackApiPort = 8041,
|
||||
[int]$FrontendPort = 3001
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$RepoRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
||||
$Backend = Join-Path $RepoRoot "backend"
|
||||
$Frontend = Join-Path $RepoRoot "frontend"
|
||||
$RedisDir = Join-Path $Backend "redis"
|
||||
$RedisExe = Join-Path $RedisDir "redis-server.exe"
|
||||
$RedisCli = Join-Path $RedisDir "redis-cli.exe"
|
||||
|
||||
function Test-PortListening([int]$Port) {
|
||||
$line = netstat -ano | Select-String ":$Port\s+.*LISTENING" | Select-Object -First 1
|
||||
return [bool]$line
|
||||
}
|
||||
|
||||
function Ensure-Redis {
|
||||
if (Test-PortListening 6379) {
|
||||
Write-Host '[OK] Redis already listening on 6379' -ForegroundColor Green
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path $RedisExe)) {
|
||||
throw "Redis executable not found: $RedisExe"
|
||||
}
|
||||
Write-Host '[RUN] Starting Redis on 6379 ...' -ForegroundColor Yellow
|
||||
Start-Process -FilePath $RedisExe -ArgumentList "--port 6379" -WorkingDirectory $RedisDir | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
if (-not (Test-PortListening 6379)) {
|
||||
throw "Redis failed: port 6379 not listening"
|
||||
}
|
||||
if (Test-Path $RedisCli) {
|
||||
& $RedisCli -p 6379 ping | Out-Null
|
||||
}
|
||||
Write-Host '[OK] Redis started' -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Resolve-ApiPort {
|
||||
if (-not (Test-PortListening $ApiPort)) {
|
||||
return $ApiPort
|
||||
}
|
||||
Write-Host ('[WARN] Port {0} is occupied, switching to {1}' -f $ApiPort, $FallbackApiPort) -ForegroundColor Yellow
|
||||
if (Test-PortListening $FallbackApiPort) {
|
||||
throw "Ports $ApiPort and $FallbackApiPort are in use; free one first"
|
||||
}
|
||||
return $FallbackApiPort
|
||||
}
|
||||
|
||||
Write-Host '== AIAgent one-click start ==' -ForegroundColor Cyan
|
||||
Write-Host "Repo: $RepoRoot"
|
||||
|
||||
Ensure-Redis
|
||||
$RealApiPort = Resolve-ApiPort
|
||||
$ApiBase = "http://127.0.0.1:$RealApiPort"
|
||||
|
||||
Write-Host ('[RUN] Starting backend API on {0} ...' -f $RealApiPort) -ForegroundColor Yellow
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"cd '$Backend'; .\venv\Scripts\Activate.ps1; python -m uvicorn app.main:app --host 0.0.0.0 --port $RealApiPort"
|
||||
)
|
||||
|
||||
Write-Host '[RUN] Starting Celery worker ...' -ForegroundColor Yellow
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"cd '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8"
|
||||
)
|
||||
|
||||
Write-Host '[RUN] Starting Celery Beat (scheduler) ...' -ForegroundColor Yellow
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"cd '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app beat --loglevel=info"
|
||||
)
|
||||
|
||||
Write-Host ('[RUN] Starting frontend on {0} (proxy -> {1}) ...' -f $FrontendPort, $ApiBase) -ForegroundColor Yellow
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"`$env:AIAGENT_API_PROXY='$ApiBase'; cd '$Frontend'; pnpm dev --port $FrontendPort"
|
||||
)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host '[DONE] Start commands issued (check new PowerShell windows)' -ForegroundColor Green
|
||||
Write-Host "Frontend: http://localhost:$FrontendPort" -ForegroundColor Cyan
|
||||
Write-Host "API docs: $ApiBase/docs" -ForegroundColor Cyan
|
||||
Write-Host "Redis: 127.0.0.1:6379" -ForegroundColor Cyan
|
||||
112
scripts/startup/start_aiagent_background.ps1
Normal file
112
scripts/startup/start_aiagent_background.ps1
Normal file
@@ -0,0 +1,112 @@
|
||||
# 静默后台启动脚本(无弹窗,日志写文件)
|
||||
# 用于 Windows 任务计划程序开机自启
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$RepoRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
||||
$Backend = Join-Path $RepoRoot "backend"
|
||||
$Frontend = Join-Path $RepoRoot "frontend"
|
||||
$RedisDir = Join-Path $Backend "redis"
|
||||
$RedisExe = Join-Path $RedisDir "redis-server.exe"
|
||||
$RedisCli = Join-Path $RedisDir "redis-cli.exe"
|
||||
$LogDir = Join-Path $RepoRoot "logs"
|
||||
$LogFile = Join-Path $LogDir "autostart_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
|
||||
|
||||
if (-not (Test-Path $LogDir)) {
|
||||
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
|
||||
}
|
||||
|
||||
function Write-Log($msg) {
|
||||
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
"$ts $msg" | Out-File -Append -FilePath $LogFile -Encoding UTF8
|
||||
}
|
||||
|
||||
Write-Log "========== AIAgent background start =========="
|
||||
Write-Log "Repo: $RepoRoot"
|
||||
|
||||
# ── 1) Redis ──────────────────────────────────
|
||||
$redisPort = 6379
|
||||
$redisListening = netstat -ano | Select-String ":$redisPort\s+.*LISTENING"
|
||||
if ($redisListening) {
|
||||
Write-Log "Redis already listening on $redisPort"
|
||||
} else {
|
||||
if (-not (Test-Path $RedisExe)) {
|
||||
Write-Log "ERROR: Redis not found at $RedisExe"
|
||||
} else {
|
||||
Write-Log "Starting Redis on $redisPort ..."
|
||||
$redisProc = Start-Process -FilePath $RedisExe `
|
||||
-ArgumentList "--port $redisPort" `
|
||||
-WorkingDirectory $RedisDir `
|
||||
-WindowStyle Hidden `
|
||||
-PassThru
|
||||
Start-Sleep -Seconds 2
|
||||
if (Test-Path $RedisCli) {
|
||||
& $RedisCli -p $redisPort ping 2>&1 | Out-File -Append $LogFile -Encoding UTF8
|
||||
}
|
||||
Write-Log "Redis PID=$($redisProc.Id) started"
|
||||
}
|
||||
}
|
||||
|
||||
# ── 2) Backend API ────────────────────────────
|
||||
$apiPort = 8037
|
||||
$apiListening = netstat -ano | Select-String ":$apiPort\s+.*LISTENING"
|
||||
if ($apiListening) {
|
||||
Write-Log "Backend API already listening on $apiPort"
|
||||
} else {
|
||||
Write-Log "Starting backend API on $apiPort ..."
|
||||
$apiLog = Join-Path $LogDir "api.log"
|
||||
Start-Process powershell `
|
||||
-WindowStyle Hidden `
|
||||
-ArgumentList @(
|
||||
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||
"-Command",
|
||||
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m uvicorn app.main:app --host 0.0.0.0 --port $apiPort 2>&1 | Out-File -Append '$apiLog' -Encoding UTF8"
|
||||
)
|
||||
Write-Log "Backend API starting (log: $apiLog)"
|
||||
}
|
||||
|
||||
# ── 3) Celery Worker ──────────────────────────
|
||||
Write-Log "Starting Celery worker ..."
|
||||
$celeryLog = Join-Path $LogDir "celery.log"
|
||||
Start-Process powershell `
|
||||
-WindowStyle Hidden `
|
||||
-ArgumentList @(
|
||||
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||
"-Command",
|
||||
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8 2>&1 | Out-File -Append '$celeryLog' -Encoding UTF8"
|
||||
)
|
||||
Write-Log "Celery worker starting (log: $celeryLog)"
|
||||
|
||||
# ── 3.5) Celery Beat (scheduler) ──────────────
|
||||
Write-Log "Starting Celery Beat ..."
|
||||
$beatLog = Join-Path $LogDir "celery_beat.log"
|
||||
Start-Process powershell `
|
||||
-WindowStyle Hidden `
|
||||
-ArgumentList @(
|
||||
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||
"-Command",
|
||||
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app beat --loglevel=info 2>&1 | Out-File -Append '$beatLog' -Encoding UTF8"
|
||||
)
|
||||
Write-Log "Celery Beat starting (log: $beatLog)"
|
||||
|
||||
# ── 4) Frontend (pnpm dev) ────────────────────
|
||||
$frontendPort = 3001
|
||||
$frontendListening = netstat -ano | Select-String ":$frontendPort\s+.*LISTENING"
|
||||
if ($frontendListening) {
|
||||
Write-Log "Frontend already listening on $frontendPort"
|
||||
} else {
|
||||
Write-Log "Starting frontend on $frontendPort ..."
|
||||
$frontendLog = Join-Path $LogDir "frontend.log"
|
||||
Start-Process powershell `
|
||||
-WindowStyle Hidden `
|
||||
-ArgumentList @(
|
||||
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||
"-Command",
|
||||
"`$env:AIAGENT_API_PROXY='http://127.0.0.1:$apiPort'; Set-Location '$Frontend'; pnpm dev --port $frontendPort 2>&1 | Out-File -Append '$frontendLog' -Encoding UTF8"
|
||||
)
|
||||
Write-Log "Frontend starting (log: $frontendLog)"
|
||||
}
|
||||
|
||||
Write-Log "========== All services issued =========="
|
||||
Write-Log "Frontend: http://localhost:$frontendPort"
|
||||
Write-Log "API docs: http://127.0.0.1:$apiPort/docs"
|
||||
87
scripts/startup/stop_aiagent.ps1
Normal file
87
scripts/startup/stop_aiagent.ps1
Normal file
@@ -0,0 +1,87 @@
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
Write-Host "== AIAgent stop ==" -ForegroundColor Cyan
|
||||
|
||||
function Get-PidsListeningOnPort([int]$Port) {
|
||||
$pids = New-Object System.Collections.Generic.HashSet[int]
|
||||
try {
|
||||
netstat -ano | ForEach-Object {
|
||||
$ln = $_.Trim()
|
||||
if ($ln -notmatch "LISTENING") { return }
|
||||
# 匹配 :8037 或 :3001 等端口后的 LISTENING 行末 PID
|
||||
if ($ln -match ":$Port\s+.*LISTENING\s+(\d+)\s*$") {
|
||||
[void]$pids.Add([int]$Matches[1])
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
return @($pids)
|
||||
}
|
||||
|
||||
function Stop-OnPorts([int[]]$ports, [string]$name) {
|
||||
$all = New-Object System.Collections.Generic.HashSet[int]
|
||||
foreach ($p in $ports) {
|
||||
foreach ($pid in (Get-PidsListeningOnPort $p)) {
|
||||
[void]$all.Add($pid)
|
||||
}
|
||||
}
|
||||
if ($all.Count -eq 0) {
|
||||
Write-Host "[SKIP] ${name}: no listener on ports $($ports -join ',')" -ForegroundColor DarkGray
|
||||
return
|
||||
}
|
||||
foreach ($pid in $all) {
|
||||
if ($pid -le 4) { continue }
|
||||
try {
|
||||
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "[OK] stopped ${name} PID=$pid" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[WARN] failed to stop ${name} PID=$pid" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 后端 API(8037 / 8041 备用)
|
||||
Stop-OnPorts @(8037, 8041) "backend-api"
|
||||
|
||||
# 前端 Vite(3001)
|
||||
Stop-OnPorts @(3001) "frontend-dev"
|
||||
|
||||
# Redis(6379,仅当监听在本地开发端口时结束;若与其它项目共用请谨慎)
|
||||
Stop-OnPorts @(6379) "redis"
|
||||
|
||||
# Celery Worker / Beat(不监听端口,需要按命令行匹配)
|
||||
$celeryPatterns = @(
|
||||
@{Pattern='celery\s+-A\s+app\.core\.celery_app\s+worker'; Name='celery-worker'},
|
||||
@{Pattern='celery\s+-A\s+app\.core\.celery_app\s+beat'; Name='celery-beat'}
|
||||
)
|
||||
foreach ($cp in $celeryPatterns) {
|
||||
$targets = Get-CimInstance Win32_Process | Where-Object {
|
||||
$_.CommandLine -and $_.CommandLine -match $cp.Pattern
|
||||
}
|
||||
if (-not $targets) {
|
||||
Write-Host "[SKIP] $($cp.Name): no matching process" -ForegroundColor DarkGray
|
||||
}
|
||||
foreach ($p in $targets) {
|
||||
try {
|
||||
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "[OK] stopped $($cp.Name) PID=$($p.ProcessId)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[WARN] failed to stop $($cp.Name) PID=$($p.ProcessId)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 600
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Port check:" -ForegroundColor Cyan
|
||||
foreach ($port in 3001, 8037, 8041, 6379) {
|
||||
$line = netstat -ano | Select-String ":$port\s+.*LISTENING" | Select-Object -First 1
|
||||
if ($line) {
|
||||
Write-Host " - ${port}: LISTEN" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host " - ${port}: free" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "DONE: stop script finished" -ForegroundColor Green
|
||||
Reference in New Issue
Block a user