Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/Deployment/CommercialPackage/Verify-LserpCommercialPackage.ps1
T
2026-08-14 14:28:28 +08:00

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 }