2026-08-26 06:41:07 +08:00
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$root = Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $root
$api = 'http://localhost:58080'
$tokenUrl = 'http://localhost:58180/realms/voice-local/protocol/openid-connect/token'
$tenant = '10000000-0000-0000-0000-000000000010'
$candidate = '0.54.0-rc.local'
$key = 'release-evidence-smoke-' + [ Guid ]:: NewGuid (). ToString ( 'N' )
$idempotencyKeys = @ ( $key )
$requestID = 'release-evidence-smoke-' + [ Guid ]:: NewGuid (). ToString ( 'N' )
$temporaryRoot = Join-Path ([ System.IO.Path ]:: GetTempPath ()) ( 'voice-release-evidence-' + [ Guid ]:: NewGuid (). ToString ( 'N' ))
$snapshotPath = Join-Path $temporaryRoot 'acceptance.json'
$indexPath = Join-Path $temporaryRoot 'evidence-index.json'
$bundlePath = Join-Path $temporaryRoot 'candidate-evidence.zip'
$expandedPath = Join-Path $temporaryRoot 'expanded'
$weekdayOffset = (([ int ]( Get-Date ). DayOfWeek ) + 6 ) % 7
$completedWeekStart = ( Get-Date ). Date . AddDays (- $weekdayOffset - 7 )
function Write-JsonUtf8([string]$path , $value , [ int ] $depth = 20 ) {
$json = $value | ConvertTo-Json -Depth $depth
[ System.IO.File ]:: WriteAllText ( $path , $json , [ System.Text.UTF8Encoding ]:: new ( $false ))
}
try {
New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
$token = ( Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @ {
grant_type = 'password' ; client_id = 'voice-platform-web' ; username = 'owner' ; password = 'LocalOnly123!' ; scope = 'openid'
}). access_token
$buildDigest = ( docker image inspect data-terminal -cloud-api - -format '{{.Id}}' ). Trim ()
if ( $buildDigest -notmatch '^sha256:[a-f0-9]{64}$' ) { throw '无法取得API候选镜像摘要' }
$body = @ {
candidate_version = $candidate ; build_digest = $buildDigest ; environment = 'LAB' ; run_type = 'PILOT'
scope = '本地PILOT候选证据包纵向验证' ; required_dependencies = @ ( 'oidc_jwks' )
reason = '验证技术验收快照与阶段8外部证据索引可形成带摘要的候选交付包'
} | ConvertTo-Json -Compress
$response = Invoke-RestMethod -Method Post -Uri " $api /api/v1/ops/diagnostics/acceptance-snapshots" -Headers @ {
Authorization = "Bearer $token " ; 'X-Tenant-Id' = $tenant ; 'Idempotency-Key' = $key ; 'X-Request-Id' = $requestID
} -ContentType 'application/json' -Body $body
if ( -not $response . data . overall_gate_satisfied ) { throw '当前技术验收快照门禁未通过,不能生成候选证据包' }
Write-JsonUtf8 $snapshotPath $response . data
$dailyPaths = @ ()
for ( $day = 0 ; $day -lt 7 ; $day ++) {
$localDate = $completedWeekStart . AddDays ( $day ). ToString ( 'yyyy-MM-dd' )
$dailyKey = 'release-evidence-daily-' + [ Guid ]:: NewGuid (). ToString ( 'N' )
$idempotencyKeys += $dailyKey
$dailyBody = @ { candidate_version = $candidate ; build_digest = $buildDigest ; local_date = $localDate ; timezone = 'Asia/Shanghai' ; reason = '生成已结束试点自然周的候选证据包日报' } | ConvertTo-Json -Compress
$dailyResponse = Invoke-RestMethod -Method Post -Uri " $api /api/v1/ops/diagnostics/pilot-daily-snapshots" -Headers @ {
Authorization = "Bearer $token " ; 'X-Tenant-Id' = $tenant ; 'Idempotency-Key' = $dailyKey ; 'X-Request-Id' = $requestID
} -ContentType 'application/json' -Body $dailyBody
$dailyPath = Join-Path $temporaryRoot "pilot-daily- $localDate .json"
Write-JsonUtf8 $dailyPath $dailyResponse . data
$dailyPaths += $dailyPath
}
$weeklyKey = 'release-evidence-weekly-' + [ Guid ]:: NewGuid (). ToString ( 'N' )
$idempotencyKeys += $weeklyKey
$weekStartText = $completedWeekStart . ToString ( 'yyyy-MM-dd' )
$weeklyBody = @ { candidate_version = $candidate ; build_digest = $buildDigest ; week_start = $weekStartText ; timezone = 'Asia/Shanghai' ; reason = '生成已结束试点自然周的候选证据包周报' } | ConvertTo-Json -Compress
$weeklyResponse = Invoke-RestMethod -Method Post -Uri " $api /api/v1/ops/diagnostics/pilot-weekly-snapshots" -Headers @ {
Authorization = "Bearer $token " ; 'X-Tenant-Id' = $tenant ; 'Idempotency-Key' = $weeklyKey ; 'X-Request-Id' = $requestID
} -ContentType 'application/json' -Body $weeklyBody
if ( -not $weeklyResponse . data . is_complete ) { throw '已结束自然周不应生成周中快照' }
$weeklyPath = Join-Path $temporaryRoot "pilot-weekly- $weekStartText .json"
Write-JsonUtf8 $weeklyPath $weeklyResponse . data
$codes = @ ( 'REMOTE_CI_SUPPLY_CHAIN' , 'REAL_DEVICE_RTC' , 'REAL_DEVICE_FIRMWARE' , 'REAL_AGENT_MODELS' , 'PRODUCTION_INFRASTRUCTURE' , 'PRODUCTION_RECOVERY' , 'KMS_ROTATION' , 'REAL_ALERT_RECEIVER' , 'SAFARI_COMPATIBILITY' , 'BUSINESS_APPROVAL' )
$index = @ {
schema_version = 'release-evidence-index-v2' ; candidate_version = $candidate ; build_digest = $buildDigest
items = @ ( $codes | ForEach-Object { @ { code = $_ ; status = 'PENDING' ; owner = '待外部责任人确认' ; note = '本地证据包保留此外部验收缺口,不以模拟结果代替' ; files = @ ()} })
}
Write-JsonUtf8 $indexPath $index 8
$bundleArguments = @ ( 'run' , './cmd/release-evidence' , '-root' , '.' , '-candidate-version' , $candidate , '-build-digest' , $buildDigest , '-source-commit' , 'local-uncommitted' , '-acceptance-snapshot' , $snapshotPath , '-evidence-index' , $indexPath , '-pilot-weekly-snapshot' , $weeklyPath , '-out' , $bundlePath )
foreach ( $dailyPath in $dailyPaths ) { $bundleArguments += @ ( '-pilot-daily-snapshot' , $dailyPath ) }
$resultLine = & go @bundleArguments
$result = $resultLine | ConvertFrom-Json
if ( $result . status -ne 'EXTERNAL_VALIDATION_PENDING' -or $result . pending -ne 10 -or $result . verified -ne 0 -or $result . sha256 -notmatch '^sha256:[a-f0-9]{64}$' ) { throw '证据包生成结果不正确' }
Expand-Archive -LiteralPath $bundlePath -DestinationPath $expandedPath
$manifest = Get-Content -Raw -LiteralPath ( Join-Path $expandedPath 'release-manifest.json' ) | ConvertFrom-Json
2026-08-26 23:30:59 +08:00
if ( -not $manifest . technical_gate_satisfied -or $manifest . evidence_status -ne 'EXTERNAL_VALIDATION_PENDING' -or $manifest . pending_count -ne 10 -or $manifest . openapi_version -ne '0.68.0' -or $manifest . schema_version -ne 'release-candidate-evidence-bundle-v3' -or -not $manifest . pilot_evidence_included -or $manifest . pilot_daily_count -ne 7 -or $manifest . pilot_week_start -ne $weekStartText -or $manifest . pilot_timezone -ne 'Asia/Shanghai' ) { throw '证据包清单内容不正确' }
2026-08-26 06:41:07 +08:00
if ( $manifest . firmware_validation . status -ne 'PENDING' -or $manifest . firmware_validation . coverage_complete -or $manifest . firmware_validation . required_device_count -ne 3 -or @ ( $manifest . firmware_validation . required_scenarios ). Count -ne 4 -or @ ( $manifest . firmware_validation . gaps ). Count -ne 5 ) { throw '真机固件场景缺口未在证据包中保留' }
if ( $manifest . alert_receiver_validation . status -ne 'PENDING' -or $manifest . alert_receiver_validation . coverage_complete -or $manifest . alert_receiver_validation . report_present -or @ ( $manifest . alert_receiver_validation . gaps ). Count -ne 2 ) { throw '真实告警接收器与正式演练缺口未在证据包中保留' }
if ( $manifest . kms_rotation_validation . status -ne 'PENDING' -or $manifest . kms_rotation_validation . coverage_complete -or $manifest . kms_rotation_validation . report_present -or @ ( $manifest . kms_rotation_validation . gaps ). Count -ne 3 ) { throw '真实KMS/HSM两类轮换缺口未在证据包中保留' }
if ( $manifest . production_recovery_validation . status -ne 'PENDING' -or $manifest . production_recovery_validation . coverage_complete -or $manifest . production_recovery_validation . report_present -or @ ( $manifest . production_recovery_validation . gaps ). Count -ne 5 ) { throw '生产等价四类恢复演练缺口未在证据包中保留' }
if ( $manifest . production_infrastructure_validation . status -ne 'PENDING' -or $manifest . production_infrastructure_validation . coverage_complete -or $manifest . production_infrastructure_validation . report_present -or @ ( $manifest . production_infrastructure_validation . gaps ). Count -ne 8 ) { throw '生产基础设施七类组件缺口未在证据包中保留' }
if ( $manifest . safari_compatibility_validation . status -ne 'PENDING' -or $manifest . safari_compatibility_validation . coverage_complete -or $manifest . safari_compatibility_validation . report_present -or @ ( $manifest . safari_compatibility_validation . gaps ). Count -ne 3 ) { throw '最近两个真实Safari版本缺口未在证据包中保留' }
if ( $manifest . real_device_rtc_validation . status -ne 'PENDING' -or $manifest . real_device_rtc_validation . coverage_complete -or $manifest . real_device_rtc_validation . report_present -or @ ( $manifest . real_device_rtc_validation . gaps ). Count -ne 10 ) { throw '真实终端、三模式和四类RTC场景缺口未在证据包中保留' }
if ( $manifest . real_agent_models_validation . status -ne 'PENDING' -or $manifest . real_agent_models_validation . coverage_complete -or $manifest . real_agent_models_validation . report_present -or @ ( $manifest . real_agent_models_validation . gaps ). Count -ne 16 ) { throw '真实生成、Embedding和内容安全质量缺口未在证据包中保留' }
if ( $manifest . remote_ci_supply_chain_validation . status -ne 'PENDING' -or $manifest . remote_ci_supply_chain_validation . coverage_complete -or $manifest . remote_ci_supply_chain_validation . report_present -or @ ( $manifest . remote_ci_supply_chain_validation . gaps ). Count -ne 20 ) { throw '远程CI、镜像摘要和SBOM缺口未在证据包中保留' }
foreach ( $required in @ ( 'evidence-index.json' , 'acceptance/technical-acceptance-snapshot.json' , 'baseline/openapi/openapi.yaml' , 'baseline/docs/external-acceptance-checklist.md' , "pilot/weekly/ $weekStartText .json" ,( "pilot/daily/" + $completedWeekStart . AddDays ( 6 ). ToString ( 'yyyy-MM-dd' ) + '.json' ))) {
if ( -not ( Test-Path -LiteralPath ( Join-Path $expandedPath $required ))) { throw "证据包缺少文件: $required " }
}
Write-Host "发布候选证据包烟测通过:文件= $( $result . files ) , PILOT日报=7,周报=$weekStartText ,外部待验= $( $result . pending ) ,远程CI、真机RTC/固件、Agent模型、告警、KMS、恢复、基础设施和Safari保持PENDING,包摘要= $( $result . sha256 ) 。"
} finally {
$quotedKeys = ( $idempotencyKeys | ForEach-Object { "' $_ '" }) -join ','
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP = 1 -c "BEGIN; DELETE FROM audit_event WHERE request_id=' $requestID '; DELETE FROM idempotency_record WHERE idempotency_key IN ( $quotedKeys ); COMMIT;" | Out-Null
if ( Test-Path -LiteralPath $temporaryRoot ) { Remove-Item -LiteralPath $temporaryRoot -Recurse -Force }
Write-Host '已清理证据包烟测的临时文件、审计和幂等记录。'
}