using System.Text.Json; using System.Collections.Generic; using System.Linq; using Lskj.AgentPet.Host.Core.Configuration; namespace Lskj.AgentPet.Host.Core.ErpBridge; public sealed class SessionBoundErpBridgeClient : IErpBridgeClient { private readonly IErpBridgeClient _inner; private readonly ErpSessionScopeBinding _scope; public SessionBoundErpBridgeClient( IErpBridgeClient inner, ErpSessionScopeBinding scope) { _inner = inner ?? throw new ArgumentNullException(nameof(inner)); _scope = scope ?? throw new ArgumentNullException(nameof(scope)); } public async Task SendAsync( JsonElement request, CancellationToken cancellationToken = default) { string method = RequiredString(request, "method", 128); if (string.Equals(method, "context.get", StringComparison.Ordinal)) { using JsonDocument boundRequest = BindRequest(request); JsonDocument context = await _inner.SendAsync( boundRequest.RootElement, cancellationToken) .ConfigureAwait(false); try { _scope.VerifyContextResponse(context.RootElement); return context; } catch { context.Dispose(); throw; } } string protocol = RequiredString(request, "protocolVersion", 16); string clientSessionId = RequiredString(request, "clientSessionId", 128); ErpSessionScopeSnapshot before = await ReadContextAsync( protocol, clientSessionId, cancellationToken).ConfigureAwait(false); JsonDocument? result = null; try { using JsonDocument boundRequest = BindRequest(request); result = await _inner.SendAsync( boundRequest.RootElement, cancellationToken) .ConfigureAwait(false); ErpSessionScopeSnapshot after; try { after = await ReadContextAsync( protocol, clientSessionId, cancellationToken).ConfigureAwait(false); } catch (HostError error) when (error.Code == "erp_session_scope_mismatch") { throw new HostError( "erp_session_scope_changed", "ERP 会话在桌宠操作期间发生切换,操作结果不会交给页面继续使用。", error); } bool targetMayChangeUi = string.Equals( method, "command.execute", StringComparison.Ordinal); if (!before.SameSessionIdentity(after) || (!targetMayChangeUi && !before.SameUiState(after))) { throw new HostError( "erp_session_scope_changed", "ERP 会话在桌宠操作期间发生切换,操作结果不会交给页面继续使用。"); } JsonDocument accepted = result; result = null; return accepted; } finally { result?.Dispose(); } } private async Task ReadContextAsync( string protocol, string clientSessionId, CancellationToken cancellationToken) { string requestId = Guid.NewGuid().ToString("N"); string correlationId = "scope-" + Guid.NewGuid().ToString("N"); using JsonDocument request = JsonSerializer.SerializeToDocument(new { protocolVersion = protocol, requestId, correlationId, clientSessionId, sessionScopeToken = _scope.Token, method = "context.get", payload = new { } }); using JsonDocument response = await _inner.SendAsync( request.RootElement, cancellationToken).ConfigureAwait(false); return _scope.VerifyContextResponse(response.RootElement); } private JsonDocument BindRequest(JsonElement request) { if (request.ValueKind != JsonValueKind.Object) throw new HostError( "bridge_request_invalid", "ERP 桥请求必须是 JSON 对象。"); Dictionary fields = new(StringComparer.Ordinal); foreach (JsonProperty property in request.EnumerateObject()) { if (!fields.TryAdd(property.Name, property.Value.Clone())) throw new HostError( "bridge_request_invalid", "ERP 桥请求包含重复字段。"); } // 浏览器页面不能声明或覆盖会话令牌;Host 只使用启动时绑定的 // ERP 登录范围重新注入它,然后才把请求交给命名管道客户端。 fields["sessionScopeToken"] = JsonSerializer.SerializeToElement( _scope.Token); return JsonSerializer.SerializeToDocument(fields); } private static string RequiredString(JsonElement source, string name, int maximumLength) { if (source.ValueKind != JsonValueKind.Object || !source.TryGetProperty(name, out JsonElement value) || value.ValueKind != JsonValueKind.String) { throw new HostError("bridge_request_invalid", "ERP 桥请求缺少会话绑定字段。"); } string result = value.GetString() ?? string.Empty; if (string.IsNullOrWhiteSpace(result) || result.Length > maximumLength || result.Any(char.IsControl)) throw new HostError("bridge_request_invalid", "ERP 桥请求会话绑定字段无效。"); return result; } }