Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e20e6aed | ||
|
|
19c0868c5d | ||
|
|
1c6b30fac0 | ||
|
|
6702b8a5b9 |
@@ -15,6 +15,14 @@
|
||||
|
||||
E2E 入口从 PowerShell 7 调用时会自动转入 Windows PowerShell 5.1 执行本地 HTTP 回归;源码打包仍显式使用冻结要求的 PowerShell 7。这样与 Windows 交付脚本的宿主一致,也避开当前机器 PowerShell 7 HTTP 客户端对本地 Go/MediaMTX 响应的兼容问题。
|
||||
|
||||
默认入口只复制 Git 已跟踪的 `Sense/` 源码到系统临时目录,因此正常开发工作区中已有的 `node_modules`、`dist`、本地配置、日志和其他未跟踪文件不会进入验收副本。`-PreparedPackageRoot` 也会先把指定包复制到本次临时目录,运行时配置、浏览器脚本、截图和日志不会写回原包或源码树。
|
||||
|
||||
Sense 进程提前退出或 HTTP 就绪超时时,脚本返回非零并输出阶段、退出状态、临时日志位置和经过过滤、截断的日志摘要;数据库连接、密码、token、Cookie、JWT 和 credential key 不得出现在诊断中。默认无论成功或失败都会清理所属进程和临时目录;`-KeepTemporary` 仅用于排错,仍会停止进程,但保留目录可能包含随机运行时秘密,必须限制访问并在排错后安全删除。
|
||||
|
||||
为避免 Windows 首次扫描临时复制的 MediaMTX 二进制占用产品固定的就绪窗口,E2E 会先在同一动态端口和临时配置上启动一次包内 MediaMTX,确认 Control API 可用并完全停止,再由 Sense 以 managed 模式启动并完成生命周期验收。预检失败会单独报告 `MediaMTX preflight` 阶段,不会被误报为 Sense HTTP 超时。
|
||||
|
||||
HTTP、RTSP、HLS、Control API 和 ONVIF 动态端口使用 TCP 绑定探测;WebRTC 本地 UDP 端口必须使用 UDP socket 实际绑定探测,不得用 TCP 空闲结果代替,避免落入 Windows 的 UDP 排除或占用范围。
|
||||
|
||||
## 回归矩阵
|
||||
|
||||
| 范围 | 自动化证据 | 判定 |
|
||||
|
||||
@@ -49,4 +49,18 @@ foreach ($file in $fixtureFiles) {
|
||||
if ($file.Extension -eq '.json') { [void]($content | ConvertFrom-Json); $passed++ }
|
||||
}
|
||||
|
||||
$e2eScript = Read-Utf8 (Join-Path $senseRoot 'tests\e2e\run-isolated-e2e.ps1')
|
||||
Assert-True ($e2eScript.Contains('Copy-TrackedSenseSource $repositoryRoot $senseCopy')) 'Sense E2E no longer copies only tracked source'
|
||||
Assert-True (-not $e2eScript.Contains('Copy-Item -LiteralPath $sourceSense -Destination $senseCopy -Recurse')) 'Sense E2E regressed to recursive source-tree copying'
|
||||
Assert-True ($e2eScript.Contains("`$browserScript = Join-Path `$PSScriptRoot 'browser-smoke.cjs'")) 'browser smoke script no longer runs from its tracked location'
|
||||
Assert-True ($e2eScript.Contains("`$packageRoot = Join-Path `$temporary 'prepared-package'")) 'prepared package no longer runs from an isolated temporary copy'
|
||||
Assert-True ($e2eScript.Contains("-Process `$process -Stage 'Sense HTTP'")) 'Sense HTTP readiness no longer observes the package process'
|
||||
Assert-True ($e2eScript.Contains('Get-SafeLogSummary')) 'Sense readiness diagnostics no longer use log redaction'
|
||||
Assert-True ($e2eScript.Contains("-Stage 'MediaMTX preflight'")) 'Sense E2E no longer preflights the copied MediaMTX package and config'
|
||||
Assert-True ($e2eScript.Contains('function Get-FreeUdpPort')) 'Sense E2E no longer probes WebRTC UDP ports with the UDP protocol'
|
||||
Assert-True ($e2eScript.Contains('do { $webrtcUDPort = Get-FreeUdpPort }')) 'Sense E2E WebRTC UDP port regressed to TCP-only discovery'
|
||||
|
||||
& powershell.exe -NoProfile -File (Join-Path $senseRoot 'tests\e2e\run-isolated-e2e.ps1') -HarnessSelfTest
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Sense E2E harness self-test failed' }
|
||||
|
||||
Write-Host "Sense compatibility regression passed: $passed assertions."
|
||||
|
||||
@@ -3,11 +3,13 @@ param(
|
||||
[string]$MediaMTX = 'C:\Users\ila20\Desktop\mediamtx\mediamtx.exe',
|
||||
[string]$Browser = 'C:\Program Files\Google\Chrome\Application\chrome.exe',
|
||||
[string]$PreparedPackageRoot = '',
|
||||
[switch]$HarnessSelfTest,
|
||||
[switch]$KeepTemporary
|
||||
)
|
||||
if ($PSVersionTable.PSEdition -eq 'Core') {
|
||||
$legacyArguments = @('-NoProfile', '-File', $PSCommandPath, '-PostgresBin', $PostgresBin, '-MediaMTX', $MediaMTX, '-Browser', $Browser)
|
||||
if (-not [string]::IsNullOrWhiteSpace($PreparedPackageRoot)) { $legacyArguments += @('-PreparedPackageRoot', $PreparedPackageRoot) }
|
||||
if ($HarnessSelfTest) { $legacyArguments += '-HarnessSelfTest' }
|
||||
if ($KeepTemporary) { $legacyArguments += '-KeepTemporary' }
|
||||
& powershell.exe @legacyArguments
|
||||
exit $LASTEXITCODE
|
||||
@@ -34,8 +36,10 @@ $fixtureStatus = Join-Path $temporary 'fixture-status.json'
|
||||
$server = $null
|
||||
$fixture = $null
|
||||
$publisher = $null
|
||||
$preflightMedia = $null
|
||||
$pgStarted = $false
|
||||
$savedEnvironment = @{}
|
||||
$sensitiveValues = @()
|
||||
|
||||
function Get-FreePort {
|
||||
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
|
||||
@@ -61,15 +65,68 @@ function Set-TestEnvironment([string]$Name, [string]$Value) {
|
||||
}
|
||||
[Environment]::SetEnvironmentVariable($Name, $Value, 'Process')
|
||||
}
|
||||
function Wait-Http([string]$Uri, [int]$Attempts = 120) {
|
||||
function Get-FreeUdpPort {
|
||||
$client = [Net.Sockets.UdpClient]::new([Net.IPEndPoint]::new([Net.IPAddress]::Loopback, 0))
|
||||
try { return ([Net.IPEndPoint]$client.Client.LocalEndPoint).Port } finally { $client.Dispose() }
|
||||
}
|
||||
function Copy-TrackedSenseSource([string]$RepositoryRoot, [string]$Destination) {
|
||||
$tracked = @(& git -C $RepositoryRoot ls-files -- 'Sense')
|
||||
if ($LASTEXITCODE -ne 0 -or $tracked.Count -eq 0) { throw 'Could not enumerate tracked Sense source files' }
|
||||
$sensePrefix = 'Sense\'
|
||||
foreach ($relative in $tracked) {
|
||||
$normalized = ([string]$relative).Replace('/', '\')
|
||||
if (-not $normalized.StartsWith($sensePrefix, [StringComparison]::Ordinal)) {
|
||||
throw "Unexpected tracked path outside Sense: $relative"
|
||||
}
|
||||
$source = Join-Path $RepositoryRoot $normalized
|
||||
if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { throw "Tracked Sense source is missing: $relative" }
|
||||
$target = Join-Path $Destination $normalized.Substring($sensePrefix.Length)
|
||||
$parent = Split-Path -Parent $target
|
||||
if (-not (Test-Path -LiteralPath $parent)) { [void](New-Item -ItemType Directory -Path $parent -Force) }
|
||||
Copy-Item -LiteralPath $source -Destination $target
|
||||
}
|
||||
}
|
||||
function Get-SafeLogSummary([string[]]$Paths, [int]$MaximumCharacters = 2000) {
|
||||
$parts = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($path in @($Paths)) {
|
||||
if ([string]::IsNullOrWhiteSpace($path) -or -not (Test-Path -LiteralPath $path -PathType Leaf)) { continue }
|
||||
$text = [string](@(Get-Content -LiteralPath $path -Tail 20 -ErrorAction SilentlyContinue) -join ' | ')
|
||||
foreach ($secret in @($script:sensitiveValues)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($secret) -and $secret.Length -ge 4) { $text = $text.Replace($secret, '<redacted>') }
|
||||
}
|
||||
$text = $text -replace '(?i)((?:password|token|secret|credential(?:_key)?|database(?:_url)?|cookie|authorization)["'']?\s*[:=]\s*["'']?)[^\s,;"'']+', '$1<redacted>'
|
||||
$text = $text -replace '(?i)(postgres(?:ql)?://)[^\s]+', '$1<redacted>'
|
||||
if ($text.Length -gt $MaximumCharacters) { $text = $text.Substring($text.Length - $MaximumCharacters) }
|
||||
if (-not [string]::IsNullOrWhiteSpace($text)) { $parts.Add("$([IO.Path]::GetFileName($path)): $text") }
|
||||
}
|
||||
if ($parts.Count -eq 0) { return '<no log output>' }
|
||||
return ($parts -join ' || ')
|
||||
}
|
||||
function Wait-Http {
|
||||
param(
|
||||
[string]$Uri,
|
||||
[int]$Attempts = 120,
|
||||
$Process = $null,
|
||||
[string]$Stage = 'HTTP endpoint',
|
||||
[string[]]$LogPaths = @()
|
||||
)
|
||||
for ($attempt = 0; $attempt -lt $Attempts; $attempt++) {
|
||||
if ($null -ne $Process -and $Process.HasExited) {
|
||||
try { $Process.WaitForExit(); $Process.Refresh() } catch {}
|
||||
$exitCode = try { [string]$Process.ExitCode } catch { 'unknown' }
|
||||
if ([string]::IsNullOrWhiteSpace($exitCode)) { $exitCode = 'unknown' }
|
||||
$summary = Get-SafeLogSummary $LogPaths
|
||||
throw "$Stage process exited before readiness: exit_code=$exitCode; log_files=$($LogPaths -join ','); summary=$summary"
|
||||
}
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 1
|
||||
if ($response.StatusCode -eq 200) { return }
|
||||
} catch {}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
throw "HTTP endpoint did not become ready: $Uri"
|
||||
$processState = if ($null -eq $Process) { 'not-observed' } elseif ($Process.HasExited) { "exited:$($Process.ExitCode)" } else { 'running' }
|
||||
$summary = Get-SafeLogSummary $LogPaths
|
||||
throw "$Stage did not become ready: uri=$Uri; process_state=$processState; log_files=$($LogPaths -join ','); summary=$summary"
|
||||
}
|
||||
function Wait-Tcp([int]$Port, [bool]$Open, [int]$Attempts = 120) {
|
||||
for ($attempt = 0; $attempt -lt $Attempts; $attempt++) {
|
||||
@@ -104,13 +161,62 @@ function Invoke-SenseJson {
|
||||
function Start-SensePackage([string]$PackageRoot) {
|
||||
$launcher = Join-Path $PackageRoot 'start-sense.bat'
|
||||
$process = Start-Process -FilePath 'cmd.exe' -ArgumentList '/d', '/c', "`"$launcher`"" -WorkingDirectory $PackageRoot -RedirectStandardOutput $runtimeLog -RedirectStandardError $runtimeError -WindowStyle Hidden -PassThru
|
||||
Wait-Http "$script:baseUrl/"
|
||||
Wait-Http -Uri "$script:baseUrl/" -Process $process -Stage 'Sense HTTP' -LogPaths @($runtimeLog, $runtimeError)
|
||||
return $process
|
||||
}
|
||||
function Stop-ProcessTree($Process) {
|
||||
if ($Process -and -not $Process.HasExited) { & taskkill.exe /PID $Process.Id /T /F 2>$null | Out-Null }
|
||||
}
|
||||
|
||||
function Invoke-HarnessSelfTest {
|
||||
$root = Join-Path ([IO.Path]::GetTempPath()) ('sense-e2e-selftest-' + [guid]::NewGuid().ToString('N'))
|
||||
$originalSensitiveValues = @($script:sensitiveValues)
|
||||
try {
|
||||
$fixtureRepository = Join-Path $root 'repository'
|
||||
$trackedSource = Join-Path $fixtureRepository 'Sense\tracked.txt'
|
||||
$ignoredSource = Join-Path $fixtureRepository 'Sense\ui\node_modules\ignored.txt'
|
||||
[void](New-Item -ItemType Directory -Path (Split-Path -Parent $trackedSource) -Force)
|
||||
[void](New-Item -ItemType Directory -Path (Split-Path -Parent $ignoredSource) -Force)
|
||||
[IO.File]::WriteAllText($trackedSource, 'tracked', (New-Object Text.UTF8Encoding($false)))
|
||||
[IO.File]::WriteAllText($ignoredSource, 'ignored', (New-Object Text.UTF8Encoding($false)))
|
||||
& git -C $fixtureRepository init --quiet
|
||||
& git -C $fixtureRepository add -- 'Sense/tracked.txt'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Harness self-test could not prepare tracked source' }
|
||||
$copy = Join-Path $root 'copy'
|
||||
Copy-TrackedSenseSource $fixtureRepository $copy
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $copy 'tracked.txt'))) { throw 'Harness self-test did not copy tracked source' }
|
||||
if (Test-Path -LiteralPath (Join-Path $copy 'ui\node_modules\ignored.txt')) { throw 'Harness self-test copied ignored node_modules content' }
|
||||
|
||||
$udpPort = Get-FreeUdpPort
|
||||
$udpProbe = [Net.Sockets.UdpClient]::new()
|
||||
try { $udpProbe.Client.Bind([Net.IPEndPoint]::new([Net.IPAddress]::Loopback, $udpPort)) } finally { $udpProbe.Dispose() }
|
||||
|
||||
$diagnosticLog = Join-Path $root 'sense.err.log'
|
||||
[IO.File]::WriteAllText($diagnosticLog, 'SENSE_JWT_SECRET=unit-secret-value', (New-Object Text.UTF8Encoding($false)))
|
||||
$script:sensitiveValues = @('unit-secret-value')
|
||||
$exited = [pscustomobject]@{ HasExited = $true; ExitCode = 23 }
|
||||
$earlyFailure = ''
|
||||
try { Wait-Http -Uri 'http://127.0.0.1:1/' -Attempts 3 -Process $exited -Stage 'Self-test early exit' -LogPaths @($diagnosticLog) } catch { $earlyFailure = $_.Exception.Message }
|
||||
if ($earlyFailure -notmatch 'exit_code=23' -or $earlyFailure.Contains('unit-secret-value') -or $earlyFailure -notmatch '<redacted>') {
|
||||
throw "Harness self-test early-exit diagnostic was unsafe or incomplete: $earlyFailure"
|
||||
}
|
||||
$timeoutFailure = ''
|
||||
try { Wait-Http -Uri 'http://127.0.0.1:1/' -Attempts 1 -Stage 'Self-test timeout' -LogPaths @($diagnosticLog) } catch { $timeoutFailure = $_.Exception.Message }
|
||||
if ($timeoutFailure -notmatch 'process_state=not-observed' -or $timeoutFailure.Contains('unit-secret-value') -or $timeoutFailure -notmatch '<redacted>') {
|
||||
throw "Harness self-test timeout diagnostic was unsafe or incomplete: $timeoutFailure"
|
||||
}
|
||||
Write-Host 'Sense E2E harness self-test passed: tracked copy, UDP bind, early exit, timeout and redaction.'
|
||||
} finally {
|
||||
$script:sensitiveValues = $originalSensitiveValues
|
||||
if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
|
||||
}
|
||||
}
|
||||
|
||||
if ($HarnessSelfTest) {
|
||||
Invoke-HarnessSelfTest
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($required in @(
|
||||
(Join-Path $PostgresBin 'initdb.exe'), (Join-Path $PostgresBin 'pg_ctl.exe'),
|
||||
@@ -121,25 +227,32 @@ try {
|
||||
}
|
||||
$ffmpeg = (Get-Command ffmpeg.exe -ErrorAction Stop).Source
|
||||
if ([string]::IsNullOrWhiteSpace($PreparedPackageRoot)) {
|
||||
New-Item -ItemType Directory -Path $repoCopy | Out-Null
|
||||
Copy-Item -LiteralPath $sourceSense -Destination $senseCopy -Recurse
|
||||
New-Item -ItemType Directory -Path $senseCopy -Force | Out-Null
|
||||
Copy-TrackedSenseSource $repositoryRoot $senseCopy
|
||||
& git -C $repoCopy init --quiet
|
||||
& git -C $repoCopy config user.name 'Sense E2E'
|
||||
& git -C $repoCopy config user.email 'sense-e2e@invalid.local'
|
||||
& git -C $repoCopy commit --allow-empty --quiet -m 'temporary acceptance source'
|
||||
& git -C $repoCopy config core.autocrlf false
|
||||
& git -C $repoCopy add -- Sense
|
||||
if ($LASTEXITCODE -ne 0) { throw 'temporary acceptance source staging failed' }
|
||||
& git -C $repoCopy commit --quiet -m 'temporary acceptance source'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'temporary acceptance source commit failed' }
|
||||
|
||||
Write-Host 'Building Sense Windows package in an isolated temporary copy...'
|
||||
& pwsh.exe -NoProfile -File (Join-Path $senseCopy 'scripts\build\build-windows.ps1') -MediaMTXPath $MediaMTX
|
||||
if ($LASTEXITCODE -ne 0) { throw 'isolated Windows package build failed' }
|
||||
$packageRoot = Join-Path $senseCopy 'dist\sense-windows-amd64'
|
||||
} else {
|
||||
$packageRoot = [IO.Path]::GetFullPath($PreparedPackageRoot)
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $packageRoot 'sense.exe'))) { throw 'prepared Sense package is invalid' }
|
||||
$senseCopy = [IO.Path]::GetFullPath((Join-Path $packageRoot '..\..'))
|
||||
Write-Host "Using prepared isolated package: $packageRoot"
|
||||
$preparedInput = [IO.Path]::GetFullPath($PreparedPackageRoot)
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $preparedInput 'sense.exe'))) { throw 'prepared Sense package is invalid' }
|
||||
$packageRoot = Join-Path $temporary 'prepared-package'
|
||||
Copy-Item -LiteralPath $preparedInput -Destination $packageRoot -Recurse
|
||||
Write-Host "Using temporary copy of prepared package: $packageRoot"
|
||||
}
|
||||
|
||||
$pgPort, $sensePort, $rtspPort, $hlsPort, $webrtcPort, $webrtcUDPort, $mediaAPIPort, $onvifPort = Get-UniqueFreePorts 8
|
||||
$tcpPorts = @(Get-UniqueFreePorts 7)
|
||||
$pgPort, $sensePort, $rtspPort, $hlsPort, $webrtcPort, $mediaAPIPort, $onvifPort = $tcpPorts
|
||||
do { $webrtcUDPort = Get-FreeUdpPort } while ($tcpPorts -contains $webrtcUDPort)
|
||||
$script:baseUrl = "http://127.0.0.1:$sensePort"
|
||||
Write-Host "Initializing isolated PostgreSQL on port $pgPort..."
|
||||
& (Join-Path $PostgresBin 'initdb.exe') -D $pgData -U sense_e2e -A trust --encoding=UTF8 --no-locale | Out-Null
|
||||
@@ -162,6 +275,16 @@ try {
|
||||
'rtmp: false', 'srt: false', 'moq: false', 'metrics: false', 'paths:', ' fixture:'
|
||||
) -join "`n"
|
||||
[IO.File]::WriteAllText((Join-Path $packageRoot 'config\mediamtx.yml'), $mediaConfig, (New-Object Text.UTF8Encoding($false)))
|
||||
$preflightOut = Join-Path $temporary 'mediamtx-preflight.out.log'
|
||||
$preflightError = Join-Path $temporary 'mediamtx-preflight.err.log'
|
||||
$preflightBinary = Join-Path $packageRoot 'bin\mediamtx.exe'
|
||||
$preflightConfig = Join-Path $packageRoot 'config\mediamtx.yml'
|
||||
$preflightMedia = Start-Process -FilePath $preflightBinary -ArgumentList $preflightConfig -WorkingDirectory (Split-Path -Parent $preflightConfig) -RedirectStandardOutput $preflightOut -RedirectStandardError $preflightError -WindowStyle Hidden -PassThru
|
||||
Wait-Http -Uri "http://127.0.0.1:$mediaAPIPort/v3/config/global/get" -Process $preflightMedia -Stage 'MediaMTX preflight' -LogPaths @($preflightOut, $preflightError) -Attempts 60
|
||||
Stop-ProcessTree $preflightMedia
|
||||
Wait-Tcp -Port $mediaAPIPort -Open $false -Attempts 40
|
||||
$preflightMedia = $null
|
||||
Write-Host 'MediaMTX package/config preflight passed before managed Sense startup.'
|
||||
|
||||
$jwt = New-RandomText 48
|
||||
$bootstrap = New-RandomText 48
|
||||
@@ -173,6 +296,7 @@ try {
|
||||
$cameraUser = 'fixture_' + (New-RandomText 8)
|
||||
$cameraPassword = New-RandomText 24
|
||||
$database = "host=127.0.0.1 port=$pgPort user=sense_e2e dbname=sense_e2e sslmode=disable"
|
||||
$script:sensitiveValues = @($jwt, $bootstrap, $adminPassword, $credentialKey, $cameraUser, $cameraPassword, $database)
|
||||
$environment = @{
|
||||
SENSE_HOST = '127.0.0.1'; SENSE_PORT = "$sensePort"; SENSE_DATABASE_URL = $database;
|
||||
SENSE_JWT_SECRET = $jwt; SENSE_BOOTSTRAP_TOKEN = $bootstrap;
|
||||
@@ -198,7 +322,7 @@ try {
|
||||
if (-not (Test-Path $fixtureStatus)) { throw 'ONVIF fixture did not become ready' }
|
||||
|
||||
$server = Start-SensePackage $packageRoot
|
||||
Wait-Http "http://127.0.0.1:$mediaAPIPort/v3/config/global/get"
|
||||
Wait-Http -Uri "http://127.0.0.1:$mediaAPIPort/v3/config/global/get" -Process $server -Stage 'MediaMTX API' -LogPaths @($runtimeLog, $runtimeError)
|
||||
$publisherArguments = @(
|
||||
'-hide_banner', '-loglevel', 'error', '-re', '-f', 'lavfi', '-i', 'testsrc=size=640x360:rate=10',
|
||||
'-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency', '-f', 'rtsp', '-rtsp_transport', 'tcp',
|
||||
@@ -214,6 +338,7 @@ try {
|
||||
if ([int]$bootstrapResponse.code -ne 200) { throw 'administrator bootstrap failed' }
|
||||
$login = Invoke-SenseJson POST '/api/v1/login' @{ username = 'acceptance-admin'; password = $adminPassword }
|
||||
$token = [string]$login.token
|
||||
$script:sensitiveValues += $token
|
||||
if ($token.Length -lt 20) { throw 'login did not return a usable token' }
|
||||
$unauthorized = Invoke-SenseJson GET '/api/v1/devices' $null '' 401
|
||||
|
||||
@@ -265,13 +390,12 @@ try {
|
||||
$area = @($areas.data.list | Where-Object id -eq $areaCreated.data.id)[0]
|
||||
if (-not $area.needsRecalibration) { throw 'resolution change did not mark the area for recalibration' }
|
||||
|
||||
$browserScript = Join-Path $senseCopy 'ui\sense-browser-smoke.cjs'
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'browser-smoke.cjs') -Destination $browserScript
|
||||
$browserScript = Join-Path $PSScriptRoot 'browser-smoke.cjs'
|
||||
foreach ($item in @{
|
||||
SENSE_E2E_BASE_URL = $baseUrl; SENSE_E2E_TOKEN = $token; SENSE_E2E_BROWSER = $Browser;
|
||||
SENSE_E2E_SCREENSHOT = (Join-Path $temporary 'sense-browser.png')
|
||||
}.GetEnumerator()) { Set-TestEnvironment $item.Key $item.Value }
|
||||
Push-Location (Join-Path $senseCopy 'ui')
|
||||
Push-Location $temporary
|
||||
try { & node.exe $browserScript } finally { Pop-Location }
|
||||
if ($LASTEXITCODE -ne 0) { throw 'browser GoAdmin shell smoke failed' }
|
||||
|
||||
@@ -299,7 +423,7 @@ try {
|
||||
$plainCredentialCount = (& $psql -X -h 127.0.0.1 -p $pgPort -U sense_e2e -d sense_e2e -tAc "select count(*) from sense_device_credentials where position(convert_to('$cameraPassword','UTF8') in ciphertext) > 0;").Trim()
|
||||
if ([int]$plainCredentialCount -ne 0) { throw 'camera credential appeared in plaintext storage' }
|
||||
|
||||
foreach ($log in @($runtimeLog, $runtimeError, $fixtureLog, $fixtureError, $ffmpegLog, $ffmpegError)) {
|
||||
foreach ($log in @($runtimeLog, $runtimeError, $fixtureLog, $fixtureError, $ffmpegLog, $ffmpegError, $preflightOut, $preflightError)) {
|
||||
if (Test-Path $log) {
|
||||
$text = [string](Get-Content -LiteralPath $log -Raw -ErrorAction SilentlyContinue)
|
||||
if ($null -eq $text) { $text = '' }
|
||||
@@ -311,6 +435,7 @@ try {
|
||||
Stop-ProcessTree $publisher
|
||||
Stop-ProcessTree $fixture
|
||||
Stop-ProcessTree $server
|
||||
Stop-ProcessTree $preflightMedia
|
||||
if ($pgStarted) {
|
||||
$pgStopArguments = "-D `"$pgData`" -m fast stop"
|
||||
[void](Start-Process -FilePath (Join-Path $PostgresBin 'pg_ctl.exe') -ArgumentList $pgStopArguments -RedirectStandardOutput (Join-Path $temporary 'pg-stop.log') -RedirectStandardError (Join-Path $temporary 'pg-stop.err.log') -WindowStyle Hidden -PassThru)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Project-Profile
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Project-Profile.-
|
||||
wiki_revision: 5894b3f4e3152420bd9addd63c1ce80205a6fd80
|
||||
synchronized_at: 2026-08-27T15:22:01Z
|
||||
wiki_revision: 3ec1fe54504a9c5eabb76dc19f0e46eb6c58ba08
|
||||
synchronized_at: 2026-08-29T12:37:08Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 项目档案
|
||||
@@ -19,7 +19,7 @@ synchronized_at: 2026-08-27T15:22:01Z
|
||||
| 首期客户场景 | 民办寄宿学校,默认 16 路高风险点位 |
|
||||
| 首期规则 | 越线、危险区域、聚集等匿名安全规则;不启用人脸 |
|
||||
| 产品形态 | Sense 与 Bell 两个独立销售产品,Brain 为独立推理交付单元 |
|
||||
| 当前阶段 | Sense 独立纵切、Brain Python 骨架、Bell GoAdmin 产品骨架已通过用户验收并合入 `dev`;旧实现归档于 `explore`,`main` 仍为审核基线 |
|
||||
| 当前阶段 | MVP #8 三项目首个独立纵切已于 2026-08-29 通过用户验收并合入 `dev`:Sense 完成摄像头接入到区域配置,Brain 完成合成输入到匿名本地事件,Bell 完成合成事件到 Alert ack/close;旧实现归档于 `explore`,`main` 仍为审核基线 |
|
||||
| 历史来源 | `D:\OPC\yovision_old`,只读追溯 |
|
||||
|
||||
## DevHarness 来源与基线
|
||||
@@ -54,7 +54,7 @@ YoVision 采用 DevHarness 的共同工作流、统一 `harness.py` 命令、Git
|
||||
- 证据:客户侧 MinIO/S3 兼容对象存储;常态录像优先留在客户已有 NVR。
|
||||
- 首期验证平台:NVIDIA x86/Jetson;M1-M3 不承诺 GB/T 28181、信创或原生 App。
|
||||
|
||||
2026-08-14 起,原 Sense、Bell 实现只在 `explore` 和原功能分支中作为迁移参考,不再作为新开发基础。当前 `dev` 中的 Sense、Bell 已分别从下述冻结 go-admin/go-admin-ui 完整提交派生;实施时仍必须核对冻结 go-admin-doc。Brain 已建立独立 Python/PyTorch 包骨架,但尚未包含推理业务能力。
|
||||
2026-08-14 起,原 Sense、Bell 实现只在 `explore` 和原功能分支中作为迁移参考,不再作为新开发基础。当前 `dev` 中的 Sense、Bell 已分别从下述冻结 go-admin/go-admin-ui 完整提交派生;实施时仍必须核对冻结 go-admin-doc。Brain 已在独立 Python/PyTorch 包骨架上完成合成/本地输入、解码、匿名检测与单路跟踪、区域/方向越线判定和项目内匿名事件输出;真实 GPU、生产模型和跨项目契约仍属后续范围。
|
||||
|
||||
## 阅读入口
|
||||
|
||||
@@ -108,20 +108,20 @@ Sense、Bell 共用的可复现技术基线记录在仓库根 `goadmin-baseline.
|
||||
<!-- sense-runtime:start -->
|
||||
## Sense 重建状态
|
||||
|
||||
Sense 已从冻结 go-admin/go-admin-ui 源码独立派生,并完成设备、视频接入、MediaMTX、单路监看、区域配置与 Windows 交付的独立纵切。工单 #71 已从当前源码重新打包并通过隔离 PostgreSQL 17、Digest ONVIF/合成 RTSP、独立 MediaMTX、Chrome 外壳和冷启动回归;当前成果已合入 `dev`,并于 2026-08-27 通过用户验收。现场真机、16 路长稳和跨项目链路不在本轮结论内。
|
||||
Sense 已从冻结 go-admin/go-admin-ui 源码独立派生,并完成设备、视频接入、MediaMTX、单路监看、区域配置与 Windows 交付的独立纵切。工单 #71 已从当前源码重新打包并通过隔离 PostgreSQL 17、Digest ONVIF/合成 RTSP、独立 MediaMTX、Chrome 外壳和冷启动回归;#145 又修复默认验收入口的受控源码复制、UDP 端口探测、临时清理和脱敏诊断。当前成果已合入 `dev`,并随 MVP #8 于 2026-08-29 通过三项目独立纵切验收。现场真机、16 路长稳和跨项目链路不在本轮结论内。
|
||||
<!-- sense-runtime:end -->
|
||||
|
||||
|
||||
<!-- bell-runtime:start -->
|
||||
## Bell 重建状态
|
||||
|
||||
Bell 已从与 Sense 相同的冻结 go-admin/go-admin-ui 基线独立派生到 `Bell/server/` 与 `Bell/ui/`,保留来源和 MIT 许可证证据,以及独立 PostgreSQL、JWT、token key 和首次管理员边界。当前最小启用骨架已通过后端、前端和隔离 PostgreSQL smoke,并于 2026-08-27 通过用户验收、合入 `dev`;事件、规则、Alert 等业务能力继续按独立工单迁移。
|
||||
Bell 已从与 Sense 相同的冻结 go-admin/go-admin-ui 基线独立派生到 `Bell/server/` 与 `Bell/ui/`,保留来源和 MIT 许可证证据,以及独立 PostgreSQL、JWT、token key 和首次管理员边界。#131–#134 已完成 Event/Receipt、合成事件、规则匹配、Alert ack/close、审计时间线、Windows 交付和独立 E2E;生产验证码、最小菜单和 GoAdmin 外壳缺陷也已闭环。当前成果已合入 `dev`,并随 MVP #8 于 2026-08-29 通过三项目独立纵切验收。
|
||||
<!-- bell-runtime:end -->
|
||||
|
||||
<!-- brain-runtime:start -->
|
||||
## Brain 初始化状态
|
||||
|
||||
Brain 已建立 CPython 3.11.15 / PyTorch 2.12.1 的无界面包骨架,提供安装、版本、runtime-info 与 CPU/CUDA smoke 入口。CPU wheel、包测试和 CPU tensor smoke 已通过,并于 2026-08-27 通过用户验收、合入 `dev`;CUDA wheel、真实 GPU、视频、模型、规则、事件与部署尚未验证或实现。
|
||||
Brain 已在 CPython 3.11.15 / PyTorch 2.12.1 无界面包骨架上完成合成与本地视频输入、可替换解码、匿名检测与单路跟踪、危险区域与方向越线判定,以及项目内匿名事件输出。独立验收中 43 项测试通过,CLI 合成输入实际生成 `brain.internal.event-candidate/v1` 匿名事件;当前成果已合入 `dev`,并随 MVP #8 于 2026-08-29 通过用户验收。CUDA wheel、真实 GPU、生产模型、容量和跨项目事件契约仍未验证。
|
||||
<!-- brain-runtime:end -->
|
||||
|
||||
## 分支治理
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Architecture-and-Code-Map
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Architecture-and-Code-Map.-
|
||||
wiki_revision: 578ddbaae3d7e846037d958085e40609cd398bef
|
||||
synchronized_at: 2026-08-28T08:02:32Z
|
||||
wiki_revision: 14b961599d6954357142713a5667fb37d38e86b7
|
||||
synchronized_at: 2026-08-29T12:37:31Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 架构与代码地图
|
||||
@@ -236,3 +236,63 @@ PostgreSQL 表 `sense_provisioning_batches` 保存幂等键、配额快照和汇
|
||||
- GoAdmin 路由位于 `Sense/server/app/admin/router/sense_media_shard.go`,只开放列表、详情和迁移预检三个 GET 接口。go-admin-ui 页面位于 `Sense/ui/src/views/sense/media-shard/`,复用 BasicLayout、Element Plus 表格、进度、Dialog、Tag、Alert 和权限指令。
|
||||
- 迁移 `2026082814000_media_shard.go` 建表、注册动态菜单并为 implementation_operator、site_admin、viewer 建立只读权限;生产迁移和启动不写入合成分片。
|
||||
<!-- sense-media-shards:end -->
|
||||
|
||||
<!-- sense-outbox:start -->
|
||||
## Sense 内部可靠投递入口
|
||||
|
||||
工单 #78 在 `Sense/server/app/sense/outbox/` 建立内部事务 Outbox。业务写入通过同一 GORM 事务创建领域记录与 outbox;`Sense/server/app/sense/local_event/outbox.go` 是当前首个原子写入入口。GoAdmin 路由位于 `Sense/server/app/admin/router/sense_outbox.go`,迁移与菜单/RBAC 位于 `Sense/server/cmd/migrate/migration/version/2026082815000_outbox.go`,前端页面位于 `Sense/ui/src/views/sense/outbox/index.vue`。
|
||||
|
||||
内部状态为 pending、processing、retry、dead、delivered。relay 使用数据库 claim、lease 和版本号避免并发重复领取;失败按退避进入 retry,超过上限进入 dead,租约过期可恢复。成功投递写入永久幂等收据。当前模块不定义 Brain/Bell 正式 schema、connector 或机器身份,内部 payload 也不通过管理 API 暴露。
|
||||
<!-- sense-outbox:end -->
|
||||
|
||||
<!-- sense-ops-alerts:start -->
|
||||
## Sense 运维告警代码路径
|
||||
|
||||
工单 #79 在 `Sense/server/app/sense/ops_alert/` 建立持久化运维告警:`sense_ops_alerts` 以“告警类型 + 对象类型 + 对象 ID”唯一指纹保存当前生命周期,`sense_ops_alert_transitions` 追加发现、确认、健康恢复、恢复确认、恢复失败和再次发生历史。健康事实只读取既有设备接入、媒体路由、媒体分片和边缘节点投影,不建立第二套设备或媒体状态事实源。
|
||||
|
||||
GoAdmin 路由位于 `Sense/server/app/admin/router/sense_ops_alert.go`,API 为 `GET /api/v1/ops-alerts`、`GET /api/v1/ops-alerts/:id`、`POST /api/v1/ops-alerts/evaluate`、`POST /api/v1/ops-alerts/:id/acknowledge` 和 `POST /api/v1/ops-alerts/:id/recover`。前端入口为 `Sense/ui/src/views/sense/ops-alert/index.vue`,继续复用 GoAdmin BasicLayout、动态菜单、Axios、Element Plus 表格/表单/分页/Dialog/Tag/Alert 和权限指令。
|
||||
|
||||
本模块只写 Sense 运维告警和 GoAdmin 操作审计,不导入或写入本地安全事件、Brain、Bell、Outbox 或共享契约模型。viewer 只读;implementation_operator 与 site_admin 可刷新健康事实、确认和恢复。
|
||||
<!-- sense-ops-alerts:end -->
|
||||
|
||||
<!-- brain-input-v1:start -->
|
||||
## Brain 内部输入与配置边界
|
||||
|
||||
Brain 的首个独立输入边界位于 `Brain/src/yovision_brain/input/`,项目内配置模型位于 `Brain/src/yovision_brain/config/`。配置显式标记为 `brain.internal.input/v1`,只用于 Brain 独立开发与测试,不是 Sense→Brain 共享契约。
|
||||
|
||||
输入端口当前提供确定性 RGB 合成源和显式本地文件源。两者携带逻辑设备、Profile 与分辨率元数据;合成源提供固定种子、帧序列和确定性时间基准,本地文件源提供可替换解码器消费的容器字节、EOF 和协作取消边界。错误只暴露安全文件标签,不把机器绝对路径、凭据或客户数据写入日志/事件。
|
||||
|
||||
正式 RTSP、Sense 源配置、共享区域契约和跨项目投递仍由协调工单建立版本化 `contracts/` 适配器,不得把本内部模型直接发布给 Sense 或 Bell。
|
||||
<!-- brain-input-v1:end -->
|
||||
|
||||
<!-- brain-decode-v1:start -->
|
||||
## Brain 可替换解码边界
|
||||
|
||||
Brain 解码层位于 `Brain/src/yovision_brain/decode/`,只依赖 #11 的内部 `InputPacket` 端口,向后续视觉模块输出顺序、纳秒时间戳、逻辑设备、Profile、分辨率、像素格式和尺寸变化标记明确的 `DecodedFrame`。具体后端通过 `DecoderBackend` 注册,不要求检测、跟踪或规则层依赖某个编解码 SDK。
|
||||
|
||||
当前独立纵切支持确定性 RGB24 合成帧,以及标准库实现的最小 YUV4MPEG2 C444 本地视频流。Y4M 只用于匿名本地/合成验证;生产 RTSP、FFmpeg/PyAV、NVIDIA 硬件解码、重连和多路调度仍是后续范围。损坏输入、不支持格式、Profile 尺寸不匹配和安全大小上限均产生明确错误;正常 EOF 与主动取消不伪装成失败。
|
||||
<!-- brain-decode-v1:end -->
|
||||
|
||||
<!-- brain-vision-v1:start -->
|
||||
## Brain 匿名检测与单路跟踪边界
|
||||
|
||||
`Brain/src/yovision_brain/vision/` 定义可替换 Detector、匿名边界框观测和会话内单路 IoU 跟踪。输出仅包含类别 `anonymous_target`、置信度、边界框、帧时间和当前进程内轨迹 ID;轨迹 ID 不跨进程、不跨摄像头,也不是自然人身份。
|
||||
|
||||
当前基线是版本 `1.0.0` 的 YoVision first-party 亮度连通区域算法,并提供 PyTorch 2.12.1 张量实现;不分发外部模型权重,PyTorch 许可已在 Brain 第三方清单记录。它用于验证匿名检测/跟踪链路,不代表人员检测效果,不承诺召回率或误报率。人脸、生物特征和跨摄像头 ReID 均未启用。
|
||||
<!-- brain-vision-v1:end -->
|
||||
|
||||
<!-- brain-rules-v1:start -->
|
||||
## Brain 区域与方向越线规则边界
|
||||
|
||||
`Brain/src/yovision_brain/rules/` 只消费匿名轨迹。轨迹框底边中心是归一化规则锚点;多边形边界视为区域内,状态区分 outside、entered、inside。有向警戒线按起点→终点的左右侧定义 `left_to_right` / `right_to_left`,deadband 内不触发且保留上一次显著侧。
|
||||
|
||||
每个结果绑定规则配置版本、Profile、分辨率、锚点和可解释原因。结果是 Brain 内部候选,不是标准事件或 Bell Alert;时段、持续时间、冷却、聚集和正式 Sense 配置契约不在本阶段。
|
||||
<!-- brain-rules-v1:end -->
|
||||
|
||||
<!-- brain-local-events-v1:start -->
|
||||
## Brain 独立纵切与内部事件边界
|
||||
|
||||
`Brain/src/yovision_brain/app/` 编排输入、解码、匿名检测/跟踪和规则端口;`Brain/src/yovision_brain/events/` 将触发结果映射为 `brain.internal.event-candidate/v1` 并写入可替换 JSON Lines sink。事件 ID 基于规范化输入事实与版本的 SHA-256,同一输入、配置和实现版本重复运行保持稳定。
|
||||
|
||||
内部候选包含逻辑输入引用、规则/模型版本、发生时间、匿名框和解释原因,不包含摄像头凭据、客户隐私、人脸、生物特征、机器绝对路径或证据引用。该格式不是 Brain→Bell 共享契约;Bell API、Outbox、机器身份、证据和跨项目投递必须由协调工单另行实现。
|
||||
<!-- brain-local-events-v1:end -->
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: 999eb1aee3558ff75cfa929acac77841af20b742
|
||||
synchronized_at: 2026-08-28T08:02:42Z
|
||||
wiki_revision: 27749cbf699093d997284afc277ea53e73a5876f
|
||||
synchronized_at: 2026-08-29T12:37:41Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
@@ -210,3 +210,26 @@ synchronized_at: 2026-08-28T08:02:42Z
|
||||
- 跨分片迁移预检是只读操作,只检查源状态、全部受影响路径和候选目标容量;即使预检通过也不授予执行权限。实际迁移必须另建高风险工单并取得人工确认。
|
||||
- 额外分片只能配置为 external,Control API 必须使用无用户信息、无查询参数、无路径的本机回环 HTTP 地址。Sense 不停止外部实例。
|
||||
<!-- sense-media-shards:end -->
|
||||
|
||||
<!-- sense-outbox:start -->
|
||||
## Sense 内部 Outbox 业务规则
|
||||
|
||||
- 领域记录与 outbox 必须在同一 PostgreSQL 事务中提交;任一写入失败时两者一起回滚。
|
||||
- 幂等键在消息表唯一,成功后还保留永久投递收据;重试和人工恢复沿用原业务记录与幂等键。
|
||||
- worker 只能领取到期的 pending/retry 或租约已过期的 processing 记录;同一记录不能被两个 worker 同时成功领取。
|
||||
- 失败保留脱敏错误与尝试历史,按退避等待;达到最大次数进入 dead。人工重新排队必须填写原因并记录操作者,不删除历史。
|
||||
- implementation_operator、site_admin、viewer 可查看;只有 implementation_operator、site_admin 可重新排队。
|
||||
- 未配置外部 connector 时保留内部记录且不阻断 Sense 核心功能。测试 sink 在 prod/production 模式禁止启用。
|
||||
- 管理 API 不返回内部 payload、外部凭据或机器身份;Brain/Bell 正式协议属于后续协调工单。
|
||||
<!-- sense-outbox:end -->
|
||||
|
||||
<!-- sense-ops-alerts:start -->
|
||||
## Sense 运维告警规则
|
||||
|
||||
- 六类运维告警固定为:设备/边缘节点离线、设备认证失败、设备时间漂移、媒体状态对账失败、媒体分片异常、控制隧道异常。
|
||||
- 每个“告警类型 + 对象类型 + 对象 ID”只有一条记录;同一源版本重复刷新不增加发现次数,也不产生第二条活动告警。恢复后再次异常复用原记录、递增处理周期并保留全部历史。
|
||||
- 状态为 `unacknowledged`(待确认)、`acknowledged`(已确认)、`recovering`(恢复观察)、`recovered`(已恢复)。人工确认只表示已接手,不表示故障恢复。
|
||||
- 健康事实恢复后先进入固定 5 分钟观察窗口;只有 `recovering` 且观察窗口结束后才能人工确认恢复。观察期再次异常返回原处理状态并追加恢复失败历史。
|
||||
- 确认和恢复都要求 6–256 字符原因、当前版本和允许的状态;旧版本或错误状态返回冲突。所有动作写入独立流转历史和 GoAdmin 操作审计。
|
||||
- 运维告警永远设置为 Sense 内部运维记录,不创建本地安全事件或 Bell Alert,不进入跨项目 Outbox,也不实现通知升级。
|
||||
<!-- sense-ops-alerts:end -->
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Local-Development-and-Verification
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Local-Development-and-Verification.-
|
||||
wiki_revision: 05435d53528271a866c525655486689afc198762
|
||||
synchronized_at: 2026-08-28T08:02:52Z
|
||||
wiki_revision: e6068ff0e42765d32ff4e0ee0e8e51cf7d79b7da
|
||||
synchronized_at: 2026-08-29T12:37:58Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 本地开发与验证
|
||||
@@ -515,3 +515,112 @@ go test ./cmd/migrate/migration/version -run TestMediaShardMigrationOnPostgres -
|
||||
|
||||
验证应覆盖任意配置容量、稳定重复分配、容量耗尽、分片故障后归属不变、设备/Profile/路径影响范围、只读迁移预检、只读 RBAC、Control API 不出现在响应,以及 Brain/Bell 均不运行。没有专用 PostgreSQL 连接时必须记录真库迁移测试未执行。
|
||||
<!-- sense-media-shards:end -->
|
||||
|
||||
<!-- sense-outbox:start -->
|
||||
## Sense Outbox 本地验证
|
||||
|
||||
从 `Sense/server` 运行完整后端测试:
|
||||
|
||||
```powershell
|
||||
go test ./...
|
||||
```
|
||||
|
||||
PostgreSQL 多 worker 集成测试必须使用专用隔离数据库,不得指向开发或生产库:
|
||||
|
||||
```powershell
|
||||
$env:SENSE_OUTBOX_TEST_DATABASE_URL = '<隔离 PostgreSQL 连接>'
|
||||
go test ./app/sense/outbox -run TestPostgresConcurrentWorkersDoNotClaimSameMessage -count=1 -v
|
||||
```
|
||||
|
||||
前端从 `Sense/ui` 运行:
|
||||
|
||||
```powershell
|
||||
pnpm lint
|
||||
pnpm test:unit -- --runInBand
|
||||
pnpm build:prod
|
||||
```
|
||||
|
||||
验证至少覆盖:领域记录与 outbox 原子回滚、并发 claim/lease、租约恢复、退避与 dead、人工重新排队及操作人、永久幂等收据、production 禁用测试 sink、只读/恢复权限、API 不泄露 payload,以及 Brain/Bell 均不运行时页面可观察。无专用 PostgreSQL 连接时必须明确记录真库并发测试未执行。
|
||||
<!-- sense-outbox:end -->
|
||||
|
||||
<!-- sense-ops-alerts:start -->
|
||||
## Sense 运维告警验证
|
||||
|
||||
从后端目录运行:
|
||||
|
||||
```powershell
|
||||
cd Sense/server
|
||||
go test ./app/sense/ops_alert ./app/admin/router ./cmd/migrate/migration/version
|
||||
go test ./...
|
||||
```
|
||||
|
||||
从前端目录运行:
|
||||
|
||||
```powershell
|
||||
cd Sense/ui
|
||||
pnpm lint
|
||||
pnpm test:unit -- --runInBand
|
||||
pnpm build:prod
|
||||
```
|
||||
|
||||
故障注入至少覆盖六类来源、相同源版本重复刷新、健康恢复、5 分钟观察门槛、观察期复发、恢复后再次发生、旧版本并发冲突、viewer 只读权限和脱敏操作审计。Brain/Bell 不启动。隔离启动 smoke 必须验证 `2026082816000_ops_alert.go` 迁移、菜单和 API 注册;不得把开发或生产数据库当作破坏性故障注入库。
|
||||
<!-- sense-ops-alerts:end -->
|
||||
|
||||
<!-- brain-input-v1:start -->
|
||||
## Brain 合成与本地输入验证
|
||||
|
||||
从仓库根目录使用 Brain 的隔离 CPython 3.11 环境执行:
|
||||
|
||||
```powershell
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests/input Brain/tests/config -q
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests -q
|
||||
```
|
||||
|
||||
定向测试覆盖固定种子与时间基准、Profile/分辨率和规则配置、EOF、取消、文件不存在、非法配置、凭据字段拒绝及安全错误文本。测试只使用运行时生成的小型匿名字节文件,不启动 Sense/Bell,不连接摄像头或网络服务。
|
||||
<!-- brain-input-v1:end -->
|
||||
|
||||
<!-- brain-decode-v1:start -->
|
||||
## Brain 视频解码验证
|
||||
|
||||
```powershell
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests/decode -q
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests -q
|
||||
```
|
||||
|
||||
定向测试使用运行时生成的匿名 YUV4MPEG2 字节流,覆盖跨输入分块解码、顺序与时间戳、Profile/分辨率、RGB24 尺寸变化、正常 EOF、主动取消、截断帧、不支持格式/色度和配置尺寸不匹配。该结果不证明生产 RTSP、硬件解码、GPU 或多路性能。
|
||||
<!-- brain-decode-v1:end -->
|
||||
|
||||
<!-- brain-vision-v1:start -->
|
||||
## Brain 匿名检测与跟踪验证
|
||||
|
||||
```powershell
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests/vision -q
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests -q
|
||||
Brain\.venv\Scripts\python.exe -m yovision_brain --smoke cpu
|
||||
```
|
||||
|
||||
定向测试覆盖空帧、目标出现/移动、短暂遮挡、消失、轨迹结束、会话 ID 边界及 PyTorch CPU 后端。合成几何帧不含人脸或客户数据;结果只证明链路可运行,不是效果评估。
|
||||
<!-- brain-vision-v1:end -->
|
||||
|
||||
<!-- brain-rules-v1:start -->
|
||||
## Brain 区域与方向越线验证
|
||||
|
||||
```powershell
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests/rules -q
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests -q
|
||||
```
|
||||
|
||||
定向测试覆盖区域外/进入/内部、边界点、正反方向、贴线 deadband、无效多边形/警戒线、重复 ID 和 Profile/分辨率不匹配;只使用合成归一化几何与匿名轨迹。
|
||||
<!-- brain-rules-v1:end -->
|
||||
|
||||
<!-- brain-local-events-v1:start -->
|
||||
## Brain 独立纵切运行与验证
|
||||
|
||||
```powershell
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests/events Brain/tests/app -q
|
||||
Brain\.venv\Scripts\python.exe -m pytest Brain/tests -q
|
||||
Brain\.venv\Scripts\python.exe -m yovision_brain.app --config Brain\tests\fixtures\events\area.json --output -
|
||||
```
|
||||
|
||||
CLI 将内部事件 JSON Lines 写入 stdout,并把 completed/cancelled、帧数、检测数和事件数摘要写入 stderr。配置文件必须显式提供,当前使用 JSON;无命中正常返回零事件,读取/配置/模块失败返回非零且不回显机器路径。命令不启动 Sense/Bell、不连接摄像头或网络。
|
||||
<!-- brain-local-events-v1:end -->
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Product-Requirements
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Product-Requirements.-
|
||||
wiki_revision: dcbdcf563017a1749fa76ad5f78c74a2c3cc6be1
|
||||
synchronized_at: 2026-08-28T06:16:00Z
|
||||
wiki_revision: eac307b5aa55ff770ff034c53a65b70dc01cb00d
|
||||
synchronized_at: 2026-08-29T12:39:32Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 产品需求
|
||||
@@ -232,3 +232,43 @@ Sense 为网管和非技术运维人员提供只读的“边缘节点”页面
|
||||
|
||||
本能力只管理 Sense 自有投影,不建立 Brain/Bell 共享身份、控制协议或跨项目回填执行;Brain、Bell 未运行时仍可独立查看。合成节点仅供显式开发和测试,不由生产启动或迁移自动写入。
|
||||
<!-- sense-edge-nodes:end -->
|
||||
|
||||
<!-- brain-input-delivery:start -->
|
||||
## BRN-001 独立输入适配交付边界
|
||||
|
||||
BRN-001 的首个独立实现已通过工单 #11 验收。Brain 可在 Sense、Bell 均未启动时使用固定种子和时间基准生成可重复的 RGB 合成帧,也可从显式本地路径读取容器字节供后续解码层消费;两种输入都携带 Brain 内部逻辑设备、Profile 和分辨率信息,并支持 EOF、协作取消及安全可定位错误。
|
||||
|
||||
项目内配置版本为 `brain.internal.input/v1`,可承载测试用区域和方向线,但它不是 Sense→Brain 共享契约。正式 RTSP、Sense 源/区域配置和跨项目机器身份仍须由协调工单在版本化 `contracts/` 中冻结;不得让 Sense 或 Bell 直接依赖此内部模型。配置和测试不得包含摄像头凭据、客户视频、个人数据或机器绝对路径。
|
||||
<!-- brain-input-delivery:end -->
|
||||
|
||||
<!-- brain-decode-delivery:start -->
|
||||
## BRN-002 独立解码交付边界
|
||||
|
||||
BRN-002 的首个解码阶段已通过工单 #13 验收。Brain 通过可替换 `DecoderBackend` 把内部输入转换为顺序、纳秒时间戳、逻辑设备、Profile、分辨率和像素格式明确的帧;当前独立路径支持确定性 RGB24 与匿名本地 YUV4MPEG2 C444。正常 EOF、主动取消、损坏或不支持格式、尺寸变化/不匹配均有明确结果。
|
||||
|
||||
该验收不包括生产 RTSP、FFmpeg/PyAV、NVIDIA 硬件解码、多路性能或客户视频,不得据此声明 GPU/生产编解码能力。
|
||||
<!-- brain-decode-delivery:end -->
|
||||
|
||||
<!-- brain-vision-delivery:start -->
|
||||
## BRN-002 匿名检测与跟踪交付边界
|
||||
|
||||
工单 #14 已验收匿名目标检测和会话内单路跟踪。输出只包含匿名类别、置信度、边界框、帧时间和当前进程内轨迹 ID;不包含姓名、人脸模板、生物特征、摄像头凭据或跨摄像头身份。
|
||||
|
||||
当前版本化基线是无外部权重的 first-party 亮度目标算法及 PyTorch 2.12.1 张量后端,只证明匿名检测/跟踪接口与链路可运行。真实人员检测效果、GPU、召回率、误报率和 ReID 均未验证或启用。
|
||||
<!-- brain-vision-delivery:end -->
|
||||
|
||||
<!-- brain-rules-delivery:start -->
|
||||
## BRN-003/BRN-004 区域与方向规则交付边界
|
||||
|
||||
工单 #15 已验收 Brain 内部危险区域与方向越线判定。轨迹框底边中心为归一化锚点;多边形边界视为区域内,状态区分 outside、entered、inside;有向线按起点→终点区分左右方向,并使用 deadband 抑制贴线抖动。
|
||||
|
||||
每个结果绑定规则配置版本、Profile、分辨率和解释原因。结果仍是 Brain 内部候选,不是 Bell Alert 或正式共享事件;聚集、完整时段/持续/冷却和正式 Sense 配置契约仍是后续范围。
|
||||
<!-- brain-rules-delivery:end -->
|
||||
|
||||
<!-- brain-local-events-delivery:start -->
|
||||
## BRN-005 独立内部事件候选交付边界
|
||||
|
||||
工单 #16 已验收 Brain 首个独立纵切:合成/本地输入经过解码、匿名检测/单路跟踪和区域/方向规则后,可输出 `brain.internal.event-candidate/v1` JSON Lines 候选。事件 ID 基于规范化输入事实与版本生成稳定 SHA-256;相同输入、配置和版本重复运行不制造不同 ID。
|
||||
|
||||
内部候选只含逻辑输入引用、规则/模型版本、发生时间、匿名观测和解释原因,不含摄像头凭据、客户隐私、人脸、生物特征、机器绝对路径或伪造证据。该格式不是正式 Brain→Bell 契约;证据、机器身份、Outbox/可靠投递和跨项目 E2E 仍须协调工单实现。
|
||||
<!-- brain-local-events-delivery:end -->
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Product-Roadmap
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Product-Roadmap.-
|
||||
wiki_revision: 2d5b8550109a8ff0795ad46dd0f96c85929fb506
|
||||
synchronized_at: 2026-08-11T10:31:04Z
|
||||
wiki_revision: 5142de162b4665bd7c9ff201168cb0d4a7552f35
|
||||
synchronized_at: 2026-08-29T12:40:13Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 产品路线图
|
||||
@@ -84,3 +84,40 @@ Sense/Brain Event → 持久 Outbox/可靠投递
|
||||
- 契约未冻结却尝试共享数据库、用户会话或内部文件;
|
||||
- 新纵切尚未验收却删除或覆盖旧仓库;
|
||||
- 许可证、隐私或客户/法务门禁未满足却进入生产试点。
|
||||
|
||||
<!-- brain-input-delivery:start -->
|
||||
## Brain 独立纵切进度
|
||||
|
||||
- 工单 #11 已验收:确定性合成输入、本地文件输入和 Brain 内部版本化配置已合入 `dev`。
|
||||
- 下一项按真实依赖进入 #13 视频解码流水线;#14 检测/跟踪、#15 区域/越线和 #16 项目内匿名事件仍需依次完成。
|
||||
- 当前输入模型只用于 Brain 独立纵切,不代替阶段 2 的 Sense→Brain 正式契约。
|
||||
<!-- brain-input-delivery:end -->
|
||||
|
||||
<!-- brain-decode-delivery:start -->
|
||||
## Brain 解码进度
|
||||
|
||||
- 工单 #13 已验收:可替换解码端口、RGB24 和匿名本地 YUV4MPEG2 路径已合入 `dev`。
|
||||
- 下一项进入 #14 匿名检测与单路跟踪;#15、#16 仍按依赖顺序推进。
|
||||
<!-- brain-decode-delivery:end -->
|
||||
|
||||
<!-- brain-vision-delivery:start -->
|
||||
## Brain 匿名视觉进度
|
||||
|
||||
- 工单 #14 已验收并合入 `dev`;下一项进入 #15 区域与方向越线规则。
|
||||
- 当前基线不代表生产模型效果,#16 项目内事件仍未完成。
|
||||
<!-- brain-vision-delivery:end -->
|
||||
|
||||
<!-- brain-rules-delivery:start -->
|
||||
## Brain 规则进度
|
||||
|
||||
- 工单 #15 已验收并合入 `dev`;下一项进入 #16 独立纵切与内部匿名事件。
|
||||
- #16 完成前,Brain 首个独立纵切仍未闭环。
|
||||
<!-- brain-rules-delivery:end -->
|
||||
|
||||
<!-- brain-local-events-delivery:start -->
|
||||
## Brain 首个独立纵切完成状态
|
||||
|
||||
- #10、#11、#13、#14、#15、#16 已全部通过用户验收。
|
||||
- Brain 可在 Sense/Bell 未启动时,以合成输入产生稳定的项目内匿名区域事件。
|
||||
- 下一步是 MVP #8 三项目独立纵切集成验收;正式跨项目契约与投递不属于该 MVP。
|
||||
<!-- brain-local-events-delivery:end -->
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Deployment-and-Operations
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Deployment-and-Operations.-
|
||||
wiki_revision: ce246054849b5b797dc4da0a55116238a4c00d7e
|
||||
synchronized_at: 2026-08-28T08:04:52Z
|
||||
wiki_revision: b21bbc64f323f465b78b557537c38e334de31f45
|
||||
synchronized_at: 2026-08-29T12:41:01Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# YoVision 部署与运维
|
||||
@@ -112,3 +112,25 @@ Sense\start_sense.bat
|
||||
|
||||
页面“运行异常”表示最近一次 Control API 探测失败;“状态已陈旧”表示运行循环已超过 30 秒没有更新探测结果。故障时先在详情定位设备/Profile/路径,不要手工改数据库归属。迁移预检不会执行迁移;任何实际跨分片迁移都必须另建高风险工单和回退方案。
|
||||
<!-- sense-media-shards:end -->
|
||||
|
||||
<!-- sense-outbox:start -->
|
||||
## Sense 可靠投递运维与排错
|
||||
|
||||
升级后应执行包含 `2026082815000_outbox.go` 的数据库迁移。看不到“可靠投递”菜单时,先确认迁移成功,再重新登录或刷新动态菜单。页面提供等待投递、重试、处理中/租约和死信数量;未配置正式 connector 时队列保留,不影响 Sense 设备接入、实时监看和其他核心能力。
|
||||
|
||||
积压时先查看状态、可用时间、租约、尝试次数和最近脱敏错误。processing 长时间不恢复时检查 worker 是否仍运行、数据库时间与租约是否过期;不要手工清空租约或删除消息。dead 只能由 implementation_operator 或 site_admin 在排除根因后填写恢复原因重新排队,原业务记录、幂等键和失败历史必须保留。
|
||||
|
||||
日志、页面和 API 不得输出内部 payload、外部凭据或机器身份。production 配置不得启用测试 sink。正式 Brain/Bell connector、机器身份、共享 schema 和跨项目 E2E 必须通过后续协调工单交付;停用 relay 可以作为回退,但不得删除未投递记录或永久幂等收据。
|
||||
<!-- sense-outbox:end -->
|
||||
|
||||
<!-- sense-ops-alerts:start -->
|
||||
## Sense 运维告警运行与排错
|
||||
|
||||
升级后必须执行包含 `2026082816000_ops_alert.go` 的数据库迁移。看不到“运维告警”菜单时,先确认迁移成功,再重新登录或刷新动态菜单。viewer 只能查看列表和详情;implementation_operator、site_admin 可使用“刷新状态”、确认和恢复。
|
||||
|
||||
“刷新状态”只读取 Sense 数据库中已有的设备接入、媒体路由、媒体分片和边缘节点健康投影。没有对应健康投影时不会伪造演示告警;先检查上游模块是否已完成探测或心跳入库。分片超过 30 秒没有探测、节点超过 90 秒没有心跳会被判定异常。
|
||||
|
||||
确认后仍显示活动告警是正常行为:确认只代表有人处理。源状态健康后进入“恢复观察”,稳定满 5 分钟才能确认恢复;期间复发会返回待确认或已确认。恢复操作被拒绝时先刷新列表,检查健康状态、观察起始时间和页面版本,不要手工改表或删除历史。
|
||||
|
||||
运维告警排错不得粘贴设备地址、Stream URI、摄像头凭据、JWT、Cookie 或数据库连接。需要回退时可停止使用刷新/处置入口,但不得删除 `sense_ops_alerts` 或 `sense_ops_alert_transitions` 历史;规则语义变化必须另建工单。
|
||||
<!-- sense-ops-alerts:end -->
|
||||
|
||||
Reference in New Issue
Block a user