$ErrorActionPreference = 'Stop' $base = 'http://s101.local:18080/' $outPath = Join-Path $PSScriptRoot 'M2-C3c正式互加好友实测-final-2026-09-30.json' $journal = [Collections.Generic.List[object]]::new() $sessions = @{}; $profiles = @{}; $before = @{}; $after = @{} $report = [ordered]@{ time=(Get-Date).ToString('o'); api=$base; accounts=@('101','102'); requests=$journal; kvInputs=@(); before=$before; after=$after; error=$null; mutual=$false } function Save-Evidence { $report | ConvertTo-Json -Depth 16 | Set-Content -Encoding utf8 $outPath } function Send-Original([string]$id, [string]$route, $body, [bool]$mutation=$false) { $row = [ordered]@{ account=$id; route=$route; mutation=$mutation; http=$null; code=$null; error=$null } $journal.Add($row); Save-Evidence try { $json = ConvertTo-Json -InputObject $body -Depth 10 -Compress $reply = Invoke-WebRequest -Uri ($base+$route) -Method Post -ContentType 'application/json' -Body $json -WebSession $sessions[$id] -MaximumRedirection 0 -TimeoutSec 20 -SkipHttpErrorCheck $row.http = [int]$reply.StatusCode if ($row.http -ne 200) { throw "HttpFailure:$route" } $parsed = $reply.Content | ConvertFrom-Json if ($null -ne $parsed.code) { $row.code=$parsed.code } Save-Evidence return $parsed } catch { $row.error=$_.Exception.GetType().Name; Save-Evidence; throw } } function Describe-Friends($data) { if ($null -eq $data.code -or $data.code -ne 1 -or $null -eq $data.friends) { throw 'FriendListNotSuccessful' } $items = @(foreach ($raw in @($data.friends)) { $item = if ($raw -is [string]) { $raw | ConvertFrom-Json } else { $raw } $idValue = $null foreach ($key in @('PlayFabId','FriendPlayFabId','PlayerId','Id')) { if ($null -ne $item.$key) { $idValue=[string]$item.$key; break } } if ([string]::IsNullOrEmpty($idValue)) { throw 'UnrecognizedFriendIdentity' } $fields = @(foreach ($p in $item.PSObject.Properties) { $entry = [ordered]@{ name=$p.Name; type=$(if ($null -eq $p.Value) {'null'} else {$p.Value.GetType().Name}) } if ($p.Name -match '^(Level|LevelB|Lv|StatValue|InfiniteBuildingLevel|LastLogin|LastLoginTime|LastLoginTimeUtc)$') { $entry.value=$p.Value } if ($p.Value -is [pscustomobject]) { $entry.childFields=@($p.Value.PSObject.Properties.Name) } $entry }) [ordered]@{ id=$(if ($idValue -in @('101','102')) {$idValue} else {'redacted-other'}); encodedAsString=($raw -is [string]); fields=$fields } }) return [ordered]@{ code=$data.code; count=@($data.friends).Count; items=$items } } function Has-Friend($snapshot, [string]$id) { return @($snapshot.items | Where-Object {$_.id -eq $id}).Count -gt 0 } try { foreach ($id in @('101','102')) { $sessions[$id] = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $auth = Send-Original $id 'account/Login' @{playerId=$id} if ($null -eq $auth.state -or $auth.state -ne 0 -or $auth.newCreate -ne $false) { throw 'ExistingAccountLoginNotConfirmed' } $kv = Send-Original $id 'playerdata/Get' @{keys=@('Lv','NewPlayerDataDic','DisplayName','AvatarUrl','InfiniteBuildingData','LastLoginTimeUtc')} if ($null -eq $kv.data) { throw 'MissingKVData' } $source='Lv'; $levelText=[string]$kv.data.Lv if ($null -ne $kv.data.NewPlayerDataDic) { $modern=$kv.data.NewPlayerDataDic | ConvertFrom-Json if ($null -ne $modern.newData) { $parts=([string]$modern.newData).Split(',') if ($parts.Length -ne 19) { throw 'InvalidModernCoreLength' } $levelText=$parts[0]; $source='NewPlayerDataDic.newData[0]' } } $level=0 if (![int]::TryParse($levelText,[ref]$level) -or $level -lt 1) { throw 'MissingActualKVLevel' } if ($null -ne $kv.data.Lv -and [int]$kv.data.Lv -ne $level) { throw 'ConflictingKVLevel' } $name=[string]$kv.data.DisplayName if ([string]::IsNullOrEmpty($name)) { $name='Angler#'+$id } $profiles[$id]=@{PlayFabId=$id;DisplayName=$name;AvatarUrl=[string]$kv.data.AvatarUrl;Level=$level} $report.kvInputs += @{ account=$id; level=$level; levelSource=$source; displayNamePresent=![string]::IsNullOrEmpty([string]$kv.data.DisplayName); avatarPresent=![string]::IsNullOrEmpty([string]$kv.data.AvatarUrl); buildingPresent=($null -ne $kv.data.InfiniteBuildingData); lastLoginTimeUtc=$kv.data.LastLoginTimeUtc } $before[$id]=Describe-Friends (Send-Original $id 'friend/GetFriendsList' @{}) } if (!((Has-Friend $before['101'] '102') -and (Has-Friend $before['102'] '101'))) { $add=Send-Original '101' 'friend/AddFriend' @{MySelf=$profiles['101'];MyFriendCount=$before['101'].count;FriendPlayFabId='102'} $true if ($null -eq $add.code -or $add.code -notin @(1,2)) { throw 'AddFriendBusinessFailure' } $forward=Describe-Friends (Send-Original '101' 'friend/GetFriendsList' @{}) $reverse=Describe-Friends (Send-Original '102' 'friend/GetFriendsList' @{}) if (!((Has-Friend $forward '102') -and (Has-Friend $reverse '101'))) { $accept=Send-Original '102' 'friend/AcceptFriend' @{Status=1;PlayFabId='102';FriendPlayFabId='101'} $true if ($null -eq $accept.code -or $accept.code -ne 1) { throw 'AcceptFriendBusinessFailure' } } } } catch { $report.error=$_.Exception.Message } finally { foreach ($id in @('101','102')) { if ($before.ContainsKey($id)) { try { $after[$id]=Describe-Friends (Send-Original $id 'friend/GetFriendsList' @{}) } catch { if (!$report.error) { $report.error=$_.Exception.GetType().Name } } } } $report.mutual=($after.ContainsKey('101') -and $after.ContainsKey('102') -and (Has-Friend $after['101'] '102') -and (Has-Friend $after['102'] '101')) Save-Evidence $sessions.Clear(); $profiles.Clear(); $auth=$null; $kv=$null } $report | ConvertTo-Json -Depth 16