112 lines
7.0 KiB
PowerShell
112 lines
7.0 KiB
PowerShell
$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'
|
|||
|
|
$deviceId = '10000000-0000-0000-0000-000000000511'
|
|||
|
|
$subjectInternalId = '10000000-0000-0000-0000-000000000401'
|
|||
|
|
$deviceSecret = [Text.Encoding]::UTF8.GetBytes('local-simulated-device-secret-32-bytes-minimum')
|
|||
|
|
$conversationId = $null
|
|||
|
|
$originalProfileBase64 = $null
|
|||
|
|
$idempotencyKey = 'context-preparation-failure-' + [Guid]::NewGuid().ToString('N')
|
|||
|
|
|
|||
|
|
function Scalar([string]$Sql) {
|
|||
|
|
$value = docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -t -A -c $Sql
|
|||
|
|
if ($LASTEXITCODE -ne 0) { throw '数据库查询失败。' }
|
|||
|
|
return ($value | Out-String).Trim()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function Token() {
|
|||
|
|
return (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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function SignedCreate([string]$Body, [string]$Key) {
|
|||
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Body)
|
|||
|
|
$digest = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant()
|
|||
|
|
$timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|||
|
|
$nonce = [Guid]::NewGuid().ToString('N')
|
|||
|
|
$canonical = "POST`n/device/v2/conversations`n$timestamp`n$nonce`n$digest"
|
|||
|
|
$hmac = [Security.Cryptography.HMACSHA256]::new($deviceSecret)
|
|||
|
|
try {
|
|||
|
|
$signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical))).TrimEnd('=').Replace('+', '-').Replace('/', '_')
|
|||
|
|
} finally {
|
|||
|
|
$hmac.Dispose()
|
|||
|
|
}
|
|||
|
|
return Invoke-WebRequest -UseBasicParsing -SkipHttpErrorCheck -Method Post -Uri "$api/device/v2/conversations" `
|
|||
|
|
-ContentType 'application/json' -Body $Body -Headers @{
|
|||
|
|
'X-Device-Id' = $deviceId; 'X-Device-Timestamp' = $timestamp; 'X-Device-Nonce' = $nonce
|
|||
|
|
'X-Device-Content-SHA256' = $digest; 'X-Device-Signature' = $signature; 'Idempotency-Key' = $Key
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if ((Scalar "SELECT count(*) FROM conversation c JOIN device d ON d.id=c.device_id WHERE d.public_id='$deviceId'::uuid AND c.status NOT IN('COMPLETED','FAILED','EXPIRED')") -ne '0') {
|
|||
|
|
throw '测试设备存在未结束通话,拒绝改动背景资料。'
|
|||
|
|
}
|
|||
|
|
$originalProfileBase64 = Scalar "SELECT encode(convert_to(profile_data::text,'UTF8'),'base64') FROM subject_profile WHERE id='$subjectInternalId'::uuid"
|
|||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c `
|
|||
|
|
"UPDATE subject_profile SET profile_data=jsonb_set(profile_data,'{background}','42'::jsonb,true) WHERE id='$subjectInternalId'::uuid;" | Out-Null
|
|||
|
|
|
|||
|
|
$body = @{
|
|||
|
|
device_time = [DateTime]::UtcNow.ToString('o'); monotonic_ms = 42001
|
|||
|
|
capabilities = @{ rtc = $true; audio_tracks = @('near', 'far'); upload_resume = $true }
|
|||
|
|
network = @{ type = 'wifi'; rssi = -50 }; requested_goal = 'open_conversation'
|
|||
|
|
} | ConvertTo-Json -Compress -Depth 6
|
|||
|
|
|
|||
|
|
$first = SignedCreate $body $idempotencyKey
|
|||
|
|
if ($first.StatusCode -ne 422) { throw "首次请求未返回422:$($first.StatusCode) $($first.Content)" }
|
|||
|
|
$firstError = ($first.Content | ConvertFrom-Json).error
|
|||
|
|
$conversationId = [string]$firstError.details.conversation_id
|
|||
|
|
if ($firstError.code -ne 'AGENT_CONTEXT_PREPARATION_FAILED' -or $firstError.details.status -ne 'FAILED' -or $firstError.details.failure_code -ne 'context_preparation_failed') {
|
|||
|
|
throw "设备失败响应不正确:$($first.Content)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$second = SignedCreate $body $idempotencyKey
|
|||
|
|
if ($second.StatusCode -ne 422 -or $second.Headers['Idempotency-Replayed'] -ne 'true') { throw '失败结果没有按同一幂等键重放。' }
|
|||
|
|
$secondError = ($second.Content | ConvertFrom-Json).error
|
|||
|
|
if ($secondError.details.conversation_id -ne $conversationId) { throw '重放返回了不同通话编号。' }
|
|||
|
|
|
|||
|
|
$databaseState = Scalar "SELECT c.status||'|'||c.failure_code||'|'||xs.preparation_status||'|'||xs.preparation_failure_code||'|'||(NOT EXISTS(SELECT 1 FROM rtc_task rt WHERE rt.conversation_id=c.id))::text||'|'||(SELECT count(*) FROM conversation_state_history h WHERE h.conversation_id=c.id) FROM conversation c JOIN context_snapshot xs ON xs.id=c.context_snapshot_id WHERE c.public_id='$conversationId'::uuid"
|
|||
|
|
if ($databaseState -ne 'FAILED|context_preparation_failed|FAILED|context_preparation_failed|true|2') { throw "权威失败状态不正确:$databaseState" }
|
|||
|
|
|
|||
|
|
$accessToken = Token
|
|||
|
|
$detail = (Invoke-RestMethod -Uri "$api/api/v1/conversations/$conversationId" -Headers @{ Authorization = "Bearer $accessToken" }).data
|
|||
|
|
if ($detail.status -ne 'FAILED' -or $detail.failure_code -ne 'context_preparation_failed' -or $detail.snapshots.context_preparation_status -ne 'FAILED' -or $null -ne $detail.rtc) {
|
|||
|
|
throw '家庭通话详情与设备失败结果不一致。'
|
|||
|
|
}
|
|||
|
|
$diagnostic = (Invoke-RestMethod -Uri "$api/api/v1/ops/diagnostics/search?field=conversation_id&value=$conversationId" -Headers @{ Authorization = "Bearer $accessToken"; 'X-Tenant-Id' = $tenant }).data.items
|
|||
|
|
if (@($diagnostic).Count -ne 1 -or $diagnostic[0].conversation_failure_code -ne 'context_preparation_failed' -or $diagnostic[0].provider_task_status) {
|
|||
|
|
throw '运营诊断结果与设备失败结果不一致。'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Write-Host '背景资料准备失败烟测通过:设备422及幂等重放、FAILED权威记录、无RTC房间、家庭详情和运营诊断结果一致。'
|
|||
|
|
} finally {
|
|||
|
|
if ($originalProfileBase64) {
|
|||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c `
|
|||
|
|
"UPDATE subject_profile SET profile_data=convert_from(decode('$originalProfileBase64','base64'),'UTF8')::jsonb WHERE id='$subjectInternalId'::uuid;" | Out-Null
|
|||
|
|
}
|
|||
|
|
if ($conversationId -match '^[0-9a-fA-F-]{36}$') {
|
|||
|
|
$cleanup = @"
|
|||
|
|
BEGIN;
|
|||
|
|
CREATE TEMP TABLE context_failure_cleanup AS
|
|||
|
|
SELECT id,context_snapshot_id,consent_snapshot_id FROM conversation WHERE public_id='$conversationId'::uuid;
|
|||
|
|
DELETE FROM conversation_state_history WHERE conversation_id IN(SELECT id FROM context_failure_cleanup);
|
|||
|
|
DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM context_failure_cleanup);
|
|||
|
|
DELETE FROM idempotency_record WHERE resource_id IN(SELECT id FROM context_failure_cleanup);
|
|||
|
|
DELETE FROM conversation WHERE id IN(SELECT id FROM context_failure_cleanup);
|
|||
|
|
DELETE FROM context_snapshot WHERE id IN(SELECT context_snapshot_id FROM context_failure_cleanup);
|
|||
|
|
DELETE FROM consent_snapshot WHERE id IN(SELECT consent_snapshot_id FROM context_failure_cleanup);
|
|||
|
|
COMMIT;
|
|||
|
|
"@
|
|||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c $cleanup | Out-Null
|
|||
|
|
}
|
|||
|
|
Write-Host '已恢复合成长者背景并清理烟测通话、快照、审计和幂等记录。'
|
|||
|
|
}
|