Test-XiaoshuLegacySyncBridge.ps1 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. [CmdletBinding()]
  2. param(
  3. [Parameter(Mandatory = $true)]
  4. [string]$BaseUrl,
  5. [Parameter(Mandatory = $true)]
  6. [string]$ConfigurationPath
  7. )
  8. $ErrorActionPreference = 'Stop'
  9. Set-StrictMode -Version Latest
  10. if (-not (Test-Path -LiteralPath $ConfigurationPath -PathType Leaf)) { throw "配置文件不存在:$ConfigurationPath" }
  11. $configuration = Get-Content -LiteralPath $ConfigurationPath -Raw | ConvertFrom-Json
  12. $keyId = [string]$configuration.XiaoshuSync.KeyId
  13. $secret = [string]$configuration.XiaoshuSync.Secret
  14. if ([string]::IsNullOrWhiteSpace($keyId) -or $secret.Length -lt 32) { throw '配置文件中的 KeyId 或 Secret 无效。' }
  15. $base = $BaseUrl.TrimEnd('/')
  16. $baseUri = [Uri]$base
  17. if (-not $baseUri.IsAbsoluteUri -or $baseUri.Scheme -notin @('http', 'https') -or $baseUri.Query -or $baseUri.Fragment -or $baseUri.UserInfo -or $baseUri.AbsolutePath.TrimEnd('/') -ne [string]$configuration.XiaoshuSync.PathBase) {
  18. throw 'BaseUrl 必须是与配置 PathBase 一致的 HTTP(S) 应用根地址,不能附加 /v1。'
  19. }
  20. $health = Invoke-RestMethod -Uri "$base/v1/health" -Method Get -TimeoutSec 15
  21. if ($health.status -ne 'ok' -or $health.service -ne 'xiaoshu-legacy-sync' -or $health.version -ne 'v1') { throw '健康地址未返回小树同步桥 v1,请检查域名绑定和 IIS 子应用。' }
  22. $pathBase = $baseUri.AbsolutePath.TrimEnd('/')
  23. function Invoke-SignedRead([string]$RelativePath) {
  24. $pathAndQuery = "$pathBase$RelativePath"
  25. $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds().ToString()
  26. $nonce = [Guid]::NewGuid().ToString('N')
  27. $sha256 = [Security.Cryptography.SHA256]::Create()
  28. try {
  29. $emptyBodyHash = ([BitConverter]::ToString($sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes('')))).Replace('-', '').ToLowerInvariant()
  30. } finally {
  31. $sha256.Dispose()
  32. }
  33. $canonical = "GET`n$pathAndQuery`n$timestamp`n$nonce`n$emptyBodyHash"
  34. $hmac = [Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($secret))
  35. try {
  36. $signature = ([BitConverter]::ToString($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical)))).Replace('-', '').ToLowerInvariant()
  37. } finally {
  38. $hmac.Dispose()
  39. }
  40. $headers = @{
  41. 'X-Xiaoshu-Key-Id' = $keyId
  42. 'X-Xiaoshu-Timestamp' = $timestamp
  43. 'X-Xiaoshu-Nonce' = $nonce
  44. 'X-Xiaoshu-Signature' = $signature
  45. }
  46. return Invoke-RestMethod -Uri "$base$RelativePath" -Method Get -Headers $headers -TimeoutSec 120
  47. }
  48. $manifest = Invoke-SignedRead '/v1/manifest'
  49. if (-not $manifest.manifestTime -or @($manifest.datasets).Count -lt 10 -or $manifest.consistent -ne $true) { throw 'manifest 返回结构不完整或读取期间旧库仍在变化,请稍后重试。' }
  50. foreach ($dataset in @($manifest.datasets)) {
  51. $key = [Uri]::EscapeDataString([string]$dataset.key)
  52. $page = Invoke-SignedRead "/v1/changes?dataset=$key&limit=1"
  53. if ($page.dataset -ne $dataset.key -or $page.phase -ne 'snapshot' -or -not $page.cursor -or $page.count -ne @($page.items).Count -or $page.count -gt 1) {
  54. throw "数据集 '$($dataset.key)' 首次读取结构异常。"
  55. }
  56. }
  57. Write-Host "健康检查:通过($($health.time))"
  58. Write-Host "数据水位:$($manifest.manifestTime)"
  59. Write-Host '各数据集 changes 读取:通过(未输出业务记录)'
  60. @($manifest.datasets) | Select-Object key, label, count, watermark | Format-Table -AutoSize