$ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true $projectRoot = Split-Path -Parent $PSScriptRoot Set-Location -LiteralPath $projectRoot $apiBase = 'http://localhost:58080' $tokenUrl = 'http://localhost:58180/realms/voice-local/protocol/openid-connect/token' $tenantId = '10000000-0000-0000-0000-000000000010' $householdId = $null function Get-LocalAccessToken([string]$Username) { $response = Invoke-RestMethod -Method Post -Uri $tokenUrl ` -ContentType 'application/x-www-form-urlencoded' ` -Body @{ grant_type = 'password' client_id = 'voice-platform-web' username = $Username password = 'LocalOnly123!' scope = 'openid' } return $response.access_token } function Invoke-JSONRequest( [string]$Method, [string]$Uri, [string]$Token, [hashtable]$Headers = @{}, [object]$Body = $null ) { $requestHeaders = @{ Authorization = "Bearer $Token" } foreach ($key in $Headers.Keys) { $requestHeaders[$key] = $Headers[$key] } $arguments = @{ Method = $Method Uri = $Uri Headers = $requestHeaders } if ($null -ne $Body) { $arguments.ContentType = 'application/json; charset=utf-8' $arguments.Body = $Body | ConvertTo-Json -Depth 10 -Compress } return Invoke-WebRequest @arguments } function Assert-Status([scriptblock]$Operation, [int]$ExpectedStatus) { try { & $Operation | Out-Null throw "请求不应成功,预期HTTP $ExpectedStatus。" } catch { if ($null -eq $_.Exception.Response -or [int]$_.Exception.Response.StatusCode -ne $ExpectedStatus) { throw } } } function Convert-Response($Response) { return $Response.Content | ConvertFrom-Json } try { $ownerToken = Get-LocalAccessToken -Username 'owner' $editorToken = Get-LocalAccessToken -Username 'editor' $outsiderToken = Get-LocalAccessToken -Username 'outsider' $createdResponse = Invoke-JSONRequest -Method Post -Uri "$apiBase/api/v1/households" ` -Token $ownerToken -Headers @{ 'X-Tenant-Id' = $tenantId } -Body @{ name = 'SaaS纵向烟测家庭' } if ([int]$createdResponse.StatusCode -ne 201) { throw '创建家庭未返回201。' } $created = Convert-Response $createdResponse $householdId = $created.data.id $householdETag = [string]$createdResponse.Headers.ETag if ([string]::IsNullOrWhiteSpace($householdETag)) { throw '创建家庭未返回ETag。' } Assert-Status { Invoke-JSONRequest -Method Get -Uri "$apiBase/api/v1/households/$householdId" -Token $outsiderToken } 403 Assert-Status { Invoke-JSONRequest -Method Patch -Uri "$apiBase/api/v1/households/$householdId" -Token $editorToken -Headers @{ 'If-Match' = $householdETag } -Body @{ name = '禁止修改' } } 403 docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c ` "INSERT INTO household_member (tenant_id,household_id,user_id,role,status) SELECT h.tenant_id,h.id,ua.id,'editor','active' FROM household h JOIN user_account ua ON ua.tenant_id=h.tenant_id WHERE h.public_id='$householdId'::uuid AND ua.oidc_subject='00000000-0000-0000-0000-000000000102';" | Out-Null $patchedResponse = Invoke-JSONRequest -Method Patch -Uri "$apiBase/api/v1/households/$householdId" ` -Token $ownerToken -Headers @{ 'If-Match' = $householdETag } -Body @{ name = 'SaaS纵向烟测家庭-已修改' } if ([int]$patchedResponse.StatusCode -ne 200) { throw '修改家庭未返回200。' } Assert-Status { Invoke-JSONRequest -Method Patch -Uri "$apiBase/api/v1/households/$householdId" -Token $ownerToken -Headers @{ 'If-Match' = $householdETag } -Body @{ name = '过期写入' } } 412 $subjectResponse = Invoke-JSONRequest -Method Post -Uri "$apiBase/api/v1/households/$householdId/subjects" ` -Token $editorToken -Body @{ preferred_name = '烟测长者' language = 'zh-CN' timezone = 'Asia/Shanghai' profile_data = @{ synthetic = $true } conversation_preferences = @{ pace = 'slow' } } if ([int]$subjectResponse.StatusCode -ne 201) { throw '创建长者未返回201。' } $subject = Convert-Response $subjectResponse $subjectId = $subject.data.id $subjectETag = [string]$subjectResponse.Headers.ETag Assert-Status { Invoke-JSONRequest -Method Get -Uri "$apiBase/api/v1/subjects/$subjectId" -Token $outsiderToken } 403 $subjectPatched = Invoke-JSONRequest -Method Patch -Uri "$apiBase/api/v1/subjects/$subjectId" ` -Token $editorToken -Headers @{ 'If-Match' = $subjectETag } -Body @{ preferred_name = '烟测长者-已修改' } if ([int]$subjectPatched.StatusCode -ne 200) { throw '修改长者未返回200。' } $factResponse = Invoke-JSONRequest -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/facts" -Token $editorToken -Body @{ fact_type = 'hometown' content = '合成城市' source_type = 'model_candidate' source_ref = 'synthetic://conversation/turn/1' confirmation_status = 'pending' sensitivity = 'normal' } $fact = Convert-Response $factResponse $factId = $fact.data.id $factETag = [string]$factResponse.Headers.ETag $confirmedFact = Invoke-JSONRequest -Method Patch -Uri "$apiBase/api/v1/subjects/$subjectId/facts/$factId" ` -Token $editorToken -Headers @{ 'If-Match' = $factETag } -Body @{ confirmation_status = 'confirmed' } if ([int]$confirmedFact.StatusCode -ne 200) { throw '确认稳定事实未返回200。' } Assert-Status { Invoke-JSONRequest -Method Patch -Uri "$apiBase/api/v1/subjects/$subjectId/facts/$factId" -Token $editorToken -Headers @{ 'If-Match' = $factETag } -Body @{ content = '过期写入' } } 412 $consentResponse = Invoke-JSONRequest -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/consents" ` -Token $editorToken -Body @{ type = 'story_extraction' scope = @{ scope = 'future_conversations' } policy_version = 'local-v1' notice_digest = ('c' * 64) evidence_ref = 'synthetic://http-smoke' } if ([int]$consentResponse.StatusCode -ne 201) { throw '授予授权未返回201。' } $consent = Convert-Response $consentResponse $consentId = $consent.data.id Assert-Status { Invoke-JSONRequest -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/consents" -Token $editorToken -Body @{ type = 'story_extraction'; scope = @{}; policy_version = 'local-v1'; notice_digest = ('d' * 64) } } 409 $withdrawn = Convert-Response (Invoke-JSONRequest -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/consents/$consentId/withdraw" -Token $editorToken) $withdrawnReplay = Convert-Response (Invoke-JSONRequest -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/consents/$consentId/withdraw" -Token $editorToken) if ($null -eq $withdrawn.data.withdrawn_at -or $withdrawn.data.version -ne $withdrawnReplay.data.version) { throw '授权撤回或重复撤回结果不正确。' } $snapshotCount = docker compose exec -T postgres psql -U voice -d voice_platform -Atc ` "SELECT count(*) FROM consent_snapshot WHERE subject_id=(SELECT id FROM subject_profile WHERE public_id='$subjectId'::uuid);" if ([int]$snapshotCount -ne 2) { throw "授权快照数量是$snapshotCount,预期为2。" } Write-Host "SaaS纵向烟测通过:家庭 $householdId,长者 $subjectId,授权 $consentId。" } finally { if (-not [string]::IsNullOrWhiteSpace($householdId)) { docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c ` "BEGIN; DELETE FROM audit_event WHERE resource_id='$householdId'::uuid OR resource_id IN (SELECT s.public_id FROM subject_profile s JOIN household h ON h.id=s.household_id WHERE h.public_id='$householdId'::uuid UNION SELECT c.public_id FROM consent_record c JOIN subject_profile s ON s.id=c.subject_id JOIN household h ON h.id=s.household_id WHERE h.public_id='$householdId'::uuid UNION SELECT f.public_id FROM subject_fact f JOIN subject_profile s ON s.id=f.subject_id JOIN household h ON h.id=s.household_id WHERE h.public_id='$householdId'::uuid); DELETE FROM consent_snapshot WHERE subject_id IN (SELECT id FROM subject_profile WHERE household_id=(SELECT id FROM household WHERE public_id='$householdId'::uuid)); DELETE FROM consent_record WHERE subject_id IN (SELECT id FROM subject_profile WHERE household_id=(SELECT id FROM household WHERE public_id='$householdId'::uuid)); DELETE FROM subject_fact WHERE subject_id IN (SELECT id FROM subject_profile WHERE household_id=(SELECT id FROM household WHERE public_id='$householdId'::uuid)); DELETE FROM subject_profile WHERE household_id=(SELECT id FROM household WHERE public_id='$householdId'::uuid); DELETE FROM household_member WHERE household_id=(SELECT id FROM household WHERE public_id='$householdId'::uuid); DELETE FROM household WHERE public_id='$householdId'::uuid; COMMIT;" | Out-Null } }