146 lines
9.5 KiB
PowerShell
146 lines
9.5 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'
|
|||
|
|
$deviceId = '10000000-0000-0000-0000-000000000511'
|
|||
|
|
$subjectId = '10000000-0000-0000-0000-000000000411'
|
|||
|
|
$subjectInternalId = '10000000-0000-0000-0000-000000000401'
|
|||
|
|
$consentId = '10000000-0000-0000-0000-000000000812'
|
|||
|
|
$deviceSecret = [Text.Encoding]::UTF8.GetBytes('local-simulated-device-secret-32-bytes-minimum')
|
|||
|
|
$conversationId = $null
|
|||
|
|
$withdrawalSnapshotId = $null
|
|||
|
|
$originalConsentVersion = $null
|
|||
|
|
$withdrawRequestId = 'consent-stop-smoke-' + [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 SignedPost([string]$Path, [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$Path`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()
|
|||
|
|
}
|
|||
|
|
$headers = @{
|
|||
|
|
'X-Device-Id' = $deviceId; 'X-Device-Timestamp' = $timestamp; 'X-Device-Nonce' = $nonce
|
|||
|
|
'X-Device-Content-SHA256' = $digest; 'X-Device-Signature' = $signature
|
|||
|
|
}
|
|||
|
|
if ($Key) { $headers['Idempotency-Key'] = $Key }
|
|||
|
|
return Invoke-WebRequest -UseBasicParsing -Method Post -Uri ($api + $Path) -ContentType 'application/json' -Body $Body -Headers $headers
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 '测试设备存在未结束通话。'
|
|||
|
|
}
|
|||
|
|
$originalConsentVersion = Scalar "SELECT version FROM consent_record WHERE public_id='$consentId'::uuid AND withdrawn_at IS NULL"
|
|||
|
|
if (-not $originalConsentVersion) { throw '故事整理授权不是有效状态。' }
|
|||
|
|
|
|||
|
|
$createBody = @{
|
|||
|
|
device_time = [DateTime]::UtcNow.ToString('o'); monotonic_ms = 51001
|
|||
|
|
capabilities = @{ rtc = $true; audio_tracks = @('near', 'far'); upload_resume = $true }
|
|||
|
|
network = @{ type = 'wifi'; rssi = -48 }; requested_goal = 'open_conversation'
|
|||
|
|
} | ConvertTo-Json -Compress -Depth 6
|
|||
|
|
$created = ((SignedPost '/device/v2/conversations' $createBody ('consent-stop-create-' + [Guid]::NewGuid().ToString('N'))).Content | ConvertFrom-Json).data
|
|||
|
|
$conversationId = [string]$created.conversation_id
|
|||
|
|
|
|||
|
|
$activatePath = "/device/v2/conversations/$conversationId/activate"
|
|||
|
|
$activateBody = @{
|
|||
|
|
joined_at = [DateTime]::UtcNow.ToString('o'); room_id = $created.rtc.room_id
|
|||
|
|
rtc_user_id = $created.rtc.user_id; rtc_join_result = 'success'; config_revision = 1
|
|||
|
|
connection_reference = 'consent-withdrawal-smoke'
|
|||
|
|
} | ConvertTo-Json -Compress
|
|||
|
|
SignedPost $activatePath $activateBody ('consent-stop-activate-' + [Guid]::NewGuid().ToString('N')) | Out-Null
|
|||
|
|
|
|||
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(15)
|
|||
|
|
do {
|
|||
|
|
Start-Sleep -Milliseconds 300
|
|||
|
|
$state = Scalar "SELECT status FROM conversation WHERE public_id='$conversationId'::uuid"
|
|||
|
|
} while ($state -ne 'ACTIVE' -and [DateTime]::UtcNow -lt $deadline)
|
|||
|
|
if ($state -ne 'ACTIVE') { throw "通话没有进入ACTIVE:$state" }
|
|||
|
|
|
|||
|
|
$accessToken = Token
|
|||
|
|
$withdraw = Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/subjects/$subjectId/consents/$consentId/withdraw" `
|
|||
|
|
-Headers @{ Authorization = "Bearer $accessToken"; 'X-Request-Id' = $withdrawRequestId }
|
|||
|
|
if ($withdraw.StatusCode -ne 200 -or -not (($withdraw.Content | ConvertFrom-Json).data.withdrawn_at)) { throw '授权撤回接口未成功。' }
|
|||
|
|
|
|||
|
|
$withdrawalSnapshotId = Scalar "SELECT cs.public_id::text FROM consent_snapshot cs WHERE cs.subject_id='$subjectInternalId'::uuid AND cs.id<>(SELECT consent_snapshot_id FROM conversation WHERE public_id='$conversationId'::uuid) ORDER BY cs.created_at DESC,cs.id DESC LIMIT 1"
|
|||
|
|
$stopEvidence = Scalar "SELECT c.stop_reason||'|'||(SELECT count(*) FROM conversation_state_history h WHERE h.conversation_id=c.id AND h.from_status='ACTIVE' AND h.to_status='STOPPING')||'|'||(SELECT count(*) FROM outbox_event o WHERE o.aggregate_id=c.id AND o.event_type='conversation.agent_stop_requested') FROM conversation c WHERE c.public_id='$conversationId'::uuid"
|
|||
|
|
if ($stopEvidence -ne 'consent_withdrawn:story_extraction|1|1') { throw "授权撤回停止意图不完整:$stopEvidence" }
|
|||
|
|
|
|||
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(15)
|
|||
|
|
do {
|
|||
|
|
Start-Sleep -Milliseconds 300
|
|||
|
|
$finalState = Scalar "SELECT c.status||'|'||rt.status||'|'||rt.stop_attempts FROM conversation c JOIN rtc_task rt ON rt.conversation_id=c.id WHERE c.public_id='$conversationId'::uuid"
|
|||
|
|
} while ($finalState -ne 'COMPLETED|COMPLETED|1' -and [DateTime]::UtcNow -lt $deadline)
|
|||
|
|
if ($finalState -ne 'COMPLETED|COMPLETED|1') { throw "Agent没有完成可靠停止:$finalState" }
|
|||
|
|
|
|||
|
|
$heartbeatBody = @{
|
|||
|
|
sequence_no = 3; device_time = [DateTime]::UtcNow.ToString('o'); monotonic_ms = 52001
|
|||
|
|
conversation_status = 'ACTIVE'; rtc = @{ joined = $true; uplink = $true; downlink = $true }
|
|||
|
|
network = @{ type = 'wifi'; rssi = -50 }; firmware_version = '0.1.0-sim'; reported_config_revision = 1
|
|||
|
|
recording_queue = @{ pending_segments = 0; failed_segments = 0 }; command_results = @()
|
|||
|
|
rtc_credential_expires_at = [string]$created.rtc.expires_at
|
|||
|
|
} | ConvertTo-Json -Compress -Depth 6
|
|||
|
|
$heartbeat = ((SignedPost "/device/v2/conversations/$conversationId/heartbeat" $heartbeatBody '').Content | ConvertFrom-Json).data
|
|||
|
|
if ($heartbeat.actions[0].type -ne 'stop') { throw '设备心跳没有收到停止动作。' }
|
|||
|
|
|
|||
|
|
$detail = (Invoke-RestMethod -Uri "$api/api/v1/conversations/$conversationId" -Headers @{ Authorization = "Bearer $accessToken" }).data
|
|||
|
|
if ($detail.status -ne 'COMPLETED' -or $detail.stop_reason -ne 'consent_withdrawn:story_extraction') { throw '家庭通话详情没有展示授权撤回后的权威终态。' }
|
|||
|
|
|
|||
|
|
Write-Host '授权撤回停止烟测通过:SaaS事务意图、Agent可靠停止、设备stop动作和家庭通话终态一致。'
|
|||
|
|
} finally {
|
|||
|
|
if ($originalConsentVersion) {
|
|||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c `
|
|||
|
|
"UPDATE consent_record SET withdrawn_at=NULL,version=$originalConsentVersion,updated_at=now() WHERE public_id='$consentId'::uuid;" | Out-Null
|
|||
|
|
}
|
|||
|
|
if ($conversationId -match '^[0-9a-fA-F-]{36}$') {
|
|||
|
|
$cleanup = @"
|
|||
|
|
BEGIN;
|
|||
|
|
CREATE TEMP TABLE consent_stop_cleanup AS
|
|||
|
|
SELECT id,public_id,device_id,context_snapshot_id,consent_snapshot_id FROM conversation WHERE public_id='$conversationId'::uuid;
|
|||
|
|
DELETE FROM web_event WHERE resource_public_id IN(SELECT public_id FROM consent_stop_cleanup)
|
|||
|
|
OR resource_public_id IN(SELECT public_id FROM device WHERE id IN(SELECT device_id FROM consent_stop_cleanup));
|
|||
|
|
DELETE FROM conversation_summary WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM conversation_turn WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM provider_event WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM conversation_state_history WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM rtc_task WHERE conversation_id IN(SELECT id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM outbox_event WHERE aggregate_id IN(SELECT id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM audit_event WHERE request_id='$withdrawRequestId' OR resource_id IN(SELECT id FROM consent_stop_cleanup) OR resource_id IN(SELECT public_id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM idempotency_record WHERE resource_id IN(SELECT id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM conversation WHERE id IN(SELECT id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM context_snapshot WHERE id IN(SELECT context_snapshot_id FROM consent_stop_cleanup);
|
|||
|
|
DELETE FROM consent_snapshot WHERE id IN(SELECT consent_snapshot_id FROM consent_stop_cleanup);
|
|||
|
|
COMMIT;
|
|||
|
|
"@
|
|||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c $cleanup | Out-Null
|
|||
|
|
docker compose exec -T redis redis-cli DEL "voice-platform:conversation:lease:$conversationId" "voice-platform:device:presence:$deviceId" | Out-Null
|
|||
|
|
}
|
|||
|
|
if ($withdrawalSnapshotId -match '^[0-9a-fA-F-]{36}$') {
|
|||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c `
|
|||
|
|
"DELETE FROM consent_snapshot WHERE public_id='$withdrawalSnapshotId'::uuid AND NOT EXISTS(SELECT 1 FROM conversation WHERE consent_snapshot_id=consent_snapshot.id);" | Out-Null
|
|||
|
|
}
|
|||
|
|
Write-Host '已恢复合成授权并清理通话、快照、任务、审计和幂等记录。'
|
|||
|
|
}
|