| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 |
- [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
|