Files

146 lines
9.5 KiB
PowerShell
Raw Permalink Normal View History

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'
$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 '已恢复合成授权并清理通话、快照、任务、审计和幂等记录。'
}