2331 lines
110 KiB
PowerShell
2331 lines
110 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$PackageRoot = $PSScriptRoot,
|
|
[string]$PackageArchivePath = '',
|
|
[AllowEmptyString()]
|
|
[ValidatePattern('^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$')]
|
|
[string]$ExpectedPackageVersion = '',
|
|
[Parameter(Mandatory = $true)][string]$SpritePath,
|
|
[Parameter(Mandatory = $true)][string]$SpriteLicenseEvidence,
|
|
[Parameter(Mandatory = $true)][string]$AstrBotComplianceEvidence,
|
|
[Parameter(Mandatory = $true)][string]$MiniMaxServiceComplianceEvidence,
|
|
[Parameter(Mandatory = $true)][string]$MiniMaxVisionProbeEvidence,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^[A-Fa-f0-9]{40}$')]
|
|
[string]$HostCertificateThumbprint,
|
|
[string]$AstrBotBaseUrl = 'http://127.0.0.1:6185',
|
|
[ValidatePattern('^[A-Za-z0-9_.-]{1,128}$')]
|
|
[string]$CredentialTarget = 'Langsu.Lserp.AstrBot.ApiKey',
|
|
[string]$BridgeDiscoveryDirectory = "$env:LOCALAPPDATA\Langsu\Lserp\AgentBridge",
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateRange(1, 2147483647)]
|
|
[int]$ErpProcessId,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
|
|
[string]$ExpectedDatabaseScopeFingerprint,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateLength(1, 256)]
|
|
[string]$ExpectedUserId,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateLength(1, 256)]
|
|
[string]$ExpectedUserName,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateLength(1, 256)]
|
|
[string]$ExpectedAccountBook,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateLength(1, 256)]
|
|
[string]$ExpectedSubSystemId,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet('true', 'false')]
|
|
[string]$ExpectedIsAdministrator,
|
|
[string]$LegacyArtifactRoot = '',
|
|
[Parameter(Mandatory = $true)][string]$RolloutPolicyPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$')]
|
|
[string]$RolloutCustomerId,
|
|
[string]$ReportDirectory = "$env:LOCALAPPDATA\Langsu\Lserp\AcceptanceReports"
|
|
)
|
|
|
|
Set-StrictMode -Version 2.0
|
|
$ErrorActionPreference = 'Stop'
|
|
$expectedAdministrator = $ExpectedIsAdministrator -ieq 'true'
|
|
$checks = New-Object System.Collections.Generic.List[object]
|
|
|
|
function Add-Check([string]$Name, [bool]$Passed, [string]$Code, [string]$Detail) {
|
|
$resultCode = if ($Passed) { 'ok' } else { $Code }
|
|
$checks.Add([ordered]@{
|
|
name = $Name
|
|
passed = $Passed
|
|
code = $resultCode
|
|
detail = $Detail
|
|
})
|
|
}
|
|
|
|
function Test-RegularFile([string]$Path, [long]$MaximumBytes) {
|
|
if (-not [IO.File]::Exists($Path)) { return $false }
|
|
$item = Get-Item -LiteralPath $Path -Force
|
|
return $item.Length -gt 0 -and $item.Length -le $MaximumBytes -and
|
|
(($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0)
|
|
}
|
|
|
|
function Get-RegularFileSha256([string]$Path, [long]$MaximumBytes) {
|
|
$stream = $null
|
|
$sha256 = $null
|
|
try {
|
|
if (-not (Test-RegularFile $Path $MaximumBytes)) { return $null }
|
|
$stream = [IO.File]::Open(
|
|
$Path,
|
|
[IO.FileMode]::Open,
|
|
[IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read)
|
|
if ($stream.Length -le 0 -or $stream.Length -gt $MaximumBytes) {
|
|
return $null
|
|
}
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
[byte[]]$digest = $sha256.ComputeHash($stream)
|
|
return -join @($digest | ForEach-Object { $_.ToString('x2') })
|
|
}
|
|
catch { return $null }
|
|
finally {
|
|
if ($null -ne $sha256) { $sha256.Dispose() }
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
function Get-BytesSha256([byte[]]$Bytes) {
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
[byte[]]$digest = $sha256.ComputeHash($Bytes)
|
|
return -join @($digest | ForEach-Object { $_.ToString('x2') })
|
|
}
|
|
finally { $sha256.Dispose() }
|
|
}
|
|
|
|
function Get-StreamSha256([IO.Stream]$Stream) {
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
[byte[]]$digest = $sha256.ComputeHash($Stream)
|
|
return -join @($digest | ForEach-Object { $_.ToString('x2') })
|
|
}
|
|
finally { $sha256.Dispose() }
|
|
}
|
|
|
|
function Test-PackageArchiveBinding(
|
|
[string]$ArchivePath,
|
|
[object]$Manifest,
|
|
[string]$ManifestSha256) {
|
|
$result = [ordered]@{
|
|
passed = $false
|
|
code = 'package_archive_binding_failed'
|
|
detail = '最终 ZIP 必须与当前解包目录的清单、逐文件大小和 SHA-256 完全一致。'
|
|
sha256 = $null
|
|
}
|
|
$stream = $null
|
|
$archive = $null
|
|
try {
|
|
if ([string]::IsNullOrWhiteSpace($ArchivePath)) {
|
|
$result.code = 'package_archive_required'
|
|
$result.detail = '必须提供最终未修改的商用 ZIP,不能只预检解包目录。'
|
|
return $result
|
|
}
|
|
$fullPath = [IO.Path]::GetFullPath($ArchivePath)
|
|
if ([IO.Path]::GetExtension($fullPath) -cne '.zip' -or
|
|
-not (Test-RegularFile $fullPath 4GB) -or
|
|
$ManifestSha256 -cnotmatch '^[a-f0-9]{64}$') {
|
|
throw 'package_archive_input_invalid'
|
|
}
|
|
|
|
Add-Type -AssemblyName System.IO.Compression -ErrorAction Stop
|
|
try {
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop
|
|
}
|
|
catch {
|
|
if ($null -eq ('System.IO.Compression.ZipArchive' -as [type])) { throw }
|
|
}
|
|
$stream = [IO.File]::Open(
|
|
$fullPath,
|
|
[IO.FileMode]::Open,
|
|
[IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read)
|
|
if ($stream.Length -le 0 -or $stream.Length -gt 4GB) {
|
|
throw 'package_archive_size_invalid'
|
|
}
|
|
$archiveSha256 = Get-StreamSha256 $stream
|
|
$stream.Position = 0
|
|
$archive = [IO.Compression.ZipArchive]::new(
|
|
$stream, [IO.Compression.ZipArchiveMode]::Read, $true)
|
|
|
|
$expected = New-Object 'System.Collections.Generic.Dictionary[string,object]' `
|
|
([StringComparer]::OrdinalIgnoreCase)
|
|
foreach ($manifestEntry in @($Manifest.files)) {
|
|
$relative = [string]$manifestEntry.path
|
|
if ($expected.ContainsKey($relative)) {
|
|
throw 'package_archive_manifest_duplicate'
|
|
}
|
|
$expected.Add($relative, $manifestEntry)
|
|
}
|
|
|
|
$seen = New-Object 'System.Collections.Generic.HashSet[string]' `
|
|
([StringComparer]::OrdinalIgnoreCase)
|
|
$files = New-Object System.Collections.Generic.List[object]
|
|
$rootName = $null
|
|
[long]$totalLength = 0
|
|
foreach ($entry in $archive.Entries) {
|
|
$name = [string]$entry.FullName
|
|
if ([string]::IsNullOrWhiteSpace($name) -or
|
|
$name.Contains('\') -or $name.StartsWith('/') -or
|
|
$name.Contains(':') -or $name.Contains('//')) {
|
|
throw 'package_archive_path_invalid'
|
|
}
|
|
$isDirectory = $name.EndsWith('/')
|
|
$trimmed = if ($isDirectory) { $name.TrimEnd('/') } else { $name }
|
|
$segments = @($trimmed.Split('/'))
|
|
if ($segments.Count -lt 1 -or
|
|
@($segments | Where-Object {
|
|
[string]::IsNullOrWhiteSpace($_) -or $_ -in @('.', '..')
|
|
}).Count -ne 0 -or
|
|
$segments[0] -cnotmatch '^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$') {
|
|
throw 'package_archive_path_invalid'
|
|
}
|
|
if ($null -eq $rootName) { $rootName = $segments[0] }
|
|
if ($segments[0] -cne $rootName -or -not $seen.Add($trimmed)) {
|
|
throw 'package_archive_root_or_duplicate_invalid'
|
|
}
|
|
|
|
[int]$externalAttributes = $entry.ExternalAttributes
|
|
[int]$unixType = (($externalAttributes -shr 16) -band 0xF000)
|
|
if ($unixType -eq 0xA000 -or
|
|
($externalAttributes -band [int][IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
throw 'package_archive_link_forbidden'
|
|
}
|
|
$attributeDirectory =
|
|
($externalAttributes -band [int][IO.FileAttributes]::Directory) -ne 0
|
|
if ($isDirectory) {
|
|
if ($entry.Length -ne 0) { throw 'package_archive_directory_invalid' }
|
|
continue
|
|
}
|
|
if ($attributeDirectory -or $segments.Count -lt 2 -or
|
|
$entry.Length -le 0 -or $entry.Length -gt 512MB -or
|
|
$totalLength -gt 4GB - $entry.Length) {
|
|
throw 'package_archive_entry_invalid'
|
|
}
|
|
$totalLength += $entry.Length
|
|
$relative = [string]::Join('/', $segments[1..($segments.Count - 1)])
|
|
$files.Add([ordered]@{ relative = $relative; entry = $entry })
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($rootName) -or
|
|
$files.Count -ne $expected.Count + 1) {
|
|
throw 'package_archive_file_count_invalid'
|
|
}
|
|
|
|
$manifestFound = $false
|
|
foreach ($archiveFile in $files) {
|
|
$relative = [string]$archiveFile.relative
|
|
$entry = $archiveFile.entry
|
|
$entryStream = $null
|
|
try {
|
|
$entryStream = $entry.Open()
|
|
$actualHash = Get-StreamSha256 $entryStream
|
|
}
|
|
finally {
|
|
if ($null -ne $entryStream) { $entryStream.Dispose() }
|
|
}
|
|
if ($relative -ceq 'SHA256SUMS.json') {
|
|
if ($manifestFound -or $actualHash -cne $ManifestSha256) {
|
|
throw 'package_archive_manifest_mismatch'
|
|
}
|
|
$manifestFound = $true
|
|
continue
|
|
}
|
|
$manifestEntry = $null
|
|
if (-not $expected.TryGetValue($relative, [ref]$manifestEntry) -or
|
|
[long]$manifestEntry.sizeBytes -ne [long]$entry.Length -or
|
|
([string]$manifestEntry.sha256).ToLowerInvariant() -cne $actualHash) {
|
|
throw 'package_archive_entry_mismatch'
|
|
}
|
|
}
|
|
if (-not $manifestFound) { throw 'package_archive_manifest_missing' }
|
|
|
|
$result.passed = $true
|
|
$result.code = 'ok'
|
|
$result.detail = ("最终 ZIP 已绑定同一清单并逐项验证 {0} 个包内文件。" -f `
|
|
$expected.Count)
|
|
$result.sha256 = $archiveSha256
|
|
return $result
|
|
}
|
|
catch { return $result }
|
|
finally {
|
|
if ($null -ne $archive) { $archive.Dispose() }
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
function Read-UInt24LittleEndian([byte[]]$Bytes, [int]$Offset) {
|
|
if ($null -eq $Bytes -or $Offset -lt 0 -or $Offset + 3 -gt $Bytes.Length) {
|
|
throw 'webp_uint24_out_of_range'
|
|
}
|
|
return ([uint32]$Bytes[$Offset] -bor `
|
|
([uint32]$Bytes[$Offset + 1] -shl 8) -bor `
|
|
([uint32]$Bytes[$Offset + 2] -shl 16))
|
|
}
|
|
|
|
function Get-WebPDimensions([string]$Path, [long]$MaximumBytes) {
|
|
$stream = $null
|
|
$reader = $null
|
|
try {
|
|
if (-not (Test-RegularFile $Path $MaximumBytes)) { return $null }
|
|
$stream = [IO.File]::Open(
|
|
$Path,
|
|
[IO.FileMode]::Open,
|
|
[IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read)
|
|
if ($stream.Length -lt 26 -or $stream.Length -gt $MaximumBytes) {
|
|
throw 'webp_length_invalid'
|
|
}
|
|
$reader = [IO.BinaryReader]::new($stream, [Text.Encoding]::ASCII, $true)
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
[byte[]]$digest = $sha256.ComputeHash($stream)
|
|
}
|
|
finally {
|
|
$sha256.Dispose()
|
|
}
|
|
$digestHex = -join @($digest | ForEach-Object { $_.ToString('x2') })
|
|
$stream.Position = 0
|
|
if ($reader.ReadUInt32() -ne [uint32]0x46464952) { # RIFF
|
|
throw 'webp_riff_missing'
|
|
}
|
|
[uint32]$riffSize = $reader.ReadUInt32()
|
|
if ([uint64]$riffSize + 8 -ne [uint64]$stream.Length -or
|
|
$reader.ReadUInt32() -ne [uint32]0x50424557) { # WEBP
|
|
throw 'webp_container_invalid'
|
|
}
|
|
|
|
$width = $null
|
|
$height = $null
|
|
$seenVp8X = $false
|
|
$seenBitstream = $false
|
|
$chunkCount = 0
|
|
while ($stream.Position -lt $stream.Length) {
|
|
$chunkCount++
|
|
if ($chunkCount -gt 1024 -or $stream.Length - $stream.Position -lt 8) {
|
|
throw 'webp_chunk_header_invalid'
|
|
}
|
|
|
|
[uint32]$chunkType = $reader.ReadUInt32()
|
|
[uint32]$chunkSize = $reader.ReadUInt32()
|
|
[long]$payloadStart = $stream.Position
|
|
[long]$paddedSize = [long]$chunkSize + ([long]$chunkSize -band 1)
|
|
if ($paddedSize -gt $stream.Length - $payloadStart) {
|
|
throw 'webp_chunk_size_invalid'
|
|
}
|
|
|
|
$candidateWidth = $null
|
|
$candidateHeight = $null
|
|
if ($chunkType -eq [uint32]0x58385056) { # VP8X
|
|
if ($seenVp8X -or $chunkCount -ne 1 -or $chunkSize -ne 10) {
|
|
throw 'webp_vp8x_invalid'
|
|
}
|
|
$seenVp8X = $true
|
|
[byte[]]$header = $reader.ReadBytes(10)
|
|
if ($header.Length -ne 10 -or
|
|
($header[0] -band 0xc3) -ne 0 -or
|
|
$header[1] -ne 0 -or $header[2] -ne 0 -or $header[3] -ne 0) {
|
|
throw 'webp_vp8x_header_invalid'
|
|
}
|
|
$candidateWidth = 1 + (Read-UInt24LittleEndian $header 4)
|
|
$candidateHeight = 1 + (Read-UInt24LittleEndian $header 7)
|
|
}
|
|
elseif ($chunkType -eq [uint32]0x20385056) { # VP8 + trailing space
|
|
if ($seenBitstream -or $chunkSize -lt 10) {
|
|
throw 'webp_vp8_invalid'
|
|
}
|
|
$seenBitstream = $true
|
|
[byte[]]$header = $reader.ReadBytes(10)
|
|
[uint32]$frameTag = Read-UInt24LittleEndian $header 0
|
|
if ($header.Length -ne 10 -or ($frameTag -band 1) -ne 0 -or
|
|
$header[3] -ne 0x9d -or $header[4] -ne 0x01 -or
|
|
$header[5] -ne 0x2a) {
|
|
throw 'webp_vp8_header_invalid'
|
|
}
|
|
$candidateWidth = 1 * (([int]$header[6] -bor `
|
|
([int]$header[7] -shl 8)) -band 0x3fff)
|
|
$candidateHeight = 1 * (([int]$header[8] -bor `
|
|
([int]$header[9] -shl 8)) -band 0x3fff)
|
|
}
|
|
elseif ($chunkType -eq [uint32]0x4c385056) { # VP8L
|
|
if ($seenBitstream -or $chunkSize -lt 5) {
|
|
throw 'webp_vp8l_invalid'
|
|
}
|
|
$seenBitstream = $true
|
|
[byte[]]$header = $reader.ReadBytes(5)
|
|
if ($header.Length -ne 5 -or $header[0] -ne 0x2f) {
|
|
throw 'webp_vp8l_header_invalid'
|
|
}
|
|
[uint64]$bits = [uint64]$header[1] -bor `
|
|
([uint64]$header[2] -shl 8) -bor `
|
|
([uint64]$header[3] -shl 16) -bor `
|
|
([uint64]$header[4] -shl 24)
|
|
if (($bits -shr 29) -ne 0) { throw 'webp_vp8l_version_invalid' }
|
|
$candidateWidth = 1 + ($bits -band 0x3fff)
|
|
$candidateHeight = 1 + (($bits -shr 14) -band 0x3fff)
|
|
}
|
|
|
|
if ($null -ne $candidateWidth) {
|
|
if ($candidateWidth -le 0 -or $candidateHeight -le 0 -or
|
|
($null -ne $width -and
|
|
($width -ne $candidateWidth -or $height -ne $candidateHeight))) {
|
|
throw 'webp_dimensions_inconsistent'
|
|
}
|
|
$width = [int]$candidateWidth
|
|
$height = [int]$candidateHeight
|
|
}
|
|
|
|
if (($chunkSize -band 1) -ne 0) {
|
|
$stream.Position = $payloadStart + [long]$chunkSize
|
|
if ($reader.ReadByte() -ne 0) { throw 'webp_padding_invalid' }
|
|
}
|
|
$stream.Position = $payloadStart + $paddedSize
|
|
}
|
|
|
|
if ($null -eq $width -or $null -eq $height -or -not $seenBitstream) {
|
|
throw 'webp_dimensions_missing'
|
|
}
|
|
return [pscustomobject]@{
|
|
width = [int]$width
|
|
height = [int]$height
|
|
sha256 = $digestHex
|
|
}
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
finally {
|
|
if ($null -ne $reader) { $reader.Dispose() }
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
function Test-ExactProperties([object]$Value, [string[]]$Expected) {
|
|
if ($null -eq $Value) { return $false }
|
|
$names = @($Value.PSObject.Properties | ForEach-Object { $_.Name })
|
|
if ($names.Count -ne $Expected.Count) { return $false }
|
|
foreach ($name in $Expected) {
|
|
if ($names -cnotcontains $name) { return $false }
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Test-SafeSessionScopeValue([object]$Value) {
|
|
if ($null -eq $Value) { return $false }
|
|
$text = [string]$Value
|
|
if ([string]::IsNullOrWhiteSpace($text) -or
|
|
$text.Length -gt 256 -or
|
|
$text -cne $text.Trim()) {
|
|
return $false
|
|
}
|
|
foreach ($character in $text.ToCharArray()) {
|
|
if ([char]::IsControl($character)) { return $false }
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Test-ExpectedSessionContext([object]$Data) {
|
|
if (-not (Test-ExactProperties $Data @(
|
|
'userId', 'userName', 'accountBook', 'subSystemId',
|
|
'databaseScopeFingerprint', 'subSystemName',
|
|
'isAdministrator', 'activeModule', 'openModuleCount',
|
|
'openModulesTruncated', 'openModules'))) {
|
|
return $false
|
|
}
|
|
foreach ($name in @(
|
|
'userId', 'userName', 'accountBook', 'subSystemId',
|
|
'subSystemName')) {
|
|
if (-not (Test-SafeSessionScopeValue $Data.$name)) { return $false }
|
|
}
|
|
$openModuleCount = -1
|
|
return ([string]$Data.userId -ceq $ExpectedUserId) -and
|
|
([string]$Data.userName -ceq $ExpectedUserName) -and
|
|
([string]$Data.accountBook -ceq $ExpectedAccountBook) -and
|
|
([string]$Data.subSystemId -ceq $ExpectedSubSystemId) -and
|
|
([string]$Data.databaseScopeFingerprint).ToLowerInvariant() -ceq
|
|
$ExpectedDatabaseScopeFingerprint.ToLowerInvariant() -and
|
|
([string]$Data.databaseScopeFingerprint).ToLowerInvariant() -cmatch
|
|
'^[a-f0-9]{64}$' -and
|
|
$Data.isAdministrator -is [bool] -and
|
|
[bool]$Data.isAdministrator -eq $expectedAdministrator -and
|
|
$Data.openModulesTruncated -is [bool] -and
|
|
[int]::TryParse(
|
|
[string]$Data.openModuleCount,
|
|
[ref]$openModuleCount) -and
|
|
$openModuleCount -ge 0 -and $openModuleCount -le 10000 -and
|
|
@($Data.openModules).Count -le 50
|
|
}
|
|
|
|
function Invoke-VerifiedBridgeRead([string]$Action) {
|
|
if ($Action -cnotin @('health', 'context')) { return $null }
|
|
$correlationId = 'preflight-' + $Action + '-' +
|
|
[Guid]::NewGuid().ToString('N')
|
|
$arguments = @(
|
|
'bridge', $Action,
|
|
'--erp-process-id', [string]$ErpProcessId,
|
|
'--expected-database-scope-fingerprint',
|
|
$ExpectedDatabaseScopeFingerprint.ToLowerInvariant(),
|
|
'--expected-user-id', $ExpectedUserId,
|
|
'--expected-user-name', $ExpectedUserName,
|
|
'--expected-account-book', $ExpectedAccountBook,
|
|
'--expected-subsystem-id', $ExpectedSubSystemId,
|
|
'--expected-is-administrator', $(if ($expectedAdministrator) {
|
|
'true'
|
|
} else { 'false' }),
|
|
'--correlation-id', $correlationId
|
|
)
|
|
try {
|
|
$raw = (& $bridgeCliPath @arguments 2>$null | Out-String)
|
|
if ($LASTEXITCODE -ne 0 -or
|
|
[string]::IsNullOrWhiteSpace($raw) -or
|
|
$raw.Length -gt 4MB) {
|
|
return $null
|
|
}
|
|
$result = $raw | ConvertFrom-Json
|
|
if (-not (Test-ExactProperties $result @(
|
|
'ok', 'correlationId', 'data')) -or
|
|
$result.ok -ne $true -or
|
|
[string]$result.correlationId -cne $correlationId -or
|
|
$null -eq $result.data) {
|
|
return $null
|
|
}
|
|
return $result.data
|
|
}
|
|
catch { return $null }
|
|
}
|
|
|
|
foreach ($expectedScopeValue in @(
|
|
$ExpectedUserId,
|
|
$ExpectedUserName,
|
|
$ExpectedAccountBook,
|
|
$ExpectedSubSystemId)) {
|
|
if (-not (Test-SafeSessionScopeValue $expectedScopeValue)) {
|
|
throw 'expected_erp_session_scope_invalid'
|
|
}
|
|
}
|
|
$ExpectedDatabaseScopeFingerprint =
|
|
$ExpectedDatabaseScopeFingerprint.ToLowerInvariant()
|
|
|
|
function Remove-JsonWhitespaceOutsideStrings([string]$Json) {
|
|
if ($null -eq $Json) { return $null }
|
|
$builder = New-Object Text.StringBuilder
|
|
$inString = $false
|
|
$escaped = $false
|
|
foreach ($character in $Json.ToCharArray()) {
|
|
if ($inString) {
|
|
$builder.Append($character) | Out-Null
|
|
if ($escaped) {
|
|
$escaped = $false
|
|
} elseif ($character -eq '\') {
|
|
$escaped = $true
|
|
} elseif ($character -eq '"') {
|
|
$inString = $false
|
|
}
|
|
} elseif ($character -eq '"') {
|
|
$inString = $true
|
|
$builder.Append($character) | Out-Null
|
|
} elseif (-not [char]::IsWhiteSpace($character)) {
|
|
$builder.Append($character) | Out-Null
|
|
}
|
|
}
|
|
if ($inString -or $escaped) { return $null }
|
|
return $builder.ToString()
|
|
}
|
|
|
|
function Test-NonNegativeInteger([object]$Value, [long]$Maximum) {
|
|
if ($null -eq $Value) { return $false }
|
|
$integerTypes = @(
|
|
[byte], [sbyte], [int16], [uint16], [int32], [uint32], [int64], [uint64]
|
|
)
|
|
if ($integerTypes -notcontains $Value.GetType()) { return $false }
|
|
try {
|
|
[long]$number = $Value
|
|
return $number -ge 0 -and $number -le $Maximum
|
|
}
|
|
catch { return $false }
|
|
}
|
|
|
|
function Test-MiniMaxVisionProbeEvidence(
|
|
[string]$EvidencePath,
|
|
[DateTimeOffset]$NowUtc) {
|
|
$result = [ordered]@{
|
|
passed = $false
|
|
code = 'minimax_vision_probe_evidence_missing'
|
|
detail = '必须提供由随包在线探针生成的、24 小时内且不含客户数据的 MiniMax VLM JSON 报告。'
|
|
sha256 = $null
|
|
observedAtUtc = $null
|
|
region = $null
|
|
contractVersion = $null
|
|
}
|
|
try {
|
|
if ([string]::IsNullOrWhiteSpace($EvidencePath)) { return $result }
|
|
$fullPath = [IO.Path]::GetFullPath($EvidencePath)
|
|
if ([IO.Path]::GetExtension($fullPath) -cne '.json' -or
|
|
-not (Test-RegularFile $fullPath 64KB)) {
|
|
return $result
|
|
}
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$raw = [IO.File]::ReadAllText($fullPath, $strictUtf8)
|
|
$evidence = $raw | ConvertFrom-Json
|
|
$contract = $evidence.contract
|
|
$probeResult = $evidence.result
|
|
$shapeValid =
|
|
(Test-ExactProperties $evidence @(
|
|
'schemaVersion', 'observedAtUtc', 'passed', 'region',
|
|
'endpoint', 'contract', 'syntheticSourceSha256', 'result')) -and
|
|
(Test-ExactProperties $contract @(
|
|
'component', 'version', 'sourceCommit', 'clientSourceSha256',
|
|
'serverSourceSha256', 'apiSourceHeader')) -and
|
|
(Test-ExactProperties $probeResult @(
|
|
'schemaVersion', 'documentType', 'lineCount',
|
|
'uncertainFieldCount', 'contentSha256')) -and
|
|
($evidence.passed -is [bool]) -and $evidence.passed -eq $true -and
|
|
([string]$evidence.schemaVersion -ceq '1.0') -and
|
|
([string]$contract.component -ceq 'minimax-coding-plan-mcp') -and
|
|
([string]$contract.version -ceq '0.0.4') -and
|
|
([string]$contract.sourceCommit -ceq
|
|
'fbac3b3e56922a1249e00eebe07d9ee68f4768dc') -and
|
|
([string]$contract.clientSourceSha256 -ceq
|
|
'08d4116a20e8a652ceb9e2b6f58b1e7cdfe464b14baff05977e08b4b05b66be3') -and
|
|
([string]$contract.serverSourceSha256 -ceq
|
|
'1dea28d6ba4ee46ba516d7eeedd325a5a102410bb7abb074fc4b0a8a66571864') -and
|
|
([string]$contract.apiSourceHeader -ceq 'Minimax-MCP') -and
|
|
([string]$evidence.syntheticSourceSha256 -ceq
|
|
'd37476a5273821c12ee4a72b512dc152db5729055b6febb8603985f86243abda') -and
|
|
([string]$probeResult.schemaVersion -ceq '1.0') -and
|
|
@('purchase_invoice', 'purchase_detail', 'unknown') -ccontains
|
|
([string]$probeResult.documentType) -and
|
|
(Test-NonNegativeInteger $probeResult.lineCount 10000) -and
|
|
(Test-NonNegativeInteger $probeResult.uncertainFieldCount 10000) -and
|
|
([string]$probeResult.contentSha256 -cmatch '^[a-f0-9]{64}$')
|
|
$region = [string]$evidence.region
|
|
$expectedEndpoint = if ($region -ceq 'cn') {
|
|
'https://api.minimaxi.com/v1/coding_plan/vlm'
|
|
} elseif ($region -ceq 'global') {
|
|
'https://api.minimax.io/v1/coding_plan/vlm'
|
|
} else { '' }
|
|
$shapeValid = $shapeValid -and -not [string]::IsNullOrWhiteSpace(
|
|
$expectedEndpoint) -and ([string]$evidence.endpoint -ceq $expectedEndpoint)
|
|
|
|
$timestampMatch = [Text.RegularExpressions.Regex]::Match(
|
|
$raw,
|
|
'"observedAtUtc"\s*:\s*"(?<value>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?\+00:00)"',
|
|
[Text.RegularExpressions.RegexOptions]::CultureInvariant)
|
|
$timestamp = if ($timestampMatch.Success) {
|
|
$timestampMatch.Groups['value'].Value
|
|
} else { '' }
|
|
$observed = [DateTimeOffset]::MinValue
|
|
$timestampValid = $timestamp -cmatch `
|
|
'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?\+00:00$' -and
|
|
[DateTimeOffset]::TryParse(
|
|
$timestamp,
|
|
[Globalization.CultureInfo]::InvariantCulture,
|
|
[Globalization.DateTimeStyles]::None,
|
|
[ref]$observed) -and
|
|
$observed.Offset -eq [TimeSpan]::Zero
|
|
$shapeValid = $shapeValid -and $timestampValid
|
|
|
|
$canonicalObject = [ordered]@{
|
|
contract = [ordered]@{
|
|
apiSourceHeader = [string]$contract.apiSourceHeader
|
|
clientSourceSha256 = [string]$contract.clientSourceSha256
|
|
component = [string]$contract.component
|
|
serverSourceSha256 = [string]$contract.serverSourceSha256
|
|
sourceCommit = [string]$contract.sourceCommit
|
|
version = [string]$contract.version
|
|
}
|
|
endpoint = [string]$evidence.endpoint
|
|
observedAtUtc = $timestamp
|
|
passed = $true
|
|
region = $region
|
|
result = [ordered]@{
|
|
contentSha256 = [string]$probeResult.contentSha256
|
|
documentType = [string]$probeResult.documentType
|
|
lineCount = [long]$probeResult.lineCount
|
|
schemaVersion = [string]$probeResult.schemaVersion
|
|
uncertainFieldCount = [long]$probeResult.uncertainFieldCount
|
|
}
|
|
schemaVersion = [string]$evidence.schemaVersion
|
|
syntheticSourceSha256 = [string]$evidence.syntheticSourceSha256
|
|
}
|
|
$actualCanonical = Remove-JsonWhitespaceOutsideStrings $raw
|
|
$expectedCanonical = Remove-JsonWhitespaceOutsideStrings `
|
|
($canonicalObject | ConvertTo-Json -Compress -Depth 8)
|
|
$shapeValid = $shapeValid -and
|
|
-not [string]::IsNullOrWhiteSpace($actualCanonical) -and
|
|
$actualCanonical -ceq $expectedCanonical
|
|
if (-not $shapeValid) {
|
|
$result.code = 'minimax_vision_probe_contract_invalid'
|
|
$result.detail = 'MiniMax 在线探针报告不是当前受审来源、固定合成图片和精确 JSON 合同。'
|
|
return $result
|
|
}
|
|
if ($observed -gt $NowUtc.AddMinutes(5) -or
|
|
$observed -lt $NowUtc.AddHours(-24)) {
|
|
$result.code = 'minimax_vision_probe_stale'
|
|
$result.detail = 'MiniMax 在线探针必须在预检前 24 小时内完成,未来时间最多容忍 5 分钟。'
|
|
return $result
|
|
}
|
|
$sha256 = Get-RegularFileSha256 $fullPath 64KB
|
|
if ([string]::IsNullOrWhiteSpace($sha256)) { return $result }
|
|
$result.passed = $true
|
|
$result.code = 'ok'
|
|
$result.detail = '合成图片在线探针合同、区域、时效和报告 SHA-256 已验证。'
|
|
$result.sha256 = $sha256
|
|
$result.observedAtUtc = $timestamp
|
|
$result.region = $region
|
|
$result.contractVersion = '0.0.4'
|
|
return $result
|
|
}
|
|
catch {
|
|
$result.code = 'minimax_vision_probe_contract_invalid'
|
|
$result.detail = 'MiniMax 在线探针报告不是严格 UTF-8 JSON 或合同字段无效。'
|
|
return $result
|
|
}
|
|
}
|
|
|
|
function Test-LegacyBuildEvidence([string]$ArtifactRoot, [string]$ExpectedCommit) {
|
|
$result = [ordered]@{
|
|
passed = $false
|
|
code = 'legacy_build_evidence_missing'
|
|
detail = '必须提供 Windows 旧 ERP/CLI 验收构建目录。'
|
|
evidenceSha256 = $null
|
|
erpPath = $null
|
|
cliPath = $null
|
|
signedPaths = @()
|
|
certificateThumbprint = $null
|
|
}
|
|
try {
|
|
if ([string]::IsNullOrWhiteSpace($ArtifactRoot) -or
|
|
$ExpectedCommit -notmatch '^[A-Fa-f0-9]{40}$') {
|
|
return $result
|
|
}
|
|
$legacyRoot = [IO.Path]::GetFullPath($ArtifactRoot).TrimEnd([char[]]@('\', '/'))
|
|
if (-not [IO.Directory]::Exists($legacyRoot)) { return $result }
|
|
$rootItem = Get-Item -LiteralPath $legacyRoot -Force
|
|
if (($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
throw 'legacy_root_reparse_forbidden'
|
|
}
|
|
$rootPrefix = $legacyRoot + [IO.Path]::DirectorySeparatorChar
|
|
$evidencePath = Join-Path $legacyRoot 'LEGACY-BUILD-EVIDENCE.json'
|
|
if (-not (Test-RegularFile $evidencePath 8MB)) { return $result }
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$evidence = [IO.File]::ReadAllText($evidencePath, $strictUtf8) | ConvertFrom-Json
|
|
$expectedProperties = @(
|
|
'schemaVersion', 'generatedAtUtc', 'buildVerified', 'releaseReadiness',
|
|
'sourceCommit', 'sourceWorktreeDirty', 'configuration', 'platform',
|
|
'targetFramework', 'devExpressContract', 'dependencies', 'msbuildVersion',
|
|
'editbinVersion', 'authenticode', 'erpBinary', 'cliBinary', 'sourceFiles',
|
|
'files', 'remainingHardGates'
|
|
)
|
|
$requiredSignedFiles = @(
|
|
'Ls_ERP.exe', 'lserp-cli.exe', 'Lskj.AgentBridge.dll',
|
|
'Lskj.CommandKernel.dll', 'Lskj.Core.dll'
|
|
)
|
|
$authenticodeFiles = @($evidence.authenticode.files | ForEach-Object { [string]$_ })
|
|
$authenticodeShapeValid = (Test-ExactProperties $evidence.authenticode @(
|
|
'signed', 'certificateThumbprint', 'certificateStoreLocation',
|
|
'timestampUrl', 'files')) -and
|
|
$evidence.authenticode.signed -eq $true -and
|
|
([string]$evidence.authenticode.certificateThumbprint) -match '^[A-Fa-f0-9]{40}$' -and
|
|
@('CurrentUser', 'LocalMachine') -contains `
|
|
([string]$evidence.authenticode.certificateStoreLocation) -and
|
|
([string]$evidence.authenticode.timestampUrl).StartsWith('https://') -and
|
|
$authenticodeFiles.Count -eq $requiredSignedFiles.Count
|
|
foreach ($signedFile in $requiredSignedFiles) {
|
|
if ($authenticodeFiles -notcontains $signedFile) { $authenticodeShapeValid = $false }
|
|
}
|
|
$dependencyShapeValid = (Test-ExactProperties $evidence.dependencies @('cefRedistX86')) -and
|
|
(Test-ExactProperties $evidence.dependencies.cefRedistX86 @(
|
|
'packageId', 'version', 'sha256')) -and
|
|
$evidence.dependencies.cefRedistX86.packageId -eq 'cef.redist.x86' -and
|
|
$evidence.dependencies.cefRedistX86.version -eq '87.1.13' -and
|
|
$evidence.dependencies.cefRedistX86.sha256 -eq `
|
|
'34dfe2504c1ffaef02eab1f38578701b045439349997b6465fd5dd6659fab021'
|
|
$binaryShape = @(
|
|
'assemblyName', 'machine', 'pe32', 'ilOnly', 'bit32Required',
|
|
'largeAddressAware', 'corFlags', 'runtimeVersion'
|
|
)
|
|
if (-not (Test-ExactProperties $evidence $expectedProperties) -or
|
|
$evidence.schemaVersion -ne '1.0' -or $evidence.buildVerified -ne $true -or
|
|
$evidence.releaseReadiness -ne $false -or
|
|
$evidence.sourceWorktreeDirty -ne $false -or
|
|
([string]$evidence.sourceCommit).ToLowerInvariant() -ne $ExpectedCommit.ToLowerInvariant() -or
|
|
$evidence.configuration -ne 'Release' -or $evidence.platform -ne 'x86' -or
|
|
$evidence.targetFramework -ne 'v4.0' -or $evidence.devExpressContract -ne '15.2' -or
|
|
$evidence.erpBinary.assemblyName -ne 'Ls_ERP' -or
|
|
$evidence.cliBinary.assemblyName -ne 'lserp-cli' -or
|
|
$evidence.erpBinary.machine -ne '0x014c' -or
|
|
$evidence.cliBinary.machine -ne '0x014c' -or
|
|
$evidence.erpBinary.pe32 -ne $true -or $evidence.cliBinary.pe32 -ne $true -or
|
|
$evidence.erpBinary.ilOnly -ne $true -or $evidence.cliBinary.ilOnly -ne $true -or
|
|
$evidence.erpBinary.bit32Required -ne $true -or
|
|
$evidence.cliBinary.bit32Required -ne $true -or
|
|
$evidence.erpBinary.largeAddressAware -ne $true -or
|
|
-not (Test-ExactProperties $evidence.erpBinary $binaryShape) -or
|
|
-not (Test-ExactProperties $evidence.cliBinary $binaryShape) -or
|
|
-not $dependencyShapeValid -or
|
|
-not $authenticodeShapeValid -or
|
|
-not ([string]$evidence.erpBinary.runtimeVersion).StartsWith('v4.0') -or
|
|
-not ([string]$evidence.cliBinary.runtimeVersion).StartsWith('v4.0') -or
|
|
$null -eq $evidence.sourceFiles -or $evidence.sourceFiles.Count -le 0 -or
|
|
$evidence.sourceFiles.Count -gt 100 -or
|
|
$null -eq $evidence.files -or $evidence.files.Count -le 0 -or
|
|
$evidence.files.Count -gt 2000 -or
|
|
$null -eq $evidence.remainingHardGates -or
|
|
$evidence.remainingHardGates.Count -ne 3) {
|
|
throw 'legacy_evidence_shape_invalid'
|
|
}
|
|
$expectedFiles = New-Object `
|
|
'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
|
foreach ($entry in $evidence.files) {
|
|
if (-not (Test-ExactProperties $entry @('path', 'sizeBytes', 'sha256'))) {
|
|
throw 'legacy_evidence_file_shape_invalid'
|
|
}
|
|
$relative = [string]$entry.path
|
|
if ([string]::IsNullOrWhiteSpace($relative) -or
|
|
$relative.Contains('\') -or $relative.StartsWith('/') -or
|
|
$relative.Split('/') -contains '..' -or
|
|
-not $expectedFiles.Add($relative) -or
|
|
([string]$entry.sha256) -notmatch '^[A-Fa-f0-9]{64}$') {
|
|
throw 'legacy_evidence_path_invalid'
|
|
}
|
|
$full = [IO.Path]::GetFullPath((Join-Path $legacyRoot `
|
|
($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar))))
|
|
if (-not $full.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase) -or
|
|
-not (Test-RegularFile $full 512MB)) {
|
|
throw 'legacy_evidence_file_invalid'
|
|
}
|
|
$item = Get-Item -LiteralPath $full -Force
|
|
if ([long]$entry.sizeBytes -ne $item.Length -or
|
|
(Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() -ne
|
|
([string]$entry.sha256).ToLowerInvariant()) {
|
|
throw 'legacy_evidence_hash_mismatch'
|
|
}
|
|
}
|
|
foreach ($sourceEntry in $evidence.sourceFiles) {
|
|
if (-not (Test-ExactProperties $sourceEntry @('path', 'sha256')) -or
|
|
([string]$sourceEntry.path).Contains('\') -or
|
|
([string]$sourceEntry.path).Split('/') -contains '..' -or
|
|
([string]$sourceEntry.sha256) -notmatch '^[A-Fa-f0-9]{64}$') {
|
|
throw 'legacy_source_evidence_shape_invalid'
|
|
}
|
|
}
|
|
$reparseDirectories = @(Get-ChildItem -LiteralPath $legacyRoot -Recurse -Directory -Force |
|
|
Where-Object {
|
|
($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0
|
|
})
|
|
if ($reparseDirectories.Count -ne 0) { throw 'legacy_reparse_directory_forbidden' }
|
|
$actualFiles = @(Get-ChildItem -LiteralPath $legacyRoot -Recurse -File -Force |
|
|
ForEach-Object {
|
|
$_.FullName.Substring($rootPrefix.Length).Replace('\', '/')
|
|
} | Where-Object { $_ -ne 'LEGACY-BUILD-EVIDENCE.json' })
|
|
foreach ($relative in $actualFiles) {
|
|
if (-not $expectedFiles.Contains($relative)) {
|
|
throw 'legacy_unexpected_artifact_file'
|
|
}
|
|
}
|
|
$requiredCefFiles = [ordered]@{
|
|
'Runtime/Xilium.CefGlue.dll' = `
|
|
'5057f66d83727e73ce926f918c459fa4a29c9e0a78a2654c09e58210f4355cd7'
|
|
'Runtime/libcef.dll' = `
|
|
'a8d4c9974cbdfc989f9c993cba0a388d69e2c5c009661b2c064baaf016e84cfd'
|
|
'Runtime/chrome_elf.dll' = `
|
|
'b6194e1b093a000a7b9a883edfd4114a9e79332775970a20a36da1dd1837e8f3'
|
|
'Runtime/icudtl.dat' = `
|
|
'8364e6c6bf5744357199de0de3f6ba30846ccda70288675b75059e6fd52241f3'
|
|
'Runtime/locales/zh-CN.pak' = `
|
|
'6be9ef1c87b3162253090c00487238bbf4e5466d235cb49fdb69915a03c480cf'
|
|
'Runtime/locales/en-US.pak' = `
|
|
'f9e993df87cad724a36be1efb4f5a71322c9de4d0885419e5f13ca564115dce7'
|
|
}
|
|
foreach ($relative in $requiredCefFiles.Keys) {
|
|
if (-not $expectedFiles.Contains($relative)) {
|
|
throw 'legacy_cef_runtime_missing'
|
|
}
|
|
$full = Join-Path $legacyRoot `
|
|
($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar))
|
|
if ((Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() -ne
|
|
$requiredCefFiles[$relative]) {
|
|
throw 'legacy_cef_runtime_hash_mismatch'
|
|
}
|
|
}
|
|
if ($actualFiles.Count -ne $expectedFiles.Count -or
|
|
-not $expectedFiles.Contains('Runtime/Ls_ERP.exe') -or
|
|
-not $expectedFiles.Contains('Runtime/lserp-cli.exe') -or
|
|
-not $expectedFiles.Contains('Runtime/Lskj.AgentBridge.dll') -or
|
|
-not $expectedFiles.Contains('Runtime/Lskj.CommandKernel.dll') -or
|
|
-not $expectedFiles.Contains('Runtime/Lskj.Core.dll') -or
|
|
-not $expectedFiles.Contains('MSBUILD.log')) {
|
|
throw 'legacy_required_artifact_missing'
|
|
}
|
|
$result.passed = $true
|
|
$result.code = 'ok'
|
|
$result.detail = ("已验证 {0} 个旧 ERP/CLI 构建文件。" -f $expectedFiles.Count)
|
|
$result.evidenceSha256 = (Get-FileHash -LiteralPath $evidencePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
$result.erpPath = Join-Path $legacyRoot 'Runtime\Ls_ERP.exe'
|
|
$result.cliPath = Join-Path $legacyRoot 'Runtime\lserp-cli.exe'
|
|
$result.signedPaths = @($requiredSignedFiles | ForEach-Object {
|
|
Join-Path $legacyRoot ('Runtime\' + $_)
|
|
})
|
|
$result.certificateThumbprint = `
|
|
([string]$evidence.authenticode.certificateThumbprint).ToUpperInvariant()
|
|
return $result
|
|
}
|
|
catch {
|
|
$result.code = 'legacy_build_evidence_invalid'
|
|
$result.detail = '旧 ERP/CLI 构建证据结构、来源绑定或文件哈希无效。'
|
|
return $result
|
|
}
|
|
}
|
|
|
|
function Get-WebView2Version {
|
|
foreach ($path in @(
|
|
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}',
|
|
'HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}',
|
|
'HKCU:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'
|
|
)) {
|
|
if (Test-Path -LiteralPath $path) {
|
|
$value = (Get-ItemProperty -LiteralPath $path -Name pv -ErrorAction SilentlyContinue).pv
|
|
if ($value) { return [string]$value }
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Test-TcpEndpoint([Uri]$Uri) {
|
|
$port = if ($Uri.IsDefaultPort) {
|
|
if ($Uri.Scheme -eq 'https') { 443 } else { 80 }
|
|
} else { $Uri.Port }
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
try {
|
|
$pending = $client.BeginConnect($Uri.DnsSafeHost, $port, $null, $null)
|
|
if (-not $pending.AsyncWaitHandle.WaitOne(3000, $false)) { return $false }
|
|
$client.EndConnect($pending)
|
|
return $client.Connected
|
|
}
|
|
catch { return $false }
|
|
finally { $client.Dispose() }
|
|
}
|
|
|
|
function Get-AstrBotRuntimeContract([Uri]$BaseUri) {
|
|
$result = [ordered]@{
|
|
passed = $false
|
|
runtimeVersion = $null
|
|
codeVersion = $null
|
|
detail = '无法验证 AstrBot 运行时与磁盘代码版本。'
|
|
}
|
|
$response = $null
|
|
$stream = $null
|
|
$memory = $null
|
|
try {
|
|
$endpoint = [Uri]::new($BaseUri, '/api/v1/stats/versions')
|
|
$request = [Net.HttpWebRequest]::CreateHttp($endpoint)
|
|
$request.Method = 'GET'
|
|
$request.Accept = 'application/json'
|
|
$request.AllowAutoRedirect = $false
|
|
$request.Timeout = 3000
|
|
$request.ReadWriteTimeout = 3000
|
|
$request.Proxy = $null
|
|
$response = [Net.HttpWebResponse]$request.GetResponse()
|
|
if ([int]$response.StatusCode -ne 200 -or
|
|
[string]::IsNullOrWhiteSpace($response.ContentType) -or
|
|
-not $response.ContentType.StartsWith(
|
|
'application/json', [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw 'astrbot_version_response_invalid'
|
|
}
|
|
$maximumBytes = 32KB
|
|
if ($response.ContentLength -gt $maximumBytes) {
|
|
throw 'astrbot_version_response_too_large'
|
|
}
|
|
$stream = $response.GetResponseStream()
|
|
$memory = [IO.MemoryStream]::new()
|
|
[byte[]]$buffer = New-Object byte[] 4096
|
|
[long]$total = 0
|
|
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
|
$total += $read
|
|
if ($total -gt $maximumBytes) {
|
|
throw 'astrbot_version_response_too_large'
|
|
}
|
|
$memory.Write($buffer, 0, $read)
|
|
}
|
|
if ($total -le 0) { throw 'astrbot_version_response_empty' }
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$payload = $strictUtf8.GetString($memory.ToArray()) | ConvertFrom-Json
|
|
if (-not (Test-ExactProperties $payload @('status', 'message', 'data')) -or
|
|
-not (Test-ExactProperties $payload.data @(
|
|
'webui_version', 'astrbot_version', 'astrbot_code_version')) -or
|
|
[string]$payload.status -cne 'ok' -or
|
|
$payload.data.astrbot_version -isnot [string] -or
|
|
$payload.data.astrbot_code_version -isnot [string]) {
|
|
throw 'astrbot_version_response_shape_invalid'
|
|
}
|
|
$result.runtimeVersion = [string]$payload.data.astrbot_version
|
|
$result.codeVersion = [string]$payload.data.astrbot_code_version
|
|
$result.passed = $result.runtimeVersion -ceq '4.27.2' -and
|
|
$result.codeVersion -ceq '4.27.2'
|
|
$result.detail = if ($result.passed) {
|
|
'运行时与磁盘代码版本均为已复核的 AstrBot 4.27.2。'
|
|
} else {
|
|
'AstrBot 运行时或磁盘代码版本不是已复核的 4.27.2。'
|
|
}
|
|
return $result
|
|
}
|
|
catch {
|
|
return $result
|
|
}
|
|
finally {
|
|
if ($null -ne $memory) { $memory.Dispose() }
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
if ($null -ne $response) { $response.Dispose() }
|
|
}
|
|
}
|
|
|
|
$runningOnWindows = $env:OS -eq 'Windows_NT'
|
|
Add-Check 'windows_os' $runningOnWindows 'windows_required' '必须在客户 Windows 10/11 环境运行。'
|
|
|
|
$root = [IO.Path]::GetFullPath($PackageRoot).TrimEnd([char[]]@('\', '/')) + [IO.Path]::DirectorySeparatorChar
|
|
$bridgeCliPath = Join-Path $root 'Host\lserp-agent-cli.exe'
|
|
$manifestPath = Join-Path $root 'SHA256SUMS.json'
|
|
$manifestPassed = $false
|
|
$manifest = $null
|
|
$manifestSha256 = $null
|
|
try {
|
|
if (-not (Test-RegularFile $manifestPath 4MB)) { throw 'manifest_missing' }
|
|
$manifestStream = $null
|
|
$manifestReader = $null
|
|
try {
|
|
$manifestStream = [IO.File]::Open(
|
|
$manifestPath,
|
|
[IO.FileMode]::Open,
|
|
[IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read)
|
|
if ($manifestStream.Length -le 0 -or $manifestStream.Length -gt 4MB) {
|
|
throw 'manifest_size_invalid'
|
|
}
|
|
$manifestReader = [IO.BinaryReader]::new($manifestStream)
|
|
[byte[]]$manifestBytes = $manifestReader.ReadBytes([int]$manifestStream.Length)
|
|
if ($manifestBytes.Length -ne $manifestStream.Length) {
|
|
throw 'manifest_read_incomplete'
|
|
}
|
|
$manifestSha256 = Get-BytesSha256 $manifestBytes
|
|
$strictManifestUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$manifest = $strictManifestUtf8.GetString($manifestBytes) | ConvertFrom-Json
|
|
}
|
|
finally {
|
|
if ($null -ne $manifestReader) { $manifestReader.Dispose() }
|
|
elseif ($null -ne $manifestStream) { $manifestStream.Dispose() }
|
|
}
|
|
if ($manifest.schemaVersion -ne '1.0' -or
|
|
$null -eq $manifest.files -or
|
|
$manifest.files.Count -le 0 -or
|
|
$manifest.files.Count -gt 2000) {
|
|
throw 'manifest_shape_invalid'
|
|
}
|
|
$packageReparseDirectories = @(Get-ChildItem -LiteralPath $root -Recurse -Directory -Force |
|
|
Where-Object {
|
|
($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0
|
|
})
|
|
if ($packageReparseDirectories.Count -ne 0) { throw 'manifest_reparse_directory' }
|
|
$expected = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
|
foreach ($entry in $manifest.files) {
|
|
$relative = [string]$entry.path
|
|
if ([string]::IsNullOrWhiteSpace($relative) -or
|
|
$relative.Contains('\') -or
|
|
$relative.StartsWith('/') -or
|
|
$relative.Split('/') -contains '..' -or
|
|
-not $expected.Add($relative)) {
|
|
throw 'manifest_path_invalid'
|
|
}
|
|
$full = [IO.Path]::GetFullPath((Join-Path $root ($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar))))
|
|
if (-not $full.StartsWith($root, [StringComparison]::OrdinalIgnoreCase) -or
|
|
-not (Test-RegularFile $full 512MB)) {
|
|
throw 'manifest_file_invalid'
|
|
}
|
|
$item = Get-Item -LiteralPath $full -Force
|
|
if ([long]$entry.sizeBytes -ne $item.Length) { throw 'manifest_size_mismatch' }
|
|
$actual = (Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
if ($actual -ne ([string]$entry.sha256).ToLowerInvariant()) {
|
|
throw 'manifest_hash_mismatch'
|
|
}
|
|
}
|
|
$actualFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -Force | ForEach-Object {
|
|
$_.FullName.Substring($root.Length).Replace('\', '/')
|
|
} | Where-Object { $_ -ne 'SHA256SUMS.json' })
|
|
foreach ($relative in $actualFiles) {
|
|
if (-not $expected.Contains($relative)) { throw 'unexpected_package_file' }
|
|
}
|
|
if ($actualFiles.Count -ne $expected.Count) { throw 'manifest_file_count_mismatch' }
|
|
$manifestPassed = $true
|
|
Add-Check 'package_hashes' $true 'ok' ("已验证 {0} 个包内文件。" -f $expected.Count)
|
|
}
|
|
catch {
|
|
$manifestFailure = [string]$_.Exception.Message
|
|
$knownManifestFailures = @(
|
|
'manifest_missing',
|
|
'manifest_size_invalid',
|
|
'manifest_read_incomplete',
|
|
'manifest_shape_invalid',
|
|
'manifest_path_invalid',
|
|
'manifest_file_invalid',
|
|
'manifest_size_mismatch',
|
|
'manifest_hash_mismatch',
|
|
'unexpected_package_file',
|
|
'manifest_file_count_mismatch'
|
|
'manifest_reparse_directory'
|
|
)
|
|
if ($knownManifestFailures -notcontains $manifestFailure) {
|
|
$manifestFailure = 'manifest_verification_error'
|
|
}
|
|
Add-Check 'package_hashes' $false 'package_integrity_failed' `
|
|
("逐文件大小或 SHA-256 校验失败;分类:{0}。" -f $manifestFailure)
|
|
}
|
|
|
|
$packageArchiveBinding = if ($manifestPassed) {
|
|
Test-PackageArchiveBinding $PackageArchivePath $manifest $manifestSha256
|
|
} else {
|
|
[ordered]@{
|
|
passed = $false
|
|
code = 'package_archive_manifest_invalid'
|
|
detail = '解包目录清单未通过,无法把最终 ZIP 绑定到本次预检。'
|
|
sha256 = $null
|
|
}
|
|
}
|
|
Add-Check 'package_archive_binding' $packageArchiveBinding.passed `
|
|
$packageArchiveBinding.code $packageArchiveBinding.detail
|
|
|
|
$packageCommit = ''
|
|
$packageVersion = ''
|
|
$expectedPackageVersionForBinding = [string]$ExpectedPackageVersion
|
|
$packageVersionBindingPassed = $false
|
|
$packageProvenancePassed = $false
|
|
$sqlCompatibilityPassed = $false
|
|
$gugaSupplyChainAuditPassed = $false
|
|
try {
|
|
$buildReportPath = Join-Path $root 'BUILD-VERIFICATION.json'
|
|
if (-not (Test-RegularFile $buildReportPath 1MB)) { throw 'build_report_missing' }
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$buildReport = [IO.File]::ReadAllText($buildReportPath, $strictUtf8) | ConvertFrom-Json
|
|
$packageCommit = ([string]$buildReport.sourceCommit).ToLowerInvariant()
|
|
$packageVersion = [string]$buildReport.packageVersion
|
|
if ([string]::IsNullOrWhiteSpace($expectedPackageVersionForBinding)) {
|
|
$archiveName = [IO.Path]::GetFileName($PackageArchivePath)
|
|
$archiveMatch = [Text.RegularExpressions.Regex]::Match(
|
|
$archiveName,
|
|
'^Lserp-AgentPet-(?<version>[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4})-win-x64\.zip$')
|
|
if ($archiveMatch.Success) {
|
|
$expectedPackageVersionForBinding =
|
|
$archiveMatch.Groups['version'].Value
|
|
}
|
|
}
|
|
$packageVersionBindingPassed =
|
|
-not [string]::IsNullOrWhiteSpace($expectedPackageVersionForBinding) -and
|
|
$packageVersion -ceq $expectedPackageVersionForBinding
|
|
$verification = $buildReport.automatedVerification
|
|
$deliveryTopology = $buildReport.deliveryTopology
|
|
$deliveryTopologyValid =
|
|
$null -ne $deliveryTopology -and
|
|
(Test-ExactProperties $deliveryTopology @(
|
|
'desktopBundleContainsLserpCli',
|
|
'desktopBundleContainsBridgeCli',
|
|
'bridgeCliPath',
|
|
'bridgeCliDatabaseDirectAccess',
|
|
'bridgeCliPublishMode',
|
|
'desktopBundleContainsLegacyErp',
|
|
'legacyArtifactMode',
|
|
'legacyBuildTool',
|
|
'runtimeRequiresLegacyArtifact')) -and
|
|
$deliveryTopology.desktopBundleContainsLserpCli -eq $false -and
|
|
$deliveryTopology.desktopBundleContainsBridgeCli -eq $true -and
|
|
[string]$deliveryTopology.bridgeCliPath -ceq `
|
|
'Host/lserp-agent-cli.exe' -and
|
|
$deliveryTopology.bridgeCliDatabaseDirectAccess -eq $false -and
|
|
[string]$deliveryTopology.bridgeCliPublishMode -ceq `
|
|
'win_x64_single_file_self_contained' -and
|
|
$deliveryTopology.desktopBundleContainsLegacyErp -eq $false -and
|
|
[string]$deliveryTopology.legacyArtifactMode -ceq `
|
|
'separate_signed_windows_build' -and
|
|
[string]$deliveryTopology.legacyBuildTool -ceq `
|
|
'Deployment/Build-LegacyErpAcceptance.ps1' -and
|
|
$deliveryTopology.runtimeRequiresLegacyArtifact -eq $true
|
|
$astrbotRuntimeEvidence = $verification.astrbotRuntimeContract
|
|
$sqlCompatibility = $verification.sqlServerCompatibility100
|
|
$sqlCompatibilityPassed =
|
|
(Test-ExactProperties $sqlCompatibility @(
|
|
'passed', 'failed', 'parserPackage', 'parserVersion', 'dialect')) -and
|
|
[int]$sqlCompatibility.passed -ge 10 -and
|
|
[int]$sqlCompatibility.failed -eq 0 -and
|
|
[string]$sqlCompatibility.parserPackage -ceq `
|
|
'Microsoft.SqlServer.TransactSql.ScriptDom' -and
|
|
[string]$sqlCompatibility.parserVersion -ceq '180.59.2' -and
|
|
[string]$sqlCompatibility.dialect -ceq 'TSql100'
|
|
$gugaBuildAudit = $verification.gugaSupplyChainAudit
|
|
$gugaAuditRelative = 'Deployment/guga-upstream-audit.v1.json'
|
|
$gugaAuditPath = Join-Path $root `
|
|
($gugaAuditRelative.Replace('/', [IO.Path]::DirectorySeparatorChar))
|
|
$gugaAuditSha256 = Get-RegularFileSha256 $gugaAuditPath 64KB
|
|
$gugaAudit = [IO.File]::ReadAllText(
|
|
$gugaAuditPath, $strictUtf8) | ConvertFrom-Json
|
|
$gugaAsset = $gugaAudit.asset
|
|
$gugaInstaller = $gugaAudit.installer
|
|
$gugaServiceSource = $gugaAudit.serviceSource
|
|
$gugaDecision = $gugaAudit.commercialDecision
|
|
$bannedGugaHashes = @(
|
|
'3ebd971ba59a0c988a6be0924669b4c5db9234bcc5d17d506e34eba332e6021f',
|
|
'f9f715811c26ca610764a7698e28f2e182882f097f4c60a3f00a79dd7530bd20',
|
|
'1b61ea2af98717b9ebe55beb4c6b820b89e9c42d4fdfeca21cf63ed3ad4e38da'
|
|
)
|
|
$bundledGugaAssets = @($manifest.files | Where-Object {
|
|
$relativePath = ([string]$_.path).Replace('\', '/')
|
|
$fileName = [IO.Path]::GetFileName($relativePath).ToLowerInvariant()
|
|
$fileName -ceq 'spritesheet.webp' -or
|
|
$fileName.EndsWith('.codex-pet.zip') -or
|
|
$bannedGugaHashes -ccontains ([string]$_.sha256).ToLowerInvariant()
|
|
})
|
|
$gugaSupplyChainAuditPassed =
|
|
(Test-ExactProperties $gugaBuildAudit @(
|
|
'passed', 'auditFile', 'auditSha256', 'assetId',
|
|
'installerPackage', 'installerVersion',
|
|
'installerTarballSha256', 'observedSpriteSha256',
|
|
'upstreamCommercialLicensePresent', 'assetBundled')) -and
|
|
$gugaBuildAudit.passed -is [bool] -and
|
|
$gugaBuildAudit.passed -eq $true -and
|
|
[string]$gugaBuildAudit.auditFile -ceq $gugaAuditRelative -and
|
|
[string]$gugaBuildAudit.auditSha256 -ceq $gugaAuditSha256 -and
|
|
[string]$gugaBuildAudit.assetId -ceq 'guga' -and
|
|
[string]$gugaBuildAudit.installerPackage -ceq 'codex-pets' -and
|
|
[string]$gugaBuildAudit.installerVersion -ceq '0.3.0' -and
|
|
[string]$gugaBuildAudit.installerTarballSha256 -ceq
|
|
'9ec8bf1ea09e6d8fdc17b33a594a178a9b20bd3dc6decbb22973758394c9c1c7' -and
|
|
[string]$gugaBuildAudit.observedSpriteSha256 -ceq
|
|
'1b61ea2af98717b9ebe55beb4c6b820b89e9c42d4fdfeca21cf63ed3ad4e38da' -and
|
|
$gugaBuildAudit.upstreamCommercialLicensePresent -is [bool] -and
|
|
$gugaBuildAudit.upstreamCommercialLicensePresent -eq $false -and
|
|
$gugaBuildAudit.assetBundled -is [bool] -and
|
|
$gugaBuildAudit.assetBundled -eq $false -and
|
|
(Test-ExactProperties $gugaAudit @(
|
|
'schemaVersion', 'auditedAtUtc', 'asset', 'installer',
|
|
'serviceSource', 'commercialDecision')) -and
|
|
[string]$gugaAudit.schemaVersion -ceq '1.0' -and
|
|
[string]$gugaAudit.auditedAtUtc -ceq
|
|
'2026-08-13T19:31:24+00:00' -and
|
|
(Test-ExactProperties $gugaAsset @(
|
|
'id', 'displayName', 'ownerHandle', 'ownerName',
|
|
'uploadedAtUtc', 'shareUrl', 'shareDataUrl', 'downloadUrl',
|
|
'packageSha256', 'packageSizeBytes', 'manifestSha256',
|
|
'spriteSha256', 'spriteSizeBytes', 'atlasSize',
|
|
'licenseMetadataPresent', 'licenseFilePresent')) -and
|
|
[string]$gugaAsset.id -ceq 'guga' -and
|
|
[string]$gugaAsset.ownerHandle -ceq 'circus' -and
|
|
[string]$gugaAsset.shareUrl -ceq
|
|
'https://codex-pets.net/share/guga' -and
|
|
[string]$gugaAsset.packageSha256 -ceq $bannedGugaHashes[0] -and
|
|
[long]$gugaAsset.packageSizeBytes -eq 1946012 -and
|
|
[string]$gugaAsset.manifestSha256 -ceq $bannedGugaHashes[1] -and
|
|
[string]$gugaAsset.spriteSha256 -ceq $bannedGugaHashes[2] -and
|
|
[long]$gugaAsset.spriteSizeBytes -eq 1945586 -and
|
|
[string]$gugaAsset.atlasSize -ceq '1536x1872' -and
|
|
$gugaAsset.licenseMetadataPresent -is [bool] -and
|
|
$gugaAsset.licenseMetadataPresent -eq $false -and
|
|
$gugaAsset.licenseFilePresent -is [bool] -and
|
|
$gugaAsset.licenseFilePresent -eq $false -and
|
|
(Test-ExactProperties $gugaInstaller @(
|
|
'packageName', 'version', 'registryUrl', 'tarballUrl',
|
|
'tarballSha1', 'tarballSha256', 'npmIntegrity',
|
|
'declaredLicense', 'repositoryDeclared', 'defaultApiBase',
|
|
'installRoot', 'writtenFiles', 'assetDigestVerification',
|
|
'assetSignatureVerification', 'assetLicenseVerification')) -and
|
|
[string]$gugaInstaller.packageName -ceq 'codex-pets' -and
|
|
[string]$gugaInstaller.version -ceq '0.3.0' -and
|
|
[string]$gugaInstaller.tarballSha1 -ceq
|
|
'82e41349ae63eb9e63099f2e06a56468182e2c90' -and
|
|
[string]$gugaInstaller.tarballSha256 -ceq
|
|
'9ec8bf1ea09e6d8fdc17b33a594a178a9b20bd3dc6decbb22973758394c9c1c7' -and
|
|
[string]$gugaInstaller.declaredLicense -ceq 'MIT' -and
|
|
$gugaInstaller.repositoryDeclared -is [bool] -and
|
|
$gugaInstaller.repositoryDeclared -eq $false -and
|
|
@($gugaInstaller.writtenFiles).Count -eq 2 -and
|
|
[string]$gugaInstaller.writtenFiles[0] -ceq 'pet.json' -and
|
|
[string]$gugaInstaller.writtenFiles[1] -ceq 'spritesheet.webp' -and
|
|
$gugaInstaller.assetDigestVerification -eq $false -and
|
|
$gugaInstaller.assetSignatureVerification -eq $false -and
|
|
$gugaInstaller.assetLicenseVerification -eq $false -and
|
|
(Test-ExactProperties $gugaServiceSource @(
|
|
'repository', 'commit', 'softwareLicense', 'licenseSha256',
|
|
'termsSourceSha256', 'termsEffectiveDate',
|
|
'uploadTermsScope')) -and
|
|
[string]$gugaServiceSource.repository -ceq
|
|
'https://github.com/portons/codex-pet-share' -and
|
|
[string]$gugaServiceSource.commit -ceq
|
|
'22725091da2787e8e525c9289cb7826a34be4950' -and
|
|
[string]$gugaServiceSource.softwareLicense -ceq 'MIT' -and
|
|
[string]$gugaServiceSource.licenseSha256 -ceq
|
|
'13e779572adacb503b7e7a0c676571fcd86114a73f6aa000412c24a9a06a97d3' -and
|
|
[string]$gugaServiceSource.termsSourceSha256 -ceq
|
|
'70ad12414864566b8cd469a7d2ca39fe60050686cecacc126ff1aca587f790bb' -and
|
|
[string]$gugaServiceSource.uploadTermsScope -ceq
|
|
'public-sharing-through-service' -and
|
|
(Test-ExactProperties $gugaDecision @(
|
|
'status', 'code', 'reason', 'requiredEvidence')) -and
|
|
[string]$gugaDecision.status -ceq 'external-license-required' -and
|
|
[string]$gugaDecision.code -ceq 'guga_commercial_license_missing' -and
|
|
@($gugaDecision.requiredEvidence).Count -eq 7 -and
|
|
$bundledGugaAssets.Count -eq 0
|
|
$astrbotRuntimeEvidenceValid =
|
|
(Test-ExactProperties $astrbotRuntimeEvidence @(
|
|
'schemaVersion', 'passed', 'repository', 'sourceTag',
|
|
'sourceCommit', 'runtimeVersion', 'versionSpecifier',
|
|
'pluginVersion', 'licenseExpression', 'licenseSha256',
|
|
'eulaSha256', 'criticalSourceFilesVerified',
|
|
'registeredTools')) -and
|
|
$astrbotRuntimeEvidence.schemaVersion -ceq '1.1' -and
|
|
$astrbotRuntimeEvidence.passed -eq $true -and
|
|
$astrbotRuntimeEvidence.repository -ceq `
|
|
'https://github.com/AstrBotDevs/AstrBot.git' -and
|
|
$astrbotRuntimeEvidence.sourceTag -ceq 'v4.27.2' -and
|
|
$astrbotRuntimeEvidence.sourceCommit -ceq `
|
|
'ad4fbfa90ca0c4ac2b30b3250e34dbf8fe7babbf' -and
|
|
$astrbotRuntimeEvidence.runtimeVersion -ceq '4.27.2' -and
|
|
$astrbotRuntimeEvidence.versionSpecifier -ceq '==4.27.2' -and
|
|
$astrbotRuntimeEvidence.pluginVersion -ceq '0.4.0' -and
|
|
$astrbotRuntimeEvidence.licenseExpression -ceq `
|
|
'AGPL-3.0-or-later' -and
|
|
$astrbotRuntimeEvidence.licenseSha256 -ceq `
|
|
'ccf7d08f932af3e813848881731113afbb7c80d0fd6d958e8d319002bf344d02' -and
|
|
$astrbotRuntimeEvidence.eulaSha256 -ceq `
|
|
'c332de7781e87c67d6d3beda463fa04705075a6bae9e52a252f7c639f6defd80' -and
|
|
[int]$astrbotRuntimeEvidence.criticalSourceFilesVerified -eq 20 -and
|
|
@($astrbotRuntimeEvidence.registeredTools).Count -eq 3 -and
|
|
[string]$astrbotRuntimeEvidence.registeredTools[0] -ceq 'erp_get_context' -and
|
|
[string]$astrbotRuntimeEvidence.registeredTools[1] -ceq `
|
|
'erp_get_capabilities' -and
|
|
[string]$astrbotRuntimeEvidence.registeredTools[2] -ceq 'erp_plan_command'
|
|
if ($buildReport.schemaVersion -ne '1.1' -or
|
|
-not $packageVersionBindingPassed -or
|
|
$packageCommit -notmatch '^[a-f0-9]{40}$' -or
|
|
$buildReport.sourceWorktreeDirty -ne $false -or
|
|
-not $deliveryTopologyValid -or
|
|
$verification.commandKernel.failed -ne 0 -or
|
|
$verification.desktopHost.failed -ne 0 -or
|
|
$verification.petWebUi.failed -ne 0 -or
|
|
$verification.astrbotPlugin.failed -ne 0 -or
|
|
$verification.astrbotPlugin.skipped -ne 0 -or
|
|
-not $astrbotRuntimeEvidenceValid -or
|
|
-not $gugaSupplyChainAuditPassed -or
|
|
$verification.deploymentContracts.failed -ne 0 -or
|
|
-not $sqlCompatibilityPassed -or
|
|
$verification.commandKernel.passed -le 0 -or
|
|
$verification.desktopHost.passed -le 0 -or
|
|
$verification.petWebUi.passed -le 0 -or
|
|
$verification.astrbotPlugin.passed -le 0 -or
|
|
$verification.deploymentContracts.passed -le 0 -or
|
|
$verification.legacyNet40ApiCompile -ne $true -or
|
|
$verification.winX64SelfContainedPublish -ne $true -or
|
|
$verification.bridgeCliWinX64SelfContainedPublish -ne $true -or
|
|
$verification.hostAuthenticode.signed -ne $true -or
|
|
([string]$verification.hostAuthenticode.certificateThumbprint).ToUpperInvariant() -ne
|
|
$HostCertificateThumbprint.ToUpperInvariant()) {
|
|
throw 'build_report_invalid'
|
|
}
|
|
$packageProvenancePassed = $true
|
|
}
|
|
catch {
|
|
$packageProvenancePassed = $false
|
|
}
|
|
Add-Check 'package_source_provenance' $packageProvenancePassed `
|
|
'package_source_provenance_invalid' '包必须来自相同干净提交、版本必须与 ExpectedPackageVersion 或标准 ZIP 文件名一致,且所有自动化契约测试为零失败、零跳过。'
|
|
Add-Check 'guga_supply_chain_audit' $gugaSupplyChainAuditPassed `
|
|
'guga_supply_chain_audit_invalid' `
|
|
'包必须携带锁定 npm/服务/素材哈希的 guga 上游审计,且不得夹带在线下载的素材;该审计不代替书面商用授权。'
|
|
Add-Check 'sqlserver_compatibility100_syntax' $sqlCompatibilityPassed `
|
|
'sqlserver_compatibility100_syntax_invalid' `
|
|
'所有随包 SQL 必须通过锁定版本 Microsoft ScriptDom TSql100 解析,并证明不会误接受高版本语法。'
|
|
|
|
$legacyBuildTool = Join-Path $root 'Deployment\Build-LegacyErpAcceptance.ps1'
|
|
Add-Check 'legacy_build_tool' (Test-RegularFile $legacyBuildTool 2MB) `
|
|
'legacy_build_tool_missing' '商用包必须携带旧 ERP/CLI 的可重复 Windows 构建与取证脚本。'
|
|
$legacyBuildContractTests = Join-Path $root 'Deployment\Test-DeploymentContracts.ps1'
|
|
Add-Check 'legacy_build_contract_tests' (Test-RegularFile $legacyBuildContractTests 2MB) `
|
|
'legacy_build_contract_tests_missing' '商用包必须携带旧 ERP/CLI 构建脚本的离线正负契约测试。'
|
|
$writeEvidenceTools = @(
|
|
(Join-Path $root 'Deployment\New-WorkflowWriteCasesTemplate.ps1'),
|
|
(Join-Path $root 'Deployment\New-WorkflowWriteIntegrationEvidence.ps1'),
|
|
(Join-Path $root 'Deployment\Invoke-WorkflowWriteCaseCapture.ps1'),
|
|
(Join-Path $root 'Deployment\Invoke-LserpFieldReadOnlyValidation.ps1'),
|
|
(Join-Path $root 'Deployment\field-readonly-validation.example.json'),
|
|
(Join-Path $root 'Deployment\Invoke-LserpReadOnlySessionPreflight.ps1'),
|
|
(Join-Path $root 'Deployment\Invoke-LserpSelectOnlyCatalogSnapshot.ps1'),
|
|
(Join-Path $root 'Deployment\Invoke-LserpSelectOnlyProfilePreflight.ps1'),
|
|
(Join-Path $root 'Deployment\New-WorkflowUatAuthorization.ps1'),
|
|
(Join-Path $root 'Deployment\New-WorkflowWriteUatCampaign.ps1'),
|
|
(Join-Path $root 'Deployment\Test-WorkflowWriteUatCampaign.ps1'),
|
|
(Join-Path $root 'Deployment\workflow-write-uat-case-catalog.v1.json'),
|
|
(Join-Path $root 'Deployment\New-WorkflowAcceptanceEvidence.ps1'),
|
|
(Join-Path $root 'Deployment\New-CustomerAcceptanceBundle.ps1')
|
|
)
|
|
$writeEvidenceToolsPresent = @($writeEvidenceTools | Where-Object {
|
|
Test-RegularFile $_ 2MB
|
|
}).Count -eq $writeEvidenceTools.Count
|
|
Add-Check 'workflow_write_evidence_tools' $writeEvidenceToolsPresent `
|
|
'workflow_write_evidence_tools_missing' '商用包必须携带经独立审批并锁定哈希的现场输入交接、绑定精确 ERP 会话的只读动态合同预检、UAT 授权、哈希绑定的 32 用例现场目录、活动编排/断点预检、受控采集、写用例模板、严格证据生成器、工作流签名和客户总验收工具。'
|
|
|
|
$workflowUatCaseCatalogPath = Join-Path $root `
|
|
'Deployment\workflow-write-uat-case-catalog.v1.json'
|
|
$expectedWorkflowUatCaseCatalogSha256 = `
|
|
'23eb6c4f308d4904bf3920ed37499f05521beebde9422026f9732983c16002d5'
|
|
$workflowUatCaseCatalogValid = $false
|
|
$workflowUatCaseCatalogStream = $null
|
|
try {
|
|
if (-not (Test-RegularFile $workflowUatCaseCatalogPath 256KB)) {
|
|
throw 'catalog_file_invalid'
|
|
}
|
|
$workflowUatCaseCatalogStream = [IO.File]::Open(
|
|
$workflowUatCaseCatalogPath, [IO.FileMode]::Open,
|
|
[IO.FileAccess]::Read, [IO.FileShare]::Read)
|
|
$catalogHash = Get-StreamSha256 $workflowUatCaseCatalogStream
|
|
if ($catalogHash -cne $expectedWorkflowUatCaseCatalogSha256) {
|
|
throw 'catalog_hash_mismatch'
|
|
}
|
|
$workflowUatCaseCatalogStream.Position = 0
|
|
$catalogReader = New-Object IO.StreamReader(
|
|
$workflowUatCaseCatalogStream, $strictUtf8, $true, 4096, $true)
|
|
try { $catalogText = $catalogReader.ReadToEnd() }
|
|
finally { $catalogReader.Dispose() }
|
|
$catalog = $catalogText | ConvertFrom-Json
|
|
if (-not (Test-ExactProperties $catalog @(
|
|
'schemaVersion', 'packageType', 'safety', 'workflows')) -or
|
|
[string]$catalog.schemaVersion -cne '1.0' -or
|
|
[string]$catalog.packageType -cne 'workflow_write_uat_case_catalog' -or
|
|
-not (Test-ExactProperties $catalog.safety @(
|
|
'productionUseProhibited',
|
|
'automaticDatabaseOrConfigurationChanges',
|
|
'approvedRestorePointRequired', 'containsCredentials',
|
|
'containsBusinessIdentifiers', 'executableInstructionsIncluded')) -or
|
|
$catalog.safety.productionUseProhibited -ne $true -or
|
|
$catalog.safety.automaticDatabaseOrConfigurationChanges -ne $false -or
|
|
$catalog.safety.approvedRestorePointRequired -ne $true -or
|
|
$catalog.safety.containsCredentials -ne $false -or
|
|
$catalog.safety.containsBusinessIdentifiers -ne $false -or
|
|
$catalog.safety.executableInstructionsIncluded -ne $false) {
|
|
throw 'catalog_contract_invalid'
|
|
}
|
|
$catalogWorkflows = @($catalog.workflows)
|
|
if ($catalogWorkflows.Count -ne 2 -or
|
|
[string]$catalogWorkflows[0].workflow -cne 'purchase' -or
|
|
[int]$catalogWorkflows[0].caseCount -ne 13 -or
|
|
@($catalogWorkflows[0].cases).Count -ne 13 -or
|
|
[string]$catalogWorkflows[1].workflow -cne 'leave' -or
|
|
[int]$catalogWorkflows[1].caseCount -ne 19 -or
|
|
@($catalogWorkflows[1].cases).Count -ne 19) {
|
|
throw 'catalog_coverage_invalid'
|
|
}
|
|
$purchaseCommitCatalogCase = @($catalogWorkflows[0].cases)[0]
|
|
$purchaseCommitPreconditions = [string]::Join(
|
|
"`n",
|
|
@($purchaseCommitCatalogCase.preconditions | ForEach-Object {
|
|
[string]$_
|
|
}))
|
|
$purchaseCommitDbaChecks = [string]::Join(
|
|
"`n",
|
|
@($purchaseCommitCatalogCase.dbaReadOnlyChecks | ForEach-Object {
|
|
[string]$_
|
|
}))
|
|
if ([string]$purchaseCommitCatalogCase.caseCode -cne
|
|
'purchase_unique_match_commit' -or
|
|
$purchaseCommitCatalogCase.sourceDocumentProofRequired -ne $true -or
|
|
-not $purchaseCommitPreconditions.Contains('脱敏电子 PDF') -or
|
|
-not $purchaseCommitPreconditions.Contains(
|
|
'pdfium_minimax_pages_v1') -or
|
|
-not $purchaseCommitDbaChecks.Contains('XML v3') -or
|
|
-not $purchaseCommitDbaChecks.Contains(
|
|
'pdfium_minimax_pages_v1')) {
|
|
throw 'catalog_pdf_evidence_contract_invalid'
|
|
}
|
|
$campaignGeneratorSource = [IO.File]::ReadAllText(
|
|
(Join-Path $root 'Deployment\New-WorkflowWriteUatCampaign.ps1'),
|
|
$strictUtf8)
|
|
$campaignCheckerSource = [IO.File]::ReadAllText(
|
|
(Join-Path $root 'Deployment\Test-WorkflowWriteUatCampaign.ps1'),
|
|
$strictUtf8)
|
|
if (-not $campaignGeneratorSource.Contains($expectedWorkflowUatCaseCatalogSha256) -or
|
|
-not $campaignCheckerSource.Contains($expectedWorkflowUatCaseCatalogSha256)) {
|
|
throw 'catalog_tool_binding_invalid'
|
|
}
|
|
$workflowUatCaseCatalogValid = $true
|
|
}
|
|
catch {
|
|
$workflowUatCaseCatalogValid = $false
|
|
}
|
|
finally {
|
|
if ($null -ne $workflowUatCaseCatalogStream) {
|
|
$workflowUatCaseCatalogStream.Dispose()
|
|
}
|
|
}
|
|
Add-Check 'workflow_uat_case_catalog' $workflowUatCaseCatalogValid `
|
|
'workflow_uat_case_catalog_invalid' '32 项现场目录必须保持受审原始字节、安全声明和采购 13/请假 19 覆盖,并由活动生成器及恢复检查器共同绑定其 SHA-256。'
|
|
|
|
$legacyEvidence = Test-LegacyBuildEvidence $LegacyArtifactRoot $packageCommit
|
|
Add-Check 'legacy_build_evidence' $legacyEvidence.passed $legacyEvidence.code $legacyEvidence.detail
|
|
$legacyErpSignature = $false
|
|
$legacyCliSignature = $false
|
|
$legacyCriticalSignatures = $false
|
|
if ($legacyEvidence.passed -and $runningOnWindows) {
|
|
try {
|
|
$criticalValid = $true
|
|
foreach ($signedPath in $legacyEvidence.signedPaths) {
|
|
$signature = Get-AuthenticodeSignature -LiteralPath $signedPath
|
|
if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or
|
|
$null -eq $signature.SignerCertificate -or
|
|
$signature.SignerCertificate.Thumbprint -ne $legacyEvidence.certificateThumbprint) {
|
|
$criticalValid = $false
|
|
}
|
|
}
|
|
$erpSignature = Get-AuthenticodeSignature -LiteralPath $legacyEvidence.erpPath
|
|
$cliSignature = Get-AuthenticodeSignature -LiteralPath $legacyEvidence.cliPath
|
|
$legacyErpSignature = $erpSignature.Status -eq `
|
|
[System.Management.Automation.SignatureStatus]::Valid
|
|
$legacyCliSignature = $cliSignature.Status -eq `
|
|
[System.Management.Automation.SignatureStatus]::Valid
|
|
$legacyCriticalSignatures = $criticalValid
|
|
}
|
|
catch {
|
|
$legacyErpSignature = $false
|
|
$legacyCliSignature = $false
|
|
$legacyCriticalSignatures = $false
|
|
}
|
|
}
|
|
Add-Check 'legacy_erp_authenticode' $legacyErpSignature `
|
|
'legacy_erp_signature_invalid' '最终 Ls_ERP.exe 必须具有有效 Authenticode 签名。'
|
|
Add-Check 'legacy_cli_authenticode' $legacyCliSignature `
|
|
'legacy_cli_signature_invalid' '最终 lserp-cli.exe 必须具有有效 Authenticode 签名。'
|
|
Add-Check 'legacy_bridge_authenticode' $legacyCriticalSignatures `
|
|
'legacy_bridge_signature_invalid' 'AgentBridge、CommandKernel 和 Core 关键程序集必须与 EXE 使用同一有效签名证书。'
|
|
|
|
$hostPath = Join-Path $root 'Host\Lskj.AgentPet.Host.exe'
|
|
$hostCriticalPaths = @(
|
|
$hostPath,
|
|
(Join-Path $root 'Host\Lskj.AgentPet.Host.dll'),
|
|
(Join-Path $root 'Host\Lskj.AgentPet.Host.Core.dll'),
|
|
$bridgeCliPath
|
|
)
|
|
$bundledFullCliCount = if ($manifestPassed) {
|
|
@($manifest.files | Where-Object {
|
|
[string]$_.path -match '(^|/)lserp-cli[.]exe$'
|
|
}).Count
|
|
} else { 1 }
|
|
$hostPresent = @($hostCriticalPaths | Where-Object {
|
|
-not (Test-RegularFile $_ 512MB)
|
|
}).Count -eq 0 -and
|
|
$bundledFullCliCount -eq 0 -and
|
|
-not [IO.File]::Exists((Join-Path $root 'Host\lserp-agent-cli.dll')) -and
|
|
-not [IO.File]::Exists((Join-Path $root 'Host\lserp-agent-cli.deps.json')) -and
|
|
-not [IO.File]::Exists((Join-Path $root 'Host\lserp-agent-cli.runtimeconfig.json'))
|
|
Add-Check 'desktop_host' $hostPresent 'host_missing' '自包含桌宠宿主必须存在。'
|
|
$hostSignaturesValid = $false
|
|
if ($hostPresent -and $runningOnWindows) {
|
|
$hostSignaturesValid = $true
|
|
$expectedHostThumbprint = $HostCertificateThumbprint.ToUpperInvariant()
|
|
foreach ($criticalPath in $hostCriticalPaths) {
|
|
$signature = Get-AuthenticodeSignature -LiteralPath $criticalPath
|
|
if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or
|
|
$null -eq $signature.SignerCertificate -or
|
|
$signature.SignerCertificate.Thumbprint.ToUpperInvariant() -ne
|
|
$expectedHostThumbprint) {
|
|
$hostSignaturesValid = $false
|
|
}
|
|
}
|
|
}
|
|
Add-Check 'host_authenticode' $hostSignaturesValid `
|
|
'host_signature_invalid' '桌宠与桥接 CLI 的一方 EXE/DLL 必须使用指定客户发布证书且签名有效。'
|
|
|
|
$bridgeCliIdentityValid = $false
|
|
if ($hostSignaturesValid -and $runningOnWindows) {
|
|
try {
|
|
$identityCorrelation = 'preflight-version-' + `
|
|
[Guid]::NewGuid().ToString('N')
|
|
$identityRaw = (& $bridgeCliPath version `
|
|
--correlation-id $identityCorrelation 2>$null | Out-String)
|
|
if ($LASTEXITCODE -ne 0 -or
|
|
[string]::IsNullOrWhiteSpace($identityRaw) -or
|
|
$identityRaw.Length -gt 64KB) {
|
|
throw 'bridge_cli_identity_execution_failed'
|
|
}
|
|
$identity = $identityRaw | ConvertFrom-Json
|
|
$identityData = $identity.data
|
|
$bridgeCliIdentityValid =
|
|
(Test-ExactProperties $identity @(
|
|
'ok', 'correlationId', 'data')) -and
|
|
$identity.ok -eq $true -and
|
|
[string]$identity.correlationId -ceq $identityCorrelation -and
|
|
(Test-ExactProperties $identityData @(
|
|
'component', 'version', 'protocolVersion', 'bridgeOnly',
|
|
'databaseDirectAccess', 'sessionSource')) -and
|
|
[string]$identityData.component -ceq 'lserp-agent-cli' -and
|
|
[string]$identityData.version -ceq $packageVersion -and
|
|
[string]$identityData.protocolVersion -ceq '1.0' -and
|
|
$identityData.bridgeOnly -eq $true -and
|
|
$identityData.databaseDirectAccess -eq $false -and
|
|
[string]$identityData.sessionSource -ceq `
|
|
'current_logged_in_erp_process'
|
|
}
|
|
catch { $bridgeCliIdentityValid = $false }
|
|
}
|
|
Add-Check 'bridge_cli_runtime_identity' $bridgeCliIdentityValid `
|
|
'bridge_cli_runtime_identity_invalid' `
|
|
'最终签名桥接 CLI 必须实际运行并逐字段证明包版本、协议、本地 ERP 会话来源和禁止数据库直连。'
|
|
|
|
$webViewVersion = if ($runningOnWindows) { Get-WebView2Version } else { $null }
|
|
$minimumWebViewVersion = [Version]'151.0.4129.50'
|
|
$parsedWebViewVersion = $null
|
|
$webViewVersionValid = -not [string]::IsNullOrWhiteSpace($webViewVersion) -and
|
|
[Version]::TryParse($webViewVersion, [ref]$parsedWebViewVersion)
|
|
$webViewCompatible = $webViewVersionValid -and
|
|
$parsedWebViewVersion -ge $minimumWebViewVersion
|
|
$webViewCode = if ([string]::IsNullOrWhiteSpace($webViewVersion)) {
|
|
'webview2_missing'
|
|
} else {
|
|
'webview2_version_unsupported'
|
|
}
|
|
$webViewDetail = if (-not $webViewVersionValid) {
|
|
'未检测到可解析的 Evergreen Runtime 版本。最低要求:151.0.4129.50。'
|
|
} elseif (-not $webViewCompatible) {
|
|
"版本:$webViewVersion;最低要求:151.0.4129.50。"
|
|
} else {
|
|
"版本:$webViewVersion"
|
|
}
|
|
Add-Check 'webview2_runtime' $webViewCompatible $webViewCode $webViewDetail
|
|
|
|
$expectedSpriteWidth = 1536
|
|
$expectedSpriteHeight = 1872
|
|
$maximumSpriteBytes = 20MB
|
|
$spriteFull = [IO.Path]::GetFullPath($SpritePath)
|
|
$licenseFull = [IO.Path]::GetFullPath($SpriteLicenseEvidence)
|
|
$astrBotComplianceFull = [IO.Path]::GetFullPath($AstrBotComplianceEvidence)
|
|
$miniMaxComplianceFull = [IO.Path]::GetFullPath($MiniMaxServiceComplianceEvidence)
|
|
$miniMaxProbeFull = [IO.Path]::GetFullPath($MiniMaxVisionProbeEvidence)
|
|
$rolloutFull = [IO.Path]::GetFullPath($RolloutPolicyPath)
|
|
$rolloutSha256 = Get-RegularFileSha256 $rolloutFull 256KB
|
|
$rolloutFileOk = -not [string]::IsNullOrWhiteSpace($rolloutSha256)
|
|
Add-Check 'rollout_policy_file' $rolloutFileOk 'rollout_policy_file_invalid' `
|
|
'命令发布策略必须是包外、非链接、不超过 256 KB 且可完整哈希的普通文件。'
|
|
$spriteDimensions = if ((Test-RegularFile $spriteFull $maximumSpriteBytes) -and
|
|
([IO.Path]::GetExtension($spriteFull) -eq '.webp')) {
|
|
Get-WebPDimensions $spriteFull $maximumSpriteBytes
|
|
} else {
|
|
$null
|
|
}
|
|
$spriteOk = $null -ne $spriteDimensions -and
|
|
$spriteDimensions.width -eq $expectedSpriteWidth -and
|
|
$spriteDimensions.height -eq $expectedSpriteHeight
|
|
$licenseExtension = [IO.Path]::GetExtension($licenseFull).ToLowerInvariant()
|
|
$licenseOk = (Test-RegularFile $licenseFull 16MB) -and
|
|
@('.pdf', '.p7s') -contains $licenseExtension
|
|
$astrBotComplianceExtension = [IO.Path]::GetExtension(
|
|
$astrBotComplianceFull).ToLowerInvariant()
|
|
$astrBotComplianceSha256 = if (
|
|
@('.pdf', '.p7s') -contains $astrBotComplianceExtension) {
|
|
Get-RegularFileSha256 $astrBotComplianceFull 16MB
|
|
} else { $null }
|
|
$astrBotComplianceOk = -not [string]::IsNullOrWhiteSpace(
|
|
$astrBotComplianceSha256)
|
|
$miniMaxComplianceExtension = [IO.Path]::GetExtension(
|
|
$miniMaxComplianceFull).ToLowerInvariant()
|
|
$miniMaxComplianceSha256 = if (
|
|
@('.pdf', '.p7s') -contains $miniMaxComplianceExtension) {
|
|
Get-RegularFileSha256 $miniMaxComplianceFull 16MB
|
|
} else { $null }
|
|
$miniMaxComplianceOk = -not [string]::IsNullOrWhiteSpace(
|
|
$miniMaxComplianceSha256)
|
|
Add-Check 'guga_sprite' $spriteOk 'sprite_invalid' `
|
|
'素材必须是包外、结构完整且尺寸为 1536×1872 的普通 WebP 文件。'
|
|
Add-Check 'guga_commercial_license_evidence' $licenseOk 'sprite_license_missing' '必须提供经人工复核的书面商用授权 PDF 或 P7S 证据。'
|
|
Add-Check 'astrbot_agpl_eula_compliance_evidence' $astrBotComplianceOk `
|
|
'astrbot_compliance_evidence_missing' `
|
|
'必须提供法务复核的 AstrBot AGPL-3.0-or-later、EULA、交付和网络源码方案 PDF 或 P7S。'
|
|
Add-Check 'minimax_api_service_compliance_evidence' $miniMaxComplianceOk `
|
|
'minimax_service_compliance_evidence_missing' `
|
|
'必须提供法务复核的 MiniMax API 服务条款、数据处理、区域、套餐与密钥管理方案 PDF 或 P7S。'
|
|
$miniMaxProbe = Test-MiniMaxVisionProbeEvidence `
|
|
$miniMaxProbeFull ([DateTimeOffset]::UtcNow)
|
|
Add-Check 'minimax_online_vision_probe_evidence' $miniMaxProbe.passed `
|
|
$miniMaxProbe.code $miniMaxProbe.detail
|
|
|
|
$credentialListing = ''
|
|
if ($runningOnWindows) {
|
|
$credentialListing = (& "$env:SystemRoot\System32\cmdkey.exe" "/list:$CredentialTarget" 2>$null | Out-String)
|
|
}
|
|
$credentialOk = $credentialListing.IndexOf($CredentialTarget, [StringComparison]::OrdinalIgnoreCase) -ge 0
|
|
Add-Check 'astrbot_credential' $credentialOk 'astrbot_credential_missing' 'AstrBot Key 必须位于 Windows 通用凭据中。'
|
|
Add-Check 'desktop_secret_environment' `
|
|
(-not $env:LSERP_ASTRBOT_API_KEY -and -not $env:MINIMAX_API_KEY) `
|
|
'desktop_secret_environment_forbidden' '桌宠进程环境不得包含 AstrBot 或 MiniMax Key。'
|
|
|
|
$uri = $null
|
|
$uriOk = [Uri]::TryCreate($AstrBotBaseUrl, [UriKind]::Absolute, [ref]$uri)
|
|
if ($uriOk) {
|
|
$uriOk = $uri.Scheme -in @('http', 'https') -and
|
|
$uri.IsLoopback -and
|
|
-not $uri.UserInfo -and -not $uri.Query -and -not $uri.Fragment
|
|
}
|
|
Add-Check 'astrbot_transport' $uriOk 'astrbot_loopback_required' `
|
|
'当前商用传输只支持同机 loopback AstrBot;远程模式必须先部署双向设备身份 Agent Gateway。'
|
|
$astrBotReachable = $uriOk -and (Test-TcpEndpoint $uri)
|
|
Add-Check 'astrbot_reachable' $astrBotReachable 'astrbot_unreachable' '只检查 TCP 连通性,不发送 Key。'
|
|
$astrbotRuntime = if ($astrBotReachable) {
|
|
Get-AstrBotRuntimeContract $uri
|
|
} else {
|
|
[ordered]@{
|
|
passed = $false
|
|
runtimeVersion = $null
|
|
codeVersion = $null
|
|
detail = 'AstrBot 不可达,无法验证运行时与磁盘代码版本。'
|
|
}
|
|
}
|
|
Add-Check 'astrbot_runtime_contract' $astrbotRuntime.passed `
|
|
'astrbot_runtime_contract_mismatch' $astrbotRuntime.detail
|
|
|
|
$bridgeFiles = @()
|
|
if ([IO.Directory]::Exists($BridgeDiscoveryDirectory)) {
|
|
$bridgeFiles = @(Get-ChildItem -LiteralPath $BridgeDiscoveryDirectory -Filter 'agentbridge-*.json' -File -ErrorAction SilentlyContinue)
|
|
}
|
|
$bridgeHealthy = $false
|
|
$rolloutAttested = $false
|
|
$sessionScopeAttested = $false
|
|
if ($runningOnWindows -and $legacyCliSignature -and $bridgeFiles.Count -gt 0) {
|
|
try {
|
|
$firstSession = Invoke-VerifiedBridgeRead 'context'
|
|
$health = Invoke-VerifiedBridgeRead 'health'
|
|
$lastSession = Invoke-VerifiedBridgeRead 'context'
|
|
$bridgeHealthy = $null -ne $health -and
|
|
(Test-ExactProperties $health @(
|
|
'status', 'protocolVersion', 'serverTimeUtc',
|
|
'commandCount', 'enabledCommandCount',
|
|
'operationalPolicy', 'rolloutPolicy', 'workflowUat')) -and
|
|
[string]$health.status -ceq 'ready' -and
|
|
[string]$health.protocolVersion -ceq '1.0'
|
|
$sessionScopeAttested =
|
|
(Test-ExpectedSessionContext $firstSession) -and
|
|
(Test-ExpectedSessionContext $lastSession) -and
|
|
[string]$firstSession.userId -ceq [string]$lastSession.userId -and
|
|
[string]$firstSession.userName -ceq [string]$lastSession.userName -and
|
|
[string]$firstSession.accountBook -ceq
|
|
[string]$lastSession.accountBook -and
|
|
[string]$firstSession.subSystemId -ceq
|
|
[string]$lastSession.subSystemId -and
|
|
[string]$firstSession.databaseScopeFingerprint -ceq
|
|
[string]$lastSession.databaseScopeFingerprint -and
|
|
[bool]$firstSession.isAdministrator -eq
|
|
[bool]$lastSession.isAdministrator
|
|
if ($bridgeHealthy) {
|
|
try {
|
|
$runtimeRollout = $health.rolloutPolicy
|
|
$expectedRolloutProperties = @(
|
|
'configured',
|
|
'failClosed',
|
|
'customerId',
|
|
'databaseScopeFingerprint',
|
|
'sourceSha256',
|
|
'defaultAction',
|
|
'ruleCount'
|
|
)
|
|
$actualRolloutProperties = @(
|
|
$runtimeRollout.PSObject.Properties |
|
|
ForEach-Object { $_.Name }
|
|
)
|
|
$rolloutShapeValid =
|
|
$actualRolloutProperties.Count -eq
|
|
$expectedRolloutProperties.Count
|
|
foreach ($name in $expectedRolloutProperties) {
|
|
$rolloutShapeValid = $rolloutShapeValid -and
|
|
($actualRolloutProperties -ccontains $name)
|
|
}
|
|
$runtimeRuleCount = -1
|
|
$ruleCountValid = [int]::TryParse(
|
|
[string]$runtimeRollout.ruleCount,
|
|
[ref]$runtimeRuleCount) -and
|
|
$runtimeRuleCount -ge 0 -and $runtimeRuleCount -le 128
|
|
$rolloutAttested = $rolloutShapeValid -and
|
|
$rolloutFileOk -and
|
|
$runtimeRollout.configured -eq $true -and
|
|
$runtimeRollout.failClosed -eq $true -and
|
|
[string]$runtimeRollout.customerId -ceq
|
|
$RolloutCustomerId -and
|
|
[string]$runtimeRollout.databaseScopeFingerprint -ceq
|
|
$ExpectedDatabaseScopeFingerprint -and
|
|
[string]$runtimeRollout.sourceSha256 -ceq
|
|
$rolloutSha256 -and
|
|
[string]$runtimeRollout.defaultAction -ceq 'deny' -and
|
|
$ruleCountValid
|
|
}
|
|
catch { $rolloutAttested = $false }
|
|
}
|
|
}
|
|
catch { $bridgeHealthy = $false }
|
|
}
|
|
Add-Check 'erp_agent_bridge' $bridgeHealthy 'erp_bridge_unhealthy' `
|
|
'必须由最终签名 CLI 实际连接 -ErpProcessId 指定的当前 ERP 命令桥。'
|
|
Add-Check 'erp_rollout_policy' $rolloutAttested 'erp_rollout_policy_mismatch' `
|
|
'目标 ERP 必须证明已加载与现场文件 SHA-256、客户部署标识和数据库作用域一致的默认拒绝发布策略。'
|
|
Add-Check 'erp_session_scope' $sessionScopeAttested `
|
|
'erp_session_scope_mismatch' `
|
|
'目标 ERP 在健康检查前后都必须逐字匹配预期用户、账套、子系统和数据库作用域;报告不保存这些原始标识。'
|
|
|
|
$astrbotPluginRoot = Join-Path $root 'AstrBotPlugin'
|
|
$expectedAstrBotPluginFiles = @(
|
|
'README.md',
|
|
'__init__.py',
|
|
'_conf_schema.json',
|
|
'astrbot-contract.json',
|
|
'astrbot_contract.py',
|
|
'attachment_extract.py',
|
|
'attachment_provenance.py',
|
|
'attachment_sandbox.py',
|
|
'attachment_worker.py',
|
|
'bridge_protocol.py',
|
|
'main.py',
|
|
'metadata.yaml',
|
|
'pdf_render_sandbox.py',
|
|
'pdf_render_worker.py',
|
|
'pdf_vision.py',
|
|
'plan_chain.py',
|
|
'prompt.py',
|
|
'purchase_tabular_binding.py',
|
|
'purchase_vision_binding.py',
|
|
'requirements.txt',
|
|
'session_auth.py',
|
|
'tools.py',
|
|
'verify_astrbot_contract.py',
|
|
'verify_minimax_vlm_contract.py',
|
|
'vision.py'
|
|
)
|
|
$astrbotPluginLayoutOk = $false
|
|
try {
|
|
$actualAstrBotPluginPaths = @(
|
|
[IO.Directory]::GetFiles(
|
|
$astrbotPluginRoot,
|
|
'*',
|
|
[IO.SearchOption]::TopDirectoryOnly)
|
|
)
|
|
$actualAstrBotPluginFiles = @(
|
|
$actualAstrBotPluginPaths |
|
|
ForEach-Object { [IO.Path]::GetFileName($_) }
|
|
)
|
|
$astrbotPluginLayoutOk =
|
|
@([IO.Directory]::GetDirectories(
|
|
$astrbotPluginRoot,
|
|
'*',
|
|
[IO.SearchOption]::TopDirectoryOnly)).Count -eq 0 -and
|
|
$actualAstrBotPluginFiles.Count -eq $expectedAstrBotPluginFiles.Count
|
|
foreach ($name in $expectedAstrBotPluginFiles) {
|
|
$astrbotPluginLayoutOk = $astrbotPluginLayoutOk -and
|
|
($actualAstrBotPluginFiles -ccontains $name)
|
|
}
|
|
foreach ($path in $actualAstrBotPluginPaths) {
|
|
$astrbotPluginLayoutOk = $astrbotPluginLayoutOk -and
|
|
(Test-RegularFile $path 2MB)
|
|
}
|
|
}
|
|
catch { $astrbotPluginLayoutOk = $false }
|
|
Add-Check 'astrbot_plugin_layout' $astrbotPluginLayoutOk `
|
|
'astrbot_plugin_layout_invalid' `
|
|
'AstrBot 插件目录只能包含受审的顶层白名单源码,禁止夹带 data、缓存、凭据或链接。'
|
|
|
|
$metadataPath = Join-Path $astrbotPluginRoot 'metadata.yaml'
|
|
$astrbotContractPath = Join-Path $root 'AstrBotPlugin\astrbot-contract.json'
|
|
$astrbotGuardPath = Join-Path $root 'AstrBotPlugin\astrbot_contract.py'
|
|
$astrbotMainPath = Join-Path $root 'AstrBotPlugin\main.py'
|
|
$metadataOk = $false
|
|
if ((Test-RegularFile $metadataPath 64KB) -and
|
|
(Test-RegularFile $astrbotContractPath 64KB) -and
|
|
(Test-RegularFile $astrbotGuardPath 64KB) -and
|
|
(Test-RegularFile $astrbotMainPath 512KB)) {
|
|
try {
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$metadata = [IO.File]::ReadAllText($metadataPath, $strictUtf8)
|
|
$contract = [IO.File]::ReadAllText(
|
|
$astrbotContractPath, $strictUtf8) | ConvertFrom-Json
|
|
$guard = [IO.File]::ReadAllText($astrbotGuardPath, $strictUtf8)
|
|
$main = [IO.File]::ReadAllText($astrbotMainPath, $strictUtf8)
|
|
$criticalHashProperties = @(
|
|
$contract.criticalSourceSha256.PSObject.Properties)
|
|
$criticalHashNames = @(
|
|
'astrbot/__init__.py',
|
|
'astrbot/api/__init__.py',
|
|
'astrbot/api/event/__init__.py',
|
|
'astrbot/api/message_components.py',
|
|
'astrbot/api/provider/__init__.py',
|
|
'astrbot/api/star/__init__.py',
|
|
'astrbot/core/agent/run_context.py',
|
|
'astrbot/core/agent/tool.py',
|
|
'astrbot/core/astr_agent_context.py',
|
|
'astrbot/core/message/components.py',
|
|
'astrbot/core/provider/entities.py',
|
|
'astrbot/core/star/base.py',
|
|
'astrbot/core/star/context.py',
|
|
'astrbot/core/star/star_manager.py',
|
|
'astrbot/dashboard/api/stats.py',
|
|
'astrbot/dashboard/responses.py',
|
|
'astrbot/dashboard/services/stat_service.py'
|
|
)
|
|
$criticalHashesValid = $criticalHashProperties.Count -eq `
|
|
$criticalHashNames.Count
|
|
foreach ($name in $criticalHashNames) {
|
|
$property = $contract.criticalSourceSha256.PSObject.Properties[$name]
|
|
$criticalHashesValid = $criticalHashesValid -and
|
|
$null -ne $property -and
|
|
([string]$property.Value) -cmatch '^[a-f0-9]{64}$'
|
|
}
|
|
$metadataOk = $metadata.Contains('version: 0.4.0') -and
|
|
$metadata.Contains('astrbot_version: "==4.27.2"') -and
|
|
(Test-ExactProperties $contract @(
|
|
'schemaVersion', 'repository', 'tag', 'commit',
|
|
'runtimeVersion', 'versionSpecifier', 'pluginVersion',
|
|
'license', 'licenseSha256', 'eulaSha256',
|
|
'projectMetadataSha256', 'criticalSourceSha256')) -and
|
|
$contract.schemaVersion -ceq '1.1' -and
|
|
$contract.repository -ceq `
|
|
'https://github.com/AstrBotDevs/AstrBot.git' -and
|
|
$contract.tag -ceq 'v4.27.2' -and
|
|
$contract.commit -ceq `
|
|
'ad4fbfa90ca0c4ac2b30b3250e34dbf8fe7babbf' -and
|
|
$contract.runtimeVersion -ceq '4.27.2' -and
|
|
$contract.versionSpecifier -ceq '==4.27.2' -and
|
|
$contract.pluginVersion -ceq '0.4.0' -and
|
|
$contract.license -ceq 'AGPL-3.0-or-later' -and
|
|
$contract.licenseSha256 -ceq `
|
|
'ccf7d08f932af3e813848881731113afbb7c80d0fd6d958e8d319002bf344d02' -and
|
|
$contract.eulaSha256 -ceq `
|
|
'c332de7781e87c67d6d3beda463fa04705075a6bae9e52a252f7c639f6defd80' -and
|
|
$contract.projectMetadataSha256 -ceq `
|
|
'd61527cc6ccb6163930f2b32f8518e8ca56a4247cd67e883adfc68a19dd233aa' -and
|
|
$criticalHashesValid -and
|
|
$guard.Contains('SUPPORTED_ASTRBOT_VERSION: Final = "4.27.2"') -and
|
|
$guard.Contains('SUPPORTED_ASTRBOT_SOURCE_SHA256: Final') -and
|
|
$guard.Contains('critical_source_hash_mismatch') -and
|
|
$guard.Contains('astrbot_runtime_contract_mismatch') -and
|
|
$main.Contains('assert_supported_astrbot_runtime()')
|
|
}
|
|
catch { $metadataOk = $false }
|
|
}
|
|
Add-Check 'astrbot_plugin_contract' $metadataOk 'astrbot_plugin_contract_invalid' `
|
|
'插件、运行时守卫和来源证据必须精确锁定 AstrBot 4.27.2。'
|
|
|
|
$visionPath = Join-Path $root 'AstrBotPlugin\vision.py'
|
|
$visionConfigPath = Join-Path $root 'AstrBotPlugin\_conf_schema.json'
|
|
$visionProbePath = Join-Path $root 'AstrBotPlugin\verify_minimax_vlm_contract.py'
|
|
$miniMaxDirectOk = $false
|
|
if ((Test-RegularFile $visionPath 64KB) -and
|
|
(Test-RegularFile $visionConfigPath 32KB) -and
|
|
(Test-RegularFile $visionProbePath 32KB) -and
|
|
-not (Test-Path -LiteralPath (Join-Path $root 'MmxRuntime'))) {
|
|
try {
|
|
$visionText = Get-Content -LiteralPath $visionPath -Raw -Encoding UTF8
|
|
$visionConfigText = Get-Content -LiteralPath $visionConfigPath -Raw -Encoding UTF8
|
|
$visionProbeText = Get-Content -LiteralPath $visionProbePath -Raw -Encoding UTF8
|
|
$miniMaxDirectOk =
|
|
$visionText.Contains('https://api.minimax.io/v1/coding_plan/vlm') -and
|
|
$visionText.Contains('https://api.minimaxi.com/v1/coding_plan/vlm') -and
|
|
$visionText.Contains('MINIMAX_VLM_CONTRACT_VERSION = "0.0.4"') -and
|
|
$visionText.Contains('MINIMAX_VLM_CONTRACT_COMMIT = "fbac3b3e56922a1249e00eebe07d9ee68f4768dc"') -and
|
|
$visionText.Contains('fbac3b3e56922a1249e00eebe07d9ee68f4768dc') -and
|
|
$visionText.Contains('08d4116a20e8a652ceb9e2b6f58b1e7cdfe464b14baff05977e08b4b05b66be3') -and
|
|
$visionText.Contains('1dea28d6ba4ee46ba516d7eeedd325a5a102410bb7abb074fc4b0a8a66571864') -and
|
|
$visionText.Contains('MM-API-Source') -and
|
|
$visionText.Contains('"MM-API-Source": MINIMAX_API_SOURCE') -and
|
|
$visionText.Contains('MINIMAX_API_SOURCE = "Minimax-MCP"') -and
|
|
$visionText.Contains('urllib.request.ProxyHandler({})') -and
|
|
$visionText.Contains('_NoRedirectHandler()') -and
|
|
$visionText.Contains('MINIMAX_API_KEY') -and
|
|
$visionText.Contains('assert_minimax_vision_runtime') -and
|
|
-not $visionText.Contains('create_subprocess_exec') -and
|
|
-not $visionText.Contains('mmx-cli') -and
|
|
$visionConfigText.Contains('"minimax_vision_enabled"') -and
|
|
$visionConfigText.Contains('"minimax_api_region"') -and
|
|
-not $visionConfigText.Contains('"mmx_executable"') -and
|
|
$visionProbeText.Contains('def synthetic_probe_png()') -and
|
|
$visionProbeText.Contains('SYNTHETIC_PROBE_SHA256 = (') -and
|
|
$visionProbeText.Contains(
|
|
'd37476a5273821c12ee4a72b512dc152db5729055b6febb8603985f86243abda') -and
|
|
$visionProbeText.Contains('if digest != SYNTHETIC_PROBE_SHA256:') -and
|
|
$visionProbeText.Contains('vision_probe_source_contract_invalid') -and
|
|
$visionProbeText.Contains('describe_business_image(') -and
|
|
$visionProbeText.Contains('os.O_EXCL') -and
|
|
$visionProbeText.Contains('"syntheticSourceSha256"') -and
|
|
-not $visionProbeText.Contains('image_source')
|
|
}
|
|
catch { $miniMaxDirectOk = $false }
|
|
}
|
|
Add-Check 'minimax_direct_https_vlm' $miniMaxDirectOk `
|
|
'minimax_direct_https_contract_invalid' `
|
|
'必须由 AstrBot 服务端使用固定区域、禁代理、禁重定向的 HTTPS VLM,并提供不含客户数据的合成图片在线探针;交付包不得包含 mmx-cli 或 Node 运行时。'
|
|
|
|
$pdfRenderSandboxPath = Join-Path $root 'AstrBotPlugin\pdf_render_sandbox.py'
|
|
$pdfRenderWorkerPath = Join-Path $root 'AstrBotPlugin\pdf_render_worker.py'
|
|
$pdfVisionPath = Join-Path $root 'AstrBotPlugin\pdf_vision.py'
|
|
$purchaseTabularPath = Join-Path $root `
|
|
'AstrBotPlugin\purchase_tabular_binding.py'
|
|
$attachmentExtractContractPath = Join-Path $root `
|
|
'AstrBotPlugin\attachment_extract.py'
|
|
$attachmentProvenanceContractPath = Join-Path $root `
|
|
'AstrBotPlugin\attachment_provenance.py'
|
|
$sourceDocumentSchemaPath = Join-Path $root `
|
|
'Deployment\SqlServer\001_agent_business_idempotency.sql'
|
|
$purchaseWriteContractPath = Join-Path $root `
|
|
'Deployment\customer-profiles\lserp-ai.workflow-write.purchase.compat100.draft.sql'
|
|
$purchaseWireContractPath = Join-Path $root `
|
|
'Contracts\erp-agent-wire-contract-v1.json'
|
|
$requirementsPath = Join-Path $root 'AstrBotPlugin\requirements.txt'
|
|
$pdfiumWheelName = 'pypdfium2-5.12.1-py3-none-win_amd64.whl'
|
|
$pdfiumWheelPath = Join-Path $root ('PythonWheels\' + $pdfiumWheelName)
|
|
$pdfiumWheelSha256 = `
|
|
'9609be73a6701a68f29dffe0335f7a2e4b3ba581542ed65d35d49f761a4600ca'
|
|
$pdfInvoicePipelineOk = $false
|
|
$pdfiumWheelStream = $null
|
|
$pdfiumWheelArchive = $null
|
|
try {
|
|
$pythonWheelsPath = Join-Path $root 'PythonWheels'
|
|
if (-not [IO.Directory]::Exists($pythonWheelsPath)) {
|
|
throw 'pdfium_wheel_directory_missing'
|
|
}
|
|
$pdfiumWheels = @([IO.Directory]::GetFiles(
|
|
$pythonWheelsPath,
|
|
'pypdfium2-*.whl',
|
|
[IO.SearchOption]::TopDirectoryOnly))
|
|
if ($pdfiumWheels.Count -ne 1 -or
|
|
[IO.Path]::GetFileName($pdfiumWheels[0]) -cne $pdfiumWheelName -or
|
|
(Get-RegularFileSha256 $pdfiumWheelPath 32MB) -cne
|
|
$pdfiumWheelSha256) {
|
|
throw 'pdfium_wheel_identity_invalid'
|
|
}
|
|
foreach ($sourcePath in @(
|
|
$pdfRenderSandboxPath,
|
|
$pdfRenderWorkerPath,
|
|
$pdfVisionPath,
|
|
$purchaseTabularPath,
|
|
$attachmentExtractContractPath,
|
|
$attachmentProvenanceContractPath,
|
|
$sourceDocumentSchemaPath,
|
|
$purchaseWriteContractPath,
|
|
$purchaseWireContractPath,
|
|
$requirementsPath)) {
|
|
if (-not (Test-RegularFile $sourcePath 512KB)) {
|
|
throw 'pdf_pipeline_source_missing'
|
|
}
|
|
}
|
|
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$requirementsText = [IO.File]::ReadAllText(
|
|
$requirementsPath, $strictUtf8)
|
|
$pdfRenderSandboxText = [IO.File]::ReadAllText(
|
|
$pdfRenderSandboxPath, $strictUtf8)
|
|
$pdfRenderWorkerText = [IO.File]::ReadAllText(
|
|
$pdfRenderWorkerPath, $strictUtf8)
|
|
$pdfVisionText = [IO.File]::ReadAllText($pdfVisionPath, $strictUtf8)
|
|
$purchaseTabularText = [IO.File]::ReadAllText(
|
|
$purchaseTabularPath, $strictUtf8)
|
|
$attachmentMainText = [IO.File]::ReadAllText(
|
|
$astrbotMainPath, $strictUtf8)
|
|
$attachmentExtractContractText = [IO.File]::ReadAllText(
|
|
$attachmentExtractContractPath, $strictUtf8)
|
|
$attachmentProvenanceContractText = [IO.File]::ReadAllText(
|
|
$attachmentProvenanceContractPath, $strictUtf8)
|
|
$sourceDocumentSchemaText = [IO.File]::ReadAllText(
|
|
$sourceDocumentSchemaPath, $strictUtf8)
|
|
$purchaseWriteContractText = [IO.File]::ReadAllText(
|
|
$purchaseWriteContractPath, $strictUtf8)
|
|
$purchaseWireContractText = [IO.File]::ReadAllText(
|
|
$purchaseWireContractPath, $strictUtf8)
|
|
|
|
if (-not $requirementsText.Contains('pypdfium2==5.12.1 \') -or
|
|
-not $requirementsText.Contains(
|
|
'--hash=sha256:9609be73a6701a68f29dffe0335f7a2e4b3ba581542ed65d35d49f761a4600ca') -or
|
|
-not $pdfRenderSandboxText.Contains('MAX_PDF_VISION_PAGES = 3') -or
|
|
-not $pdfRenderSandboxText.Contains('asyncio.create_subprocess_exec(') -or
|
|
-not $pdfRenderSandboxText.Contains('"-I",') -or
|
|
-not $pdfRenderSandboxText.Contains('"-B",') -or
|
|
-not $pdfRenderSandboxText.Contains('_validate_rgb_png(') -or
|
|
-not $pdfRenderWorkerText.Contains('import pypdfium2 as pdfium') -or
|
|
-not $pdfRenderWorkerText.Contains(
|
|
'sys.addaudithook(_deny_unsafe_runtime_operations)') -or
|
|
-not $pdfRenderWorkerText.Contains('may_draw_forms=False') -or
|
|
-not $pdfRenderWorkerText.Contains(
|
|
'if page_count > limits["maximumPages"]:') -or
|
|
-not $pdfRenderWorkerText.Contains(
|
|
'source_bytes = _read_source_snapshot(') -or
|
|
-not $pdfVisionText.Contains(
|
|
'PDF_VISION_PIPELINE = "pdfium_minimax_pages_v1"') -or
|
|
-not $pdfVisionText.Contains('describe_business_image_bytes(') -or
|
|
-not $pdfVisionText.Contains('merge_purchase_vision_documents(') -or
|
|
-not $pdfVisionText.Contains('project_pdf_vision_content(content)') -or
|
|
-not $purchaseTabularText.Contains(
|
|
'return project_pdf_vision_content(envelope["content"])') -or
|
|
-not $attachmentMainText.Contains('describe_business_pdf(') -or
|
|
-not $attachmentMainText.Contains(
|
|
'maximum_pages=remaining_pages') -or
|
|
-not $attachmentExtractContractText.Contains(
|
|
'PREPROCESS_CONTRACT_PDF = "pdfium_minimax_pages_v1"') -or
|
|
-not $attachmentExtractContractText.Contains(
|
|
'result["preprocessContract"] = _preprocess_contract(') -or
|
|
-not $attachmentExtractContractText.Contains(
|
|
'content.get("pipeline") != PREPROCESS_CONTRACT_PDF') -or
|
|
-not $attachmentProvenanceContractText.Contains(
|
|
'"preprocessContract": preprocess_contract') -or
|
|
-not $attachmentProvenanceContractText.Contains(
|
|
'expected["preprocessContract"] != preprocess_contract') -or
|
|
-not $sourceDocumentSchemaText.Contains(
|
|
'preprocess_contract VARCHAR(64) NOT NULL') -or
|
|
-not $sourceDocumentSchemaText.Contains(
|
|
"'pdfium_minimax_pages_v1'") -or
|
|
-not $purchaseWriteContractText.Contains(
|
|
'/source_documents[@version="3"]') -or
|
|
-not $purchaseWriteContractText.Contains(
|
|
"'(@preprocess_contract)[1]'") -or
|
|
-not $purchaseWriteContractText.Contains(
|
|
'source_sha256, extraction_sha256, preprocess_contract, size_bytes') -or
|
|
-not $purchaseWireContractText.Contains(
|
|
'"commandVersion": "1.4"')) {
|
|
throw 'pdf_pipeline_contract_invalid'
|
|
}
|
|
|
|
Add-Type -AssemblyName System.IO.Compression -ErrorAction Stop
|
|
try {
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem `
|
|
-ErrorAction Stop
|
|
}
|
|
catch {
|
|
if ($null -eq ('System.IO.Compression.ZipArchive' -as [type])) {
|
|
throw
|
|
}
|
|
}
|
|
$pdfiumWheelStream = [IO.File]::Open(
|
|
$pdfiumWheelPath,
|
|
[IO.FileMode]::Open,
|
|
[IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read)
|
|
$pdfiumWheelArchive = [IO.Compression.ZipArchive]::new(
|
|
$pdfiumWheelStream,
|
|
[IO.Compression.ZipArchiveMode]::Read,
|
|
$true)
|
|
if ($pdfiumWheelArchive.Entries.Count -lt 1 -or
|
|
$pdfiumWheelArchive.Entries.Count -gt 256) {
|
|
throw 'pdfium_wheel_layout_invalid'
|
|
}
|
|
$wheelEntries = New-Object `
|
|
'System.Collections.Generic.HashSet[string]' `
|
|
([StringComparer]::Ordinal)
|
|
[long]$wheelUncompressedBytes = 0
|
|
foreach ($entry in $pdfiumWheelArchive.Entries) {
|
|
if (-not $wheelEntries.Add([string]$entry.FullName) -or
|
|
$entry.Length -lt 0 -or $entry.Length -gt 64MB -or
|
|
$wheelUncompressedBytes -gt 128MB - $entry.Length) {
|
|
throw 'pdfium_wheel_layout_invalid'
|
|
}
|
|
$wheelUncompressedBytes += $entry.Length
|
|
}
|
|
foreach ($requiredWheelEntry in @(
|
|
'pypdfium2_raw/pdfium.dll',
|
|
'pypdfium2-5.12.1.dist-info/licenses/LICENSES/Apache-2.0.txt',
|
|
'pypdfium2-5.12.1.dist-info/licenses/LICENSES/BSD-3-Clause.txt',
|
|
'pypdfium2-5.12.1.dist-info/licenses/LICENSES/CC-BY-4.0.txt',
|
|
'pypdfium2-5.12.1.dist-info/licenses/data/windows_x64/BUILD_LICENSES/pdfium.txt',
|
|
'pypdfium2-5.12.1.dist-info/licenses/data/windows_x64/BUILD_LICENSES/pdfium-binaries.txt')) {
|
|
if (-not $wheelEntries.Contains($requiredWheelEntry)) {
|
|
throw 'pdfium_wheel_license_or_runtime_missing'
|
|
}
|
|
}
|
|
$pdfInvoicePipelineOk = $true
|
|
}
|
|
catch {
|
|
$pdfInvoicePipelineOk = $false
|
|
}
|
|
finally {
|
|
if ($null -ne $pdfiumWheelArchive) { $pdfiumWheelArchive.Dispose() }
|
|
if ($null -ne $pdfiumWheelStream) { $pdfiumWheelStream.Dispose() }
|
|
}
|
|
Add-Check 'pdf_invoice_pipeline' $pdfInvoicePipelineOk `
|
|
'pdf_invoice_pipeline_invalid' `
|
|
'电子 PDF 发票必须使用最多三页的隔离 PDFium 渲染、逐页 MiniMax 严格识别和原文件摘要绑定,并只携带精确哈希且内含许可证的 Windows x64 wheel。'
|
|
|
|
$attachmentSnapshotOk = $false
|
|
$attachmentMainPath = Join-Path $root 'AstrBotPlugin\main.py'
|
|
$attachmentExtractPath = Join-Path $root 'AstrBotPlugin\attachment_extract.py'
|
|
$attachmentSandboxPath = Join-Path $root 'AstrBotPlugin\attachment_sandbox.py'
|
|
$attachmentWorkerPath = Join-Path $root 'AstrBotPlugin\attachment_worker.py'
|
|
if ((Test-RegularFile $attachmentMainPath 128KB) -and
|
|
(Test-RegularFile $attachmentExtractPath 128KB) -and
|
|
(Test-RegularFile $attachmentSandboxPath 128KB) -and
|
|
(Test-RegularFile $attachmentWorkerPath 64KB) -and
|
|
(Test-RegularFile $visionPath 64KB)) {
|
|
try {
|
|
$attachmentMainText = Get-Content -LiteralPath $attachmentMainPath `
|
|
-Raw -Encoding UTF8
|
|
$attachmentExtractText = Get-Content -LiteralPath $attachmentExtractPath `
|
|
-Raw -Encoding UTF8
|
|
$attachmentSandboxText = Get-Content -LiteralPath $attachmentSandboxPath `
|
|
-Raw -Encoding UTF8
|
|
$attachmentWorkerText = Get-Content -LiteralPath $attachmentWorkerPath `
|
|
-Raw -Encoding UTF8
|
|
$attachmentSnapshotOk =
|
|
([Text.RegularExpressions.Regex]::Matches(
|
|
$attachmentMainText,
|
|
'expected_sha256\s*=\s*str\(\s*receipt_before\["sha256"\]\s*\)').Count -eq 3) -and
|
|
([Text.RegularExpressions.Regex]::Matches(
|
|
$attachmentMainText,
|
|
'expected_size_bytes\s*=\s*int\(\s*receipt_before\["sizeBytes"\]\s*\)').Count -eq 3) -and
|
|
$attachmentExtractText.Contains('_read_stable_source(') -and
|
|
$attachmentExtractText.Contains('_verify_expected_source(') -and
|
|
$attachmentExtractText.Contains('io.BytesIO(source_bytes)') -and
|
|
$attachmentSandboxText.Contains('"expectedSource"') -and
|
|
$attachmentWorkerText.Contains('"expectedSource"') -and
|
|
$visionText.Contains('_read_validated_image(') -and
|
|
$visionText.Contains(
|
|
'hashlib.sha256(image_bytes).hexdigest() != expected_sha256') -and
|
|
$visionText.Contains('attachment_changed_during_preprocess')
|
|
}
|
|
catch { $attachmentSnapshotOk = $false }
|
|
}
|
|
Add-Check 'attachment_snapshot_binding' $attachmentSnapshotOk `
|
|
'attachment_snapshot_binding_invalid' `
|
|
'视觉与文档预处理必须把实际读取的单一字节快照绑定到识别前来源 SHA-256 和大小。'
|
|
|
|
$wireContractPath = Join-Path $root 'Contracts\erp-agent-wire-contract-v1.json'
|
|
$wireContractOk = $false
|
|
if (Test-RegularFile $wireContractPath 256KB) {
|
|
try {
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$wireContract = [IO.File]::ReadAllText(
|
|
$wireContractPath, $strictUtf8) | ConvertFrom-Json
|
|
$expectedPlanFields = @(
|
|
'planId', 'commandName', 'commandVersion', 'moduleCode', 'risk',
|
|
'createdAtUtc', 'expiresAtUtc', 'valid', 'executionAllowed',
|
|
'inputFingerprint', 'outcomeCode', 'title', 'preview', 'data',
|
|
'warnings'
|
|
)
|
|
$actualPlanFields = @($wireContract.planProjectionFields)
|
|
$fieldsValid = $actualPlanFields.Count -eq $expectedPlanFields.Count
|
|
for ($index = 0; $index -lt $expectedPlanFields.Count; $index++) {
|
|
$fieldsValid = $fieldsValid -and
|
|
[string]$actualPlanFields[$index] -ceq $expectedPlanFields[$index]
|
|
}
|
|
$expectedScenarios = @(
|
|
'purchase_resolve_to_create',
|
|
'leave_resolve_to_create',
|
|
'leave_submit_followup',
|
|
'module_diagnose_read_only',
|
|
'module_trace_initialization',
|
|
'dynamic_module_resolve_to_create',
|
|
'dynamic_module_resolve_to_update',
|
|
'module_navigate'
|
|
)
|
|
$scenarios = @($wireContract.scenarios)
|
|
$scenarioNames = @($scenarios | ForEach-Object { [string]$_.name })
|
|
$scenariosValid = $scenarios.Count -eq $expectedScenarios.Count
|
|
foreach ($name in $expectedScenarios) {
|
|
$scenariosValid = $scenariosValid -and ($scenarioNames -ccontains $name)
|
|
}
|
|
foreach ($scenario in $scenarios) {
|
|
$plan = $scenario.plan
|
|
$createdAt = [DateTimeOffset]::MinValue
|
|
$expiresAt = [DateTimeOffset]::MinValue
|
|
$createdValid = [DateTimeOffset]::TryParse(
|
|
[string]$plan.createdAtUtc, [ref]$createdAt)
|
|
$expiresValid = [DateTimeOffset]::TryParse(
|
|
[string]$plan.expiresAtUtc, [ref]$expiresAt)
|
|
$risk = [string]$plan.risk
|
|
$executionExpected = $plan.valid -eq $true -and
|
|
@('navigate', 'write', 'critical') -ccontains $risk
|
|
$scenariosValid = $scenariosValid -and
|
|
(Test-ExactProperties $scenario @(
|
|
'name', 'requestedCommand', 'autoFollowedFrom', 'plan')) -and
|
|
(Test-ExactProperties $plan $expectedPlanFields) -and
|
|
([string]$plan.planId -cmatch '^[A-Fa-f0-9]{32}$') -and
|
|
([string]$plan.commandName -cmatch '^[A-Za-z0-9_.:-]{1,128}$') -and
|
|
([string]$plan.commandVersion -cmatch '^[0-9]+(?:\.[0-9]+){1,3}$') -and
|
|
([string]$plan.moduleCode -cmatch '^[A-Za-z0-9_.:-]{1,128}$') -and
|
|
(@('read', 'navigate', 'draft', 'write', 'critical') -ccontains $risk) -and
|
|
([string]$plan.inputFingerprint -cmatch '^[a-f0-9]{64}$') -and
|
|
([string]$plan.outcomeCode -cmatch '^[A-Za-z0-9_.:-]{1,128}$') -and
|
|
($plan.valid -is [bool]) -and
|
|
($plan.executionAllowed -is [bool]) -and
|
|
($plan.executionAllowed -eq $executionExpected) -and
|
|
$createdValid -and $expiresValid -and
|
|
$expiresAt -gt $createdAt -and
|
|
($expiresAt - $createdAt) -le [TimeSpan]::FromMinutes(15) -and
|
|
$null -ne $plan.data -and
|
|
@($plan.warnings).Count -le 64
|
|
}
|
|
$wireContractOk =
|
|
(Test-ExactProperties $wireContract @(
|
|
'schemaVersion', 'protocolVersion', 'requestSessionScope',
|
|
'planProjectionFields', 'trustedBridgeCorrelationId',
|
|
'scenarios')) -and
|
|
$wireContract.schemaVersion -ceq '1.1' -and
|
|
$wireContract.protocolVersion -ceq '1.0' -and
|
|
$wireContract.requestSessionScope.field -ceq 'sessionScopeToken' -and
|
|
$wireContract.requestSessionScope.tokenVersion -ceq 'v3' -and
|
|
$wireContract.requestSessionScope.format -ceq '32-lowercase-hex' -and
|
|
@($wireContract.requestSessionScope.requiredMethods).Count -eq 3 -and
|
|
@($wireContract.requestSessionScope.bootstrapOptionalMethods).Count -eq 2 -and
|
|
@($wireContract.requestSessionScope.boundFields).Count -eq 6 -and
|
|
$wireContract.requestSessionScope.serverAuthoritativeRecheck -eq $true -and
|
|
([string]$wireContract.trustedBridgeCorrelationId -cmatch
|
|
'^[A-Za-z0-9_.:-]{8,128}$') -and
|
|
$fieldsValid -and $scenariosValid
|
|
}
|
|
catch { $wireContractOk = $false }
|
|
}
|
|
Add-Check 'erp_wire_contract' $wireContractOk `
|
|
'erp_wire_contract_invalid' `
|
|
'交付包必须携带版本化、精确字段且覆盖采购、请假和模块诊断的 ERP Agent 线协议。'
|
|
|
|
$passed = ($checks | Where-Object { -not $_.passed }).Count -eq 0
|
|
$spriteHash = if ($spriteOk) { $spriteDimensions.sha256 } else { $null }
|
|
$licenseHash = if ($licenseOk) { (Get-FileHash -LiteralPath $licenseFull -Algorithm SHA256).Hash.ToLowerInvariant() } else { $null }
|
|
$nextStep = if ($passed) {
|
|
'继续执行 CUSTOMER_ACCEPTANCE.md 的客户 ERP/SQL Server 三条真实闭环;本报告本身不启用写命令。'
|
|
} else {
|
|
'修复失败门禁后生成新报告;不得启用客户写命令。'
|
|
}
|
|
$report = [ordered]@{
|
|
schemaVersion = '1.7'
|
|
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
|
|
passed = $passed
|
|
packageManifestVerified = $manifestPassed
|
|
packageSha256 = $packageArchiveBinding.sha256
|
|
packageSourceCommit = $packageCommit
|
|
legacyBuildEvidenceSha256 = $legacyEvidence.evidenceSha256
|
|
rolloutPolicySha256 = $rolloutSha256
|
|
rolloutCustomerId = $RolloutCustomerId
|
|
spriteSha256 = $spriteHash
|
|
spriteLicenseEvidenceSha256 = $licenseHash
|
|
astrBotComplianceEvidenceSha256 = $astrBotComplianceSha256
|
|
miniMaxServiceComplianceEvidenceSha256 = $miniMaxComplianceSha256
|
|
miniMaxIntegrationMode = 'direct_https_vlm'
|
|
miniMaxVisionProbeEvidenceSha256 = $miniMaxProbe.sha256
|
|
miniMaxVisionProbeObservedAtUtc = $miniMaxProbe.observedAtUtc
|
|
miniMaxVisionProbeRegion = $miniMaxProbe.region
|
|
miniMaxVisionProbeContractVersion = $miniMaxProbe.contractVersion
|
|
checks = $checks
|
|
nextStep = $nextStep
|
|
}
|
|
[IO.Directory]::CreateDirectory([IO.Path]::GetFullPath($ReportDirectory)) | Out-Null
|
|
$reportPath = Join-Path ([IO.Path]::GetFullPath($ReportDirectory)) `
|
|
("commercial-preflight-{0}.json" -f ([DateTime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ')))
|
|
$utf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$bytes = $utf8.GetBytes(($report | ConvertTo-Json -Depth 10))
|
|
$stream = [IO.File]::Open($reportPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
|
try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush() }
|
|
finally { $stream.Dispose() }
|
|
|
|
[ordered]@{
|
|
passed = $passed
|
|
reportPath = $reportPath
|
|
failedChecks = @($checks | Where-Object { -not $_.passed } | ForEach-Object { $_.name })
|
|
} | ConvertTo-Json -Depth 4
|
|
if (-not $passed) { exit 6 }
|