[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$BaseUrl, [Parameter(Mandatory = $true)] [string]$ConfigurationPath ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest if (-not (Test-Path -LiteralPath $ConfigurationPath -PathType Leaf)) { throw "配置文件不存在:$ConfigurationPath" } $configuration = Get-Content -LiteralPath $ConfigurationPath -Raw | ConvertFrom-Json $keyId = [string]$configuration.XiaoshuSync.KeyId $secret = [string]$configuration.XiaoshuSync.Secret if ([string]::IsNullOrWhiteSpace($keyId) -or $secret.Length -lt 32) { throw '配置文件中的 KeyId 或 Secret 无效。' } $base = $BaseUrl.TrimEnd('/') $baseUri = [Uri]$base 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) { throw 'BaseUrl 必须是与配置 PathBase 一致的 HTTP(S) 应用根地址,不能附加 /v1。' } $health = Invoke-RestMethod -Uri "$base/v1/health" -Method Get -TimeoutSec 15 if ($health.status -ne 'ok' -or $health.service -ne 'xiaoshu-legacy-sync' -or $health.version -ne 'v1') { throw '健康地址未返回小树同步桥 v1,请检查域名绑定和 IIS 子应用。' } $pathBase = $baseUri.AbsolutePath.TrimEnd('/') function Invoke-SignedRead([string]$RelativePath) { $pathAndQuery = "$pathBase$RelativePath" $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds().ToString() $nonce = [Guid]::NewGuid().ToString('N') $sha256 = [Security.Cryptography.SHA256]::Create() try { $emptyBodyHash = ([BitConverter]::ToString($sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes('')))).Replace('-', '').ToLowerInvariant() } finally { $sha256.Dispose() } $canonical = "GET`n$pathAndQuery`n$timestamp`n$nonce`n$emptyBodyHash" $hmac = [Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($secret)) try { $signature = ([BitConverter]::ToString($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical)))).Replace('-', '').ToLowerInvariant() } finally { $hmac.Dispose() } $headers = @{ 'X-Xiaoshu-Key-Id' = $keyId 'X-Xiaoshu-Timestamp' = $timestamp 'X-Xiaoshu-Nonce' = $nonce 'X-Xiaoshu-Signature' = $signature } return Invoke-RestMethod -Uri "$base$RelativePath" -Method Get -Headers $headers -TimeoutSec 120 } $manifest = Invoke-SignedRead '/v1/manifest' if (-not $manifest.manifestTime -or @($manifest.datasets).Count -lt 10 -or $manifest.consistent -ne $true) { throw 'manifest 返回结构不完整或读取期间旧库仍在变化,请稍后重试。' } foreach ($dataset in @($manifest.datasets)) { $key = [Uri]::EscapeDataString([string]$dataset.key) $page = Invoke-SignedRead "/v1/changes?dataset=$key&limit=1" 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) { throw "数据集 '$($dataset.key)' 首次读取结构异常。" } } Write-Host "健康检查:通过($($health.time))" Write-Host "数据水位:$($manifest.manifestTime)" Write-Host '各数据集 changes 读取:通过(未输出业务记录)' @($manifest.datasets) | Select-Object key, label, count, watermark | Format-Table -AutoSize