Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/AgentBridgeRuntime.cs
2026-08-14 14:28:28 +08:00

919 lines
36 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Lskj.CommandKernel;
using Newtonsoft.Json.Linq;
namespace Lskj.AgentBridge
{
public sealed class AgentBridgeRuntime : IAgentBridgeRuntime
{
public const string ProtocolVersion = "1.0";
private static readonly Regex SafeEnvelopeIdentifier = new Regex(
"^[A-Za-z0-9_.:-]{8,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeMethod = new Regex(
"^[A-Za-z0-9_.:-]{1,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeCommandName = new Regex(
"^[A-Za-z0-9_.:-]{1,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafePlanId = new Regex(
"^[A-Fa-f0-9]{32}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeIdempotencyKey = new Regex(
"^[A-Za-z0-9_.:-]{8,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeOutcomeCode = new Regex(
"^[a-z0-9_.-]{1,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly CommandRegistry _registry;
private readonly CommandDispatcher _dispatcher;
private readonly IServerPlanStore _plans;
private readonly IBridgeExecutionContextFactory _contexts;
private readonly IBridgeContextProvider _contextProvider;
private readonly ICommandConfirmationPrompt _confirmationPrompt;
private readonly IConfirmationTokenIssuer _confirmationIssuer;
private readonly ISystemClock _clock;
private readonly BridgeOperationalPolicy _operations;
private readonly IBridgeOperationalAuditSink _operationalAudit;
private readonly CommandRolloutPolicy _rolloutPolicy;
private readonly WorkflowUatAuthorizationPolicy _uatPolicy;
private readonly object _executionSyncRoot = new object();
public AgentBridgeRuntime(
CommandRegistry registry,
CommandDispatcher dispatcher,
IServerPlanStore plans,
IBridgeExecutionContextFactory contexts,
IBridgeContextProvider contextProvider,
ICommandConfirmationPrompt confirmationPrompt,
IConfirmationTokenIssuer confirmationIssuer,
ISystemClock clock)
: this(
registry,
dispatcher,
plans,
contexts,
contextProvider,
confirmationPrompt,
confirmationIssuer,
clock,
BridgeOperationalPolicy.CreateDefault(clock),
new NullBridgeOperationalAuditSink(),
CommandRolloutPolicy.AllowAll(),
WorkflowUatAuthorizationPolicy.Disabled(clock))
{
}
public AgentBridgeRuntime(
CommandRegistry registry,
CommandDispatcher dispatcher,
IServerPlanStore plans,
IBridgeExecutionContextFactory contexts,
IBridgeContextProvider contextProvider,
ICommandConfirmationPrompt confirmationPrompt,
IConfirmationTokenIssuer confirmationIssuer,
ISystemClock clock,
BridgeOperationalPolicy operations)
: this(
registry,
dispatcher,
plans,
contexts,
contextProvider,
confirmationPrompt,
confirmationIssuer,
clock,
operations,
new NullBridgeOperationalAuditSink(),
CommandRolloutPolicy.AllowAll(),
WorkflowUatAuthorizationPolicy.Disabled(clock))
{
}
public AgentBridgeRuntime(
CommandRegistry registry,
CommandDispatcher dispatcher,
IServerPlanStore plans,
IBridgeExecutionContextFactory contexts,
IBridgeContextProvider contextProvider,
ICommandConfirmationPrompt confirmationPrompt,
IConfirmationTokenIssuer confirmationIssuer,
ISystemClock clock,
BridgeOperationalPolicy operations,
IBridgeOperationalAuditSink operationalAudit)
: this(
registry,
dispatcher,
plans,
contexts,
contextProvider,
confirmationPrompt,
confirmationIssuer,
clock,
operations,
operationalAudit,
CommandRolloutPolicy.AllowAll(),
WorkflowUatAuthorizationPolicy.Disabled(clock))
{
}
public AgentBridgeRuntime(
CommandRegistry registry,
CommandDispatcher dispatcher,
IServerPlanStore plans,
IBridgeExecutionContextFactory contexts,
IBridgeContextProvider contextProvider,
ICommandConfirmationPrompt confirmationPrompt,
IConfirmationTokenIssuer confirmationIssuer,
ISystemClock clock,
BridgeOperationalPolicy operations,
IBridgeOperationalAuditSink operationalAudit,
CommandRolloutPolicy rolloutPolicy)
: this(
registry,
dispatcher,
plans,
contexts,
contextProvider,
confirmationPrompt,
confirmationIssuer,
clock,
operations,
operationalAudit,
rolloutPolicy,
WorkflowUatAuthorizationPolicy.Disabled(clock))
{
}
public AgentBridgeRuntime(
CommandRegistry registry,
CommandDispatcher dispatcher,
IServerPlanStore plans,
IBridgeExecutionContextFactory contexts,
IBridgeContextProvider contextProvider,
ICommandConfirmationPrompt confirmationPrompt,
IConfirmationTokenIssuer confirmationIssuer,
ISystemClock clock,
BridgeOperationalPolicy operations,
IBridgeOperationalAuditSink operationalAudit,
CommandRolloutPolicy rolloutPolicy,
WorkflowUatAuthorizationPolicy uatPolicy)
{
if (registry == null) throw new ArgumentNullException("registry");
if (dispatcher == null) throw new ArgumentNullException("dispatcher");
if (plans == null) throw new ArgumentNullException("plans");
if (contexts == null) throw new ArgumentNullException("contexts");
if (contextProvider == null) throw new ArgumentNullException("contextProvider");
if (confirmationPrompt == null) throw new ArgumentNullException("confirmationPrompt");
if (confirmationIssuer == null) throw new ArgumentNullException("confirmationIssuer");
if (clock == null) throw new ArgumentNullException("clock");
if (operations == null) throw new ArgumentNullException("operations");
if (operationalAudit == null)
throw new ArgumentNullException("operationalAudit");
if (rolloutPolicy == null)
throw new ArgumentNullException("rolloutPolicy");
if (uatPolicy == null)
throw new ArgumentNullException("uatPolicy");
_registry = registry;
_dispatcher = dispatcher;
_plans = plans;
_contexts = contexts;
_contextProvider = contextProvider;
_confirmationPrompt = confirmationPrompt;
_confirmationIssuer = confirmationIssuer;
_clock = clock;
_operations = operations;
_operationalAudit = operationalAudit;
_rolloutPolicy = rolloutPolicy;
_uatPolicy = uatPolicy;
}
public BridgeResponse Handle(BridgeRequest request)
{
if (request == null) return BridgeResponse.Error(null, "invalid_request", "请求不能为空。");
if (!string.Equals(request.ProtocolVersion, ProtocolVersion, StringComparison.Ordinal))
return BridgeResponse.Error(request, "protocol_version_unsupported", "不支持的桥协议版本。");
if (!SafeEnvelopeIdentifier.IsMatch(request.RequestId ?? string.Empty)
|| !SafeEnvelopeIdentifier.IsMatch(request.CorrelationId ?? string.Empty)
|| !SafeEnvelopeIdentifier.IsMatch(request.ClientSessionId ?? string.Empty)
|| !SafeMethod.IsMatch(request.Method ?? string.Empty))
{
return BridgeResponse.Error(
request,
"invalid_request",
"requestId、correlationId、clientSessionId 或 method 格式无效。");
}
if (request.Payload == null) request.Payload = new JObject();
if (request.UatGrant != null
&& request.Method != "command.plan"
&& request.Method != "command.execute")
return BridgeResponse.Error(
request,
"workflow_uat_grant_not_applicable",
"UAT 用例令牌只能用于受控计划或执行请求。");
BridgeResponse sessionScopeFailure = ValidateSessionScope(request);
if (sessionScopeFailure != null) return sessionScopeFailure;
BridgePolicyDecision admission = _operations.Admit(
request.ClientSessionId);
if (!admission.Allowed)
{
SafeOperationalAudit(
request,
"operational_rejected",
null,
"request",
admission.Code,
admission.AuditRecommended);
return BridgeResponse.Error(
request,
admission.Code,
admission.Message);
}
try
{
_plans.PurgeExpired(_clock.UtcNow);
switch (request.Method.Trim().ToLowerInvariant())
{
case "health":
return Health(request);
case "capabilities.list":
return Capabilities(request);
case "context.get":
return Context(request);
case "command.plan":
return Plan(request);
case "command.execute":
// ERP UI 本身是单线程;串行化还能确保同一 plan 的并发重试
// 不会重复弹出确认窗口。第一个请求完成后计划即被移除。
lock (_executionSyncRoot)
return Execute(request);
default:
return BridgeResponse.Error(request, "method_not_found", "未注册桥方法:" + request.Method);
}
}
catch (CommandKernelException ex)
{
return BridgeResponse.Error(request, ex.Code, ex.Message);
}
catch (Exception)
{
return BridgeResponse.Error(request, "internal_error", "ERP 命令桥发生内部错误,请使用关联 ID 查询审计日志。");
}
}
private BridgeResponse ValidateSessionScope(BridgeRequest request)
{
string method = (request.Method ?? string.Empty).Trim()
.ToLowerInvariant();
bool bootstrapAllowed = method == "health"
|| method == "context.get";
if (string.IsNullOrWhiteSpace(request.SessionScopeToken))
{
return bootstrapAllowed
? null
: BridgeResponse.Error(
request,
"bridge_session_scope_token_required",
"能力、计划和执行请求必须绑定当前 ERP 会话作用域。");
}
if (!BridgeSessionScopeToken.IsValid(request.SessionScopeToken))
{
return BridgeResponse.Error(
request,
"bridge_session_scope_token_invalid",
"ERP 会话作用域令牌格式无效。");
}
try
{
CommandExecutionContext context = _contexts.Create(request);
if (!BridgeSessionScopeToken.Matches(
request.SessionScopeToken,
context))
{
return BridgeResponse.Error(
request,
"erp_session_scope_mismatch",
"请求绑定的数据库、用户、账套、子系统或权限范围与当前 ERP 会话不一致。");
}
return null;
}
catch (CommandKernelException error)
{
return BridgeResponse.Error(request, error.Code, error.Message);
}
catch
{
return BridgeResponse.Error(
request,
"erp_session_scope_unavailable",
"当前 ERP 会话作用域无法安全确认。");
}
}
private BridgeResponse Health(BridgeRequest request)
{
EnsurePayloadShape(request.Payload);
BridgeOperationalSnapshot policy = _operations.Snapshot();
int enabledCommands = _registry.List().Count(item =>
_operations.IsCommandVisible(
item.Name,
IsExecutableRisk(item.Risk))
&& _uatPolicy.IsVisibleInGeneralCapabilities(item.Name));
return BridgeResponse.Ok(request, new
{
status = "ready",
protocolVersion = ProtocolVersion,
serverTimeUtc = _clock.UtcNow,
commandCount = _registry.List().Count,
enabledCommandCount = enabledCommands,
operationalPolicy = new
{
requestsPerMinute = policy.RequestsPerMinute,
maximumTrackedSessions = policy.MaximumTrackedSessions,
trackedSessionCount = policy.TrackedSessionCount,
disabledCommandCount = policy.DisabledCommandCount,
openCircuitCount = policy.OpenCircuitCount,
circuitFailureThreshold = policy.CircuitFailureThreshold,
circuitOpenSeconds = policy.CircuitOpenSeconds
},
rolloutPolicy = new
{
configured = _rolloutPolicy.Configured,
failClosed = _rolloutPolicy.Configured
&& string.Equals(
_rolloutPolicy.DefaultAction,
"deny",
StringComparison.Ordinal),
customerId = _rolloutPolicy.CustomerId,
databaseScopeFingerprint =
_rolloutPolicy.DatabaseScopeFingerprint,
sourceSha256 = _rolloutPolicy.SourceSha256,
defaultAction = _rolloutPolicy.DefaultAction,
ruleCount = _rolloutPolicy.RuleCount
},
workflowUat = _uatPolicy.SafeSnapshot()
});
}
private BridgeResponse Capabilities(BridgeRequest request)
{
EnsurePayloadShape(request.Payload);
CommandExecutionContext context = _contexts.Create(request);
IList<CommandDescriptor> commands = _registry.List()
.Where(item => _dispatcher.CheckAuthorization(item, context, false).Allowed)
.Where(item => _operations.IsCommandVisible(
item.Name,
IsExecutableRisk(item.Risk)))
.Where(item => _uatPolicy.IsVisibleInGeneralCapabilities(
item.Name))
.ToList();
return BridgeResponse.Ok(request, new
{
commands = commands.Select(item => new
{
name = item.Name,
version = item.Version,
description = item.Description,
schemaVersion = item.SchemaVersion,
inputSchema = item.InputSchema,
risk = item.Risk.ToString().ToLowerInvariant(),
requiresConfirmation = item.RequiresConfirmation,
requiresIdempotencyKey = item.RequiresIdempotencyKey
}).ToList()
});
}
private BridgeResponse Context(BridgeRequest request)
{
EnsurePayloadShape(request.Payload);
CommandExecutionContext context = _contexts.Create(request);
return BridgeResponse.Ok(request, _contextProvider.Snapshot(context));
}
private BridgeResponse Plan(BridgeRequest request)
{
EnsurePayloadShape(request.Payload, "command", "input");
string commandName = RequiredString(
request.Payload,
"command",
128,
SafeCommandName);
JToken inputToken = request.Payload["input"];
if (inputToken != null && inputToken.Type != JTokenType.Object)
throw new CommandKernelException(
"invalid_request",
"command.plan 的 input 必须是 JSON 对象。",
2);
JObject inputObject = inputToken as JObject ?? new JObject();
IDictionary<string, object> input = inputObject.ToObject<Dictionary<string, object>>();
EnsurePlanOperationalCommand(commandName, request);
try
{
CommandExecutionContext context = _contexts.Create(request);
WorkflowUatPlanBinding uatBinding = _uatPolicy.AuthorizePlan(
request,
context,
commandName);
CommandPlan plan = _dispatcher.Plan(commandName, input, context);
_uatPolicy.BindPlan(plan, uatBinding);
if (IsExecutionAllowed(plan)) _plans.Save(plan);
RecordCommandSuccess(
request,
commandName,
BridgeCommandStage.Plan);
return BridgeResponse.Ok(request, new { plan = ProjectPlan(plan) });
}
catch (CommandKernelException error)
{
RecordCommandFailure(
request,
commandName,
BridgeCommandStage.Plan,
error.Code);
throw;
}
catch
{
RecordCommandFailure(
request,
commandName,
BridgeCommandStage.Plan,
"internal_error");
throw;
}
}
private BridgeResponse Execute(BridgeRequest request)
{
EnsurePayloadShape(request.Payload, "planId", "idempotencyKey");
string planId = RequiredString(
request.Payload,
"planId",
32,
SafePlanId);
string idempotencyKey = RequiredString(
request.Payload,
"idempotencyKey",
128,
SafeIdempotencyKey);
CommandPlan plan;
if (!_plans.TryGet(planId, out plan))
return BridgeResponse.Error(request, "plan_not_found", "计划不存在或已过期,请重新生成预览。");
if (!IsExecutionAllowed(plan))
{
_plans.Remove(plan.PlanId);
return BridgeResponse.Error(request, "plan_not_executable", "该计划是解析或只读预览,不能执行。");
}
ICommandHandler handler = _registry.Resolve(plan.CommandName);
if (handler == null)
{
_plans.Remove(plan.PlanId);
return BridgeResponse.Error(request, "command_not_found", "计划对应的命令已不可用。");
}
BridgePolicyDecision availability = _operations.TryEnterCommand(
plan.CommandName,
BridgeCommandStage.Execute);
if (!availability.Allowed)
{
if (BridgeErrorRecoveryContract.InvalidatesPlan(
availability.Code))
_plans.Remove(plan.PlanId);
AuditPolicyRejection(
request,
plan.CommandName,
BridgeCommandStage.Execute,
availability);
return BridgeResponse.Error(
request,
availability.Code,
availability.Message);
}
try
{
CommandExecutionContext context = _contexts.Create(request);
_uatPolicy.AuthorizeExecute(request, context, plan);
context.IdempotencyKey = idempotencyKey;
_dispatcher.ValidateExecutionPreconditions(plan, context);
if (handler.Descriptor.RequiresConfirmation)
{
bool confirmed = _confirmationPrompt.Confirm(
handler.Descriptor,
plan,
context);
if (!confirmed)
{
RecordCommandFailure(
request,
plan.CommandName,
BridgeCommandStage.Execute,
"user_cancelled");
return BridgeResponse.Error(
request,
"user_cancelled",
"用户已取消操作。");
}
context.ConfirmationToken = _confirmationIssuer.Issue(
plan,
context,
TimeSpan.FromMinutes(2));
}
CommandResult result = _dispatcher.Execute(plan, context);
if (!result.Success)
throw new CommandKernelException(
"command_result_invalid",
"ERP 命令处理器没有返回可确认的成功结果,请使用关联 ID 查询审计。",
6);
if (result.Success) _plans.Remove(plan.PlanId);
RecordCommandSuccess(
request,
plan.CommandName,
BridgeCommandStage.Execute);
string followupCode;
CommandPlan followup = TryPlanFollowup(
handler,
plan,
result,
request,
out followupCode);
return BridgeResponse.Ok(request, new
{
result = ProjectResult(result),
followupPlan = followup == null ? null : ProjectPlan(followup),
followupCode = followupCode
});
}
catch (CommandKernelException error)
{
if (BridgeErrorRecoveryContract.InvalidatesPlan(error.Code))
_plans.Remove(plan.PlanId);
RecordCommandFailure(
request,
plan.CommandName,
BridgeCommandStage.Execute,
error.Code);
throw;
}
catch
{
_plans.Remove(plan.PlanId);
RecordCommandFailure(
request,
plan.CommandName,
BridgeCommandStage.Execute,
"internal_error");
throw;
}
}
private CommandPlan TryPlanFollowup(
ICommandHandler completedHandler,
CommandPlan completedPlan,
CommandResult completedResult,
BridgeRequest request,
out string failureCode)
{
failureCode = null;
if (completedResult == null || !completedResult.Success) return null;
ICommandExecutionFollowupProvider provider =
completedHandler as ICommandExecutionFollowupProvider;
if (provider == null) return null;
string followupCommandName = null;
try
{
CommandExecutionContext followupContext = _contexts.Create(request);
CommandFollowupRequest followup;
if (!provider.TryCreateFollowup(
completedPlan,
completedResult,
followupContext,
out followup)
|| followup == null
|| string.IsNullOrWhiteSpace(followup.CommandName))
return null;
followupCommandName = followup.CommandName;
EnsurePlanOperationalCommand(followupCommandName, request);
WorkflowUatPlanBinding uatBinding = _uatPolicy.AuthorizePlan(
request,
followupContext,
followupCommandName);
CommandPlan plan = _dispatcher.Plan(
followupCommandName,
followup.Input ?? new Dictionary<string, object>(),
followupContext);
_uatPolicy.BindPlan(plan, uatBinding);
if (IsExecutionAllowed(plan)) _plans.Save(plan);
RecordCommandSuccess(
request,
followupCommandName,
BridgeCommandStage.Plan);
return plan;
}
catch (CommandKernelException error)
{
// 主写入已成功,后续计划失败不能把整个请求伪装成写入失败。
if (!string.IsNullOrWhiteSpace(followupCommandName))
RecordCommandFailure(
request,
followupCommandName,
BridgeCommandStage.Plan,
error.Code);
failureCode = error.Code;
return null;
}
catch
{
if (!string.IsNullOrWhiteSpace(followupCommandName))
RecordCommandFailure(
request,
followupCommandName,
BridgeCommandStage.Plan,
"internal_error");
failureCode = "followup_plan_failed";
return null;
}
}
private static object ProjectPlan(CommandPlan plan)
{
object title;
object preview;
plan.Data.TryGetValue("title", out title);
plan.Data.TryGetValue("preview", out preview);
return new
{
planId = plan.PlanId,
commandName = plan.CommandName,
commandVersion = plan.CommandVersion,
moduleCode = plan.ModuleCode,
risk = plan.Risk.ToString().ToLowerInvariant(),
createdAtUtc = plan.CreatedAtUtc,
expiresAtUtc = plan.ExpiresAtUtc,
valid = plan.Valid,
executionAllowed = IsExecutionAllowed(plan),
inputFingerprint = plan.InputFingerprint,
outcomeCode = ProjectOutcomeCode(plan),
title = title,
preview = preview,
data = plan.Data,
warnings = plan.Warnings
};
}
private static string ProjectOutcomeCode(CommandPlan plan)
{
object raw;
string value = plan != null
&& plan.Data.TryGetValue("outcomeCode", out raw)
? raw as string
: null;
if (!string.IsNullOrWhiteSpace(value)
&& SafeOutcomeCode.IsMatch(value))
return value;
return plan != null && plan.Valid ? "plan_ready" : "plan_invalid";
}
private static bool IsExecutionAllowed(CommandPlan plan)
{
if (plan == null || !plan.Valid) return false;
return plan.Risk == CommandRisk.Navigate
|| plan.Risk == CommandRisk.Write
|| plan.Risk == CommandRisk.Critical;
}
private static bool IsExecutableRisk(CommandRisk risk)
{
return risk == CommandRisk.Navigate
|| risk == CommandRisk.Write
|| risk == CommandRisk.Critical;
}
private void EnsureOperationalCommand(
string commandName,
BridgeCommandStage stage,
BridgeRequest request)
{
BridgePolicyDecision decision = _operations.TryEnterCommand(
commandName,
stage);
if (decision.Allowed) return;
AuditPolicyRejection(request, commandName, stage, decision);
throw new CommandKernelException(
decision.Code,
decision.Message,
6);
}
private void EnsurePlanOperationalCommand(
string commandName,
BridgeRequest request)
{
BridgePolicyDecision planning =
_operations.CheckCommandAvailability(
commandName,
BridgeCommandStage.Plan);
if (!planning.Allowed)
{
AuditPolicyRejection(
request,
commandName,
BridgeCommandStage.Plan,
planning);
throw new CommandKernelException(
planning.Code,
planning.Message,
6);
}
ICommandHandler handler = _registry.Resolve(commandName);
if (handler != null
&& handler.Descriptor != null
&& IsExecutableRisk(handler.Descriptor.Risk))
{
BridgePolicyDecision execution =
_operations.CheckCommandAvailability(
commandName,
BridgeCommandStage.Execute);
if (!execution.Allowed)
{
AuditPolicyRejection(
request,
commandName,
BridgeCommandStage.Execute,
execution);
throw new CommandKernelException(
execution.Code,
execution.Message,
6);
}
}
EnsureOperationalCommand(
commandName,
BridgeCommandStage.Plan,
request);
}
private void AuditPolicyRejection(
BridgeRequest request,
string commandName,
BridgeCommandStage stage,
BridgePolicyDecision decision)
{
if (decision == null || decision.Allowed) return;
bool recommended = _operations.ShouldAuditCommandRejection(
commandName,
stage,
decision.Code);
SafeOperationalAudit(
request,
"operational_rejected",
commandName,
StageName(stage),
decision.Code,
recommended);
}
private void RecordCommandSuccess(
BridgeRequest request,
string commandName,
BridgeCommandStage stage)
{
if (!_operations.RecordCommandSuccess(commandName, stage)) return;
SafeOperationalAudit(
request,
"command_circuit_closed",
commandName,
StageName(stage),
"command_circuit_closed",
true);
}
private void RecordCommandFailure(
BridgeRequest request,
string commandName,
BridgeCommandStage stage,
string errorCode)
{
if (!_operations.RecordCommandFailure(
commandName,
stage,
errorCode)) return;
SafeOperationalAudit(
request,
"command_circuit_opened",
commandName,
StageName(stage),
"command_circuit_open",
true);
}
private void SafeOperationalAudit(
BridgeRequest request,
string eventName,
string commandName,
string stage,
string outcomeCode,
bool recommended)
{
if (!recommended || request == null) return;
CommandExecutionContext context = null;
try { context = _contexts.Create(request); }
catch { }
if (context == null)
{
context = new CommandExecutionContext
{
CorrelationId = request.CorrelationId,
ClientSessionId = request.ClientSessionId
};
}
try
{
_operationalAudit.RecordOperationalEvent(
eventName,
request.Method,
commandName,
stage,
outcomeCode,
context);
}
catch
{
// A rejection must remain fail-closed even if its best-effort
// operational audit cannot be appended. Duplicate rejection
// suppression prevents the rejection path becoming a log DoS.
}
}
private static string StageName(BridgeCommandStage stage)
{
return stage == BridgeCommandStage.Execute ? "execute" : "plan";
}
private static object ProjectResult(CommandResult result)
{
return new
{
success = result.Success,
code = result.Code,
message = result.Message,
recordId = result.RecordId,
replayed = result.Replayed,
transactionEvidenceId = result.TransactionEvidenceId,
businessAuditId = result.BusinessAuditId,
data = result.Data
};
}
private static void EnsurePayloadShape(JObject payload, params string[] allowedNames)
{
if (payload == null)
throw new CommandKernelException(
"invalid_request",
"桥请求 payload 必须是 JSON 对象。",
2);
ISet<string> allowed = new HashSet<string>(
allowedNames ?? new string[0],
StringComparer.Ordinal);
foreach (JProperty property in payload.Properties())
{
if (!allowed.Contains(property.Name))
throw new CommandKernelException(
"invalid_request",
"桥请求 payload 包含未知字段:" + property.Name,
2);
}
}
private static string RequiredString(
JObject source,
string name,
int maximumLength,
Regex pattern)
{
JToken token = source == null ? null : source[name];
if (token == null || token.Type != JTokenType.String)
throw new CommandKernelException("invalid_request", "缺少字段:" + name, 2);
string value = token.Value<string>();
if (string.IsNullOrWhiteSpace(value))
throw new CommandKernelException("invalid_request", "缺少字段:" + name, 2);
value = value.Trim();
if (value.Length > maximumLength || pattern == null || !pattern.IsMatch(value))
throw new CommandKernelException(
"invalid_request",
"桥请求字段格式无效:" + name,
2);
return value;
}
}
}