feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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<JsonDocument> 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<ErpSessionScopeSnapshot> 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<string, JsonElement> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user