feat: 扩展纳入版本控制体系 — runtime-control 注册、vision 改造、local-tools 脚本入 git(supervisor/public),pi-agent-public 公网地址改环境变量

This commit is contained in:
luckyyzh
2026-08-03 17:06:01 +08:00
parent ac0f0cdc0c
commit 92e8184136
7 changed files with 625 additions and 60 deletions
+66
View File
@@ -0,0 +1,66 @@
[CmdletBinding()]
param(
[ValidateSet('status', 'start', 'restart', 'stop')]
[string]$Action = 'status'
)
$ErrorActionPreference = 'Stop'
$webTaskName = 'PiAgentIntegratedWeb'
$tunnelTaskName = 'PiAgentIntegratedTunnel'
$localUrl = 'http://127.0.0.1:30141/'
# 公网地址从环境变量读取(.env 中设置 PI_AGENT_PUBLIC_URL),不在代码里写死个人域名
$publicUrl = $env:PI_AGENT_PUBLIC_URL
if ([string]::IsNullOrWhiteSpace($publicUrl)) {
$publicUrl = '(未配置 PI_AGENT_PUBLIC_URL)'
}
function Get-PublicAccessStatus {
$webTask = Get-ScheduledTask -TaskName $webTaskName -ErrorAction SilentlyContinue
$tunnelTask = Get-ScheduledTask -TaskName $tunnelTaskName -ErrorAction SilentlyContinue
$listener = Get-NetTCPConnection -State Listen -LocalPort 30141 -ErrorAction SilentlyContinue
$httpStatus = 'unavailable'
try {
$response = Invoke-WebRequest -Uri $localUrl -Method Head -TimeoutSec 10
$httpStatus = [string][int]$response.StatusCode
}
catch {
if ($_.Exception.Response) {
$httpStatus = [string][int]$_.Exception.Response.StatusCode
}
}
[pscustomobject]@{
WebTask = if ($webTask) { [string]$webTask.State } else { 'missing' }
TunnelTask = if ($tunnelTask) { [string]$tunnelTask.State } else { 'missing' }
Port30141 = if ($listener) { 'listening' } else { 'closed' }
LocalHttp = $httpStatus
PublicUrl = $publicUrl
} | Format-List
}
switch ($Action) {
'status' {
Get-PublicAccessStatus
}
'start' {
Start-ScheduledTask -TaskName $tunnelTaskName
Start-ScheduledTask -TaskName $webTaskName
Start-Sleep -Seconds 3
Get-PublicAccessStatus
}
'restart' {
Stop-ScheduledTask -TaskName $webTaskName -ErrorAction SilentlyContinue
Stop-ScheduledTask -TaskName $tunnelTaskName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Start-ScheduledTask -TaskName $tunnelTaskName
Start-ScheduledTask -TaskName $webTaskName
Start-Sleep -Seconds 3
Get-PublicAccessStatus
}
'stop' {
Stop-ScheduledTask -TaskName $webTaskName -ErrorAction SilentlyContinue
Stop-ScheduledTask -TaskName $tunnelTaskName -ErrorAction SilentlyContinue
Get-PublicAccessStatus
}
}
@@ -0,0 +1,213 @@
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$projectRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
$logsDirectory = Join-Path $projectRoot 'data\logs'
$standardOutputLog = Join-Path $logsDirectory 'pi-web-service.out.log'
$standardErrorLog = Join-Path $logsDirectory 'pi-web-service.err.log'
$supervisorLog = Join-Path $logsDirectory 'pi-web-supervisor.log'
$restartRequestPath = Join-Path $projectRoot 'data\agent\restart-request.json'
$webUrl = 'http://127.0.0.1:30141/'
$restartRequestVersion = 1
New-Item -ItemType Directory -Path $logsDirectory -Force | Out-Null
New-Item -ItemType Directory -Path (Split-Path -Parent $restartRequestPath) -Force | Out-Null
Set-Content -LiteralPath $supervisorLog -Value "$(Get-Date -Format o) supervisor started"
function Write-SupervisorLog {
param([string]$Message)
Add-Content -LiteralPath $supervisorLog -Value "$(Get-Date -Format o) $Message"
}
function Get-RestartRequest {
if (-not (Test-Path -LiteralPath $restartRequestPath -PathType Leaf)) {
return $null
}
try {
# The extension writes the request with Node (UTF-8 without BOM);
# Windows PowerShell 5.1 Get-Content defaults to ANSI and would corrupt
# non-ASCII testInstructions, breaking ConvertFrom-Json silently.
$request = Get-Content -LiteralPath $restartRequestPath -Raw -Encoding UTF8 | ConvertFrom-Json
if ([int]$request.version -ne $restartRequestVersion) {
return $null
}
if ([string]::IsNullOrWhiteSpace([string]$request.requestId)) {
return $null
}
if ([string]::IsNullOrWhiteSpace([string]$request.sessionId)) {
return $null
}
if ([string]::IsNullOrWhiteSpace([string]$request.testInstructions)) {
return $null
}
if ([string]$request.state -eq 'ready') {
return $request
}
# session_shutdown normally changes requested -> ready. If that event
# was interrupted, allow a stale request to proceed after a grace period.
if ([string]$request.state -eq 'requested') {
$createdAt = [DateTimeOffset]::MinValue
if ([DateTimeOffset]::TryParse([string]$request.createdAt, [ref]$createdAt)) {
$ageSeconds = ([DateTimeOffset]::UtcNow - $createdAt.ToUniversalTime()).TotalSeconds
if ($ageSeconds -ge 15) {
return $request
}
}
}
}
catch {
# The extension writes the request atomically. A transient parse failure
# means the file is still being replaced; retry on the next poll.
}
return $null
}
function Remove-RestartRequest {
Remove-Item -LiteralPath $restartRequestPath -Force -ErrorAction SilentlyContinue
}
function Stop-ProcessTree {
param([System.Diagnostics.Process]$Process)
if (-not $Process -or $Process.HasExited) {
return
}
Write-SupervisorLog "stopping Web process tree for restart request (PID $($Process.Id))"
& taskkill.exe /PID $Process.Id /T /F 2>$null | Out-Null
}
function Test-WebReady {
try {
$response = Invoke-WebRequest -Uri $webUrl -Method Get -TimeoutSec 3 -UseBasicParsing
return $response.StatusCode -ge 200 -and $response.StatusCode -lt 400
}
catch {
return $false
}
}
function Get-RequestField {
param(
[object]$Request,
[string]$Name
)
$property = $Request.PSObject.Properties[$Name]
if ($property) {
return [string]$property.Value
}
if ($Request -is [System.Collections.IDictionary] -and $Request.Contains($Name)) {
return [string]$Request[$Name]
}
return ''
}
function Resume-AgentSession {
param([object]$Request)
$requestId = Get-RequestField $Request 'requestId'
$sessionId = Get-RequestField $Request 'sessionId'
$testInstructions = Get-RequestField $Request 'testInstructions'
$encodedSessionId = [Uri]::EscapeDataString($sessionId)
$resumeMessage = @"
[Pi Web restart complete]
Pi Web was restarted by the external supervisor and the previous Agent session was restored. Request ID: $requestId
Continue the previous task without repeating completed edits.
Post-restart verification:
$testInstructions
"@
$body = @{
type = 'prompt'
message = $resumeMessage.Trim()
} | ConvertTo-Json -Compress
$bodyBytes = [Text.Encoding]::UTF8.GetBytes($body)
$response = Invoke-RestMethod `
-Uri "http://127.0.0.1:30141/api/agent/$encodedSessionId" `
-Method Post `
-ContentType 'application/json; charset=utf-8' `
-Body $bodyBytes `
-TimeoutSec 30
if (-not $response.success) {
throw "Agent resume API returned an unsuccessful response"
}
Write-SupervisorLog "resumed Agent session $sessionId for request $requestId"
Remove-RestartRequest
}
$pendingRequest = Get-RestartRequest
while ($true) {
$process = $null
try {
$npmCommand = (Get-Command npm.cmd -ErrorAction Stop).Source
Write-SupervisorLog 'starting npm run restart'
$process = Start-Process `
-FilePath $npmCommand `
-ArgumentList @('run', 'restart') `
-WorkingDirectory $projectRoot `
-WindowStyle Hidden `
-RedirectStandardOutput $standardOutputLog `
-RedirectStandardError $standardErrorLog `
-PassThru
$startedAt = Get-Date
$nextResumeAttempt = Get-Date
while (-not $process.HasExited) {
if ($pendingRequest -and (Get-Date) -ge $nextResumeAttempt) {
# Give npm/Next a moment to finish binding the port before the
# first resume attempt. Failed attempts remain pending and retry.
if (((Get-Date) - $startedAt).TotalSeconds -ge 2 -and (Test-WebReady)) {
try {
Resume-AgentSession $pendingRequest
$pendingRequest = $null
}
catch {
Write-SupervisorLog "Agent resume failed: $($_.Exception.Message)"
$nextResumeAttempt = (Get-Date).AddSeconds(3)
}
}
}
if (-not $pendingRequest) {
$candidate = Get-RestartRequest
if ($candidate) {
$pendingRequest = $candidate
Write-SupervisorLog "restart request $($candidate.requestId) detected"
Stop-ProcessTree $process
break
}
}
$null = $process.WaitForExit(500)
}
if (-not $process.HasExited) {
$null = $process.WaitForExit(10000)
}
if ($process.HasExited) {
Write-SupervisorLog "web process exited with code $($process.ExitCode)"
}
}
catch {
Write-SupervisorLog "supervisor error: $($_.Exception.Message)"
}
Start-Sleep -Seconds 5
}