feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Lskj.CommandKernel;
|
||||
|
||||
namespace Lskj.AgentBridge
|
||||
{
|
||||
public enum BridgeCommandStage
|
||||
{
|
||||
Plan,
|
||||
Execute
|
||||
}
|
||||
|
||||
public sealed class BridgePolicyDecision
|
||||
{
|
||||
public static BridgePolicyDecision Allow()
|
||||
{
|
||||
return new BridgePolicyDecision { Allowed = true };
|
||||
}
|
||||
|
||||
public static BridgePolicyDecision Deny(string code, string message)
|
||||
{
|
||||
return Deny(code, message, false);
|
||||
}
|
||||
|
||||
public static BridgePolicyDecision Deny(
|
||||
string code,
|
||||
string message,
|
||||
bool auditRecommended)
|
||||
{
|
||||
return new BridgePolicyDecision
|
||||
{
|
||||
Allowed = false,
|
||||
Code = code,
|
||||
Message = message,
|
||||
AuditRecommended = auditRecommended
|
||||
};
|
||||
}
|
||||
|
||||
public bool Allowed { get; set; }
|
||||
public string Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
public bool AuditRecommended { get; set; }
|
||||
}
|
||||
|
||||
public sealed class BridgeOperationalSnapshot
|
||||
{
|
||||
public int RequestsPerMinute { get; set; }
|
||||
public int MaximumTrackedSessions { get; set; }
|
||||
public int TrackedSessionCount { get; set; }
|
||||
public int DisabledCommandCount { get; set; }
|
||||
public int OpenCircuitCount { get; set; }
|
||||
public int CircuitFailureThreshold { get; set; }
|
||||
public int CircuitOpenSeconds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process-local operational guard for the ERP command bridge. Database
|
||||
/// idempotency remains the business safety boundary; this policy protects
|
||||
/// the legacy UI process from request floods and repeated infrastructure
|
||||
/// failures, and supplies a deployment-time command kill switch.
|
||||
/// </summary>
|
||||
public sealed class BridgeOperationalPolicy
|
||||
{
|
||||
public const int DefaultRequestsPerMinute = 120;
|
||||
public const int DefaultMaximumTrackedSessions = 256;
|
||||
public const int DefaultCircuitFailureThreshold = 5;
|
||||
public const int DefaultCircuitFailureWindowSeconds = 60;
|
||||
public const int DefaultCircuitOpenSeconds = 30;
|
||||
private const int MaximumDisabledCommands = 64;
|
||||
private const int MaximumCircuitStates = 512;
|
||||
private const int MaximumRejectionAuditStates = 512;
|
||||
|
||||
private static readonly Regex SafeCommandName = new Regex(
|
||||
"^[A-Za-z0-9_.:-]{1,128}$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
private static readonly Regex SafeOutcomeCode = new Regex(
|
||||
"^[a-z0-9_.-]{1,128}$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
private static readonly ISet<string> OperationalFailureCodes =
|
||||
new HashSet<string>(new[]
|
||||
{
|
||||
"internal_error",
|
||||
"workflow_database_error",
|
||||
"audit_log_unavailable",
|
||||
"audit_log_capacity_exceeded",
|
||||
"audit_log_corrupt",
|
||||
"idempotency_store_error",
|
||||
"plan_store_capacity_exceeded",
|
||||
"adapter_protocol_error",
|
||||
"adapter_result_missing",
|
||||
"adapter_commit_evidence_missing",
|
||||
"adapter_not_ready",
|
||||
"adapter_readiness_failed"
|
||||
}, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly object _syncRoot = new object();
|
||||
private readonly ISystemClock _clock;
|
||||
private readonly ISet<string> _disabledCommands;
|
||||
private readonly int _requestsPerMinute;
|
||||
private readonly int _maximumTrackedSessions;
|
||||
private readonly int _circuitFailureThreshold;
|
||||
private readonly TimeSpan _circuitFailureWindow;
|
||||
private readonly TimeSpan _circuitOpenDuration;
|
||||
private readonly TimeSpan _sessionRetention = TimeSpan.FromMinutes(5);
|
||||
private readonly IDictionary<string, RateState> _rateStates =
|
||||
new Dictionary<string, RateState>(StringComparer.Ordinal);
|
||||
private readonly IDictionary<string, CircuitState> _circuitStates =
|
||||
new Dictionary<string, CircuitState>(StringComparer.Ordinal);
|
||||
private readonly IDictionary<string, DateTime> _rejectionAudits =
|
||||
new Dictionary<string, DateTime>(StringComparer.Ordinal);
|
||||
private DateTime _capacityAuditWindowStartedAtUtc;
|
||||
|
||||
public BridgeOperationalPolicy(
|
||||
ISystemClock clock,
|
||||
IEnumerable<string> disabledCommands,
|
||||
int requestsPerMinute,
|
||||
int maximumTrackedSessions,
|
||||
int circuitFailureThreshold,
|
||||
TimeSpan circuitFailureWindow,
|
||||
TimeSpan circuitOpenDuration)
|
||||
{
|
||||
if (clock == null) throw new ArgumentNullException("clock");
|
||||
if (requestsPerMinute < 1 || requestsPerMinute > 6000)
|
||||
throw Invalid("桥每分钟请求上限必须在 1-6000 之间。");
|
||||
if (maximumTrackedSessions < 1 || maximumTrackedSessions > 4096)
|
||||
throw Invalid("桥限流会话容量必须在 1-4096 之间。");
|
||||
if (circuitFailureThreshold < 2 || circuitFailureThreshold > 100)
|
||||
throw Invalid("桥熔断故障阈值必须在 2-100 之间。");
|
||||
if (circuitFailureWindow < TimeSpan.FromSeconds(1)
|
||||
|| circuitFailureWindow > TimeSpan.FromMinutes(30))
|
||||
throw Invalid("桥熔断统计窗口必须在 1-1800 秒之间。");
|
||||
if (circuitOpenDuration < TimeSpan.FromSeconds(1)
|
||||
|| circuitOpenDuration > TimeSpan.FromMinutes(30))
|
||||
throw Invalid("桥熔断时长必须在 1-1800 秒之间。");
|
||||
|
||||
HashSet<string> disabled = new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string item in disabledCommands ?? new string[0])
|
||||
{
|
||||
string command = (item ?? string.Empty).Trim();
|
||||
if (!SafeCommandName.IsMatch(command)
|
||||
|| !disabled.Add(command)
|
||||
|| disabled.Count > MaximumDisabledCommands)
|
||||
throw Invalid("停用命令列表包含空值、重复项、非法名称或超过 64 项。");
|
||||
}
|
||||
|
||||
_clock = clock;
|
||||
_disabledCommands = disabled;
|
||||
_requestsPerMinute = requestsPerMinute;
|
||||
_maximumTrackedSessions = maximumTrackedSessions;
|
||||
_circuitFailureThreshold = circuitFailureThreshold;
|
||||
_circuitFailureWindow = circuitFailureWindow;
|
||||
_circuitOpenDuration = circuitOpenDuration;
|
||||
}
|
||||
|
||||
public static BridgeOperationalPolicy CreateDefault(ISystemClock clock)
|
||||
{
|
||||
return new BridgeOperationalPolicy(
|
||||
clock,
|
||||
new string[0],
|
||||
DefaultRequestsPerMinute,
|
||||
DefaultMaximumTrackedSessions,
|
||||
DefaultCircuitFailureThreshold,
|
||||
TimeSpan.FromSeconds(DefaultCircuitFailureWindowSeconds),
|
||||
TimeSpan.FromSeconds(DefaultCircuitOpenSeconds));
|
||||
}
|
||||
|
||||
public static BridgeOperationalPolicy FromEnvironment(ISystemClock clock)
|
||||
{
|
||||
return new BridgeOperationalPolicy(
|
||||
clock,
|
||||
ParseDisabledCommands(
|
||||
Environment.GetEnvironmentVariable(
|
||||
"LSERP_AGENT_DISABLED_COMMANDS")),
|
||||
ParseBoundedInteger(
|
||||
"LSERP_AGENT_RATE_LIMIT_PER_MINUTE",
|
||||
DefaultRequestsPerMinute,
|
||||
1,
|
||||
6000),
|
||||
ParseBoundedInteger(
|
||||
"LSERP_AGENT_RATE_LIMIT_SESSIONS",
|
||||
DefaultMaximumTrackedSessions,
|
||||
1,
|
||||
4096),
|
||||
ParseBoundedInteger(
|
||||
"LSERP_AGENT_CIRCUIT_FAILURE_THRESHOLD",
|
||||
DefaultCircuitFailureThreshold,
|
||||
2,
|
||||
100),
|
||||
TimeSpan.FromSeconds(ParseBoundedInteger(
|
||||
"LSERP_AGENT_CIRCUIT_WINDOW_SECONDS",
|
||||
DefaultCircuitFailureWindowSeconds,
|
||||
1,
|
||||
1800)),
|
||||
TimeSpan.FromSeconds(ParseBoundedInteger(
|
||||
"LSERP_AGENT_CIRCUIT_OPEN_SECONDS",
|
||||
DefaultCircuitOpenSeconds,
|
||||
1,
|
||||
1800)));
|
||||
}
|
||||
|
||||
public void ValidateRegisteredCommands(
|
||||
IEnumerable<CommandDescriptor> descriptors)
|
||||
{
|
||||
if (descriptors == null)
|
||||
throw Invalid("ERP 命令注册表不能为空。");
|
||||
HashSet<string> registered = new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
foreach (CommandDescriptor descriptor in descriptors)
|
||||
{
|
||||
string name = descriptor == null
|
||||
? string.Empty
|
||||
: (descriptor.Name ?? string.Empty).Trim();
|
||||
if (!SafeCommandName.IsMatch(name) || !registered.Add(name))
|
||||
throw Invalid("ERP 命令注册表包含空值、重复项或非法名称。");
|
||||
}
|
||||
lock (_syncRoot)
|
||||
{
|
||||
foreach (string disabled in _disabledCommands)
|
||||
{
|
||||
if (!registered.Contains(disabled))
|
||||
throw Invalid("停用列表包含当前未注册的 ERP 命令。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BridgePolicyDecision Admit(string clientSessionId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clientSessionId))
|
||||
return BridgePolicyDecision.Deny(
|
||||
"invalid_request",
|
||||
"桌宠客户端会话不能为空。");
|
||||
DateTime now = _clock.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
PurgeRateStates(now);
|
||||
RateState state;
|
||||
if (!_rateStates.TryGetValue(clientSessionId, out state))
|
||||
{
|
||||
if (_rateStates.Count >= _maximumTrackedSessions)
|
||||
{
|
||||
return BridgePolicyDecision.Deny(
|
||||
"bridge_rate_state_capacity_exceeded",
|
||||
"ERP 命令桥限流会话容量已满,请关闭无效桌宠会话后重试。",
|
||||
ShouldAuditCapacityRejection(now));
|
||||
}
|
||||
state = new RateState
|
||||
{
|
||||
WindowStartedAtUtc = now,
|
||||
LastSeenAtUtc = now
|
||||
};
|
||||
_rateStates.Add(clientSessionId, state);
|
||||
}
|
||||
if (now >= state.WindowStartedAtUtc
|
||||
&& now - state.WindowStartedAtUtc >= TimeSpan.FromMinutes(1))
|
||||
{
|
||||
state.WindowStartedAtUtc = now;
|
||||
state.RequestCount = 0;
|
||||
state.LimitAuditRecorded = false;
|
||||
}
|
||||
if (now > state.LastSeenAtUtc) state.LastSeenAtUtc = now;
|
||||
if (state.RequestCount >= _requestsPerMinute)
|
||||
{
|
||||
bool auditRecommended = !state.LimitAuditRecorded;
|
||||
state.LimitAuditRecorded = true;
|
||||
return BridgePolicyDecision.Deny(
|
||||
"bridge_rate_limit_exceeded",
|
||||
"ERP 命令桥请求过于频繁,请稍后重试。",
|
||||
auditRecommended);
|
||||
}
|
||||
state.RequestCount += 1;
|
||||
return BridgePolicyDecision.Allow();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCommandVisible(string commandName, bool requiresExecution)
|
||||
{
|
||||
if (!CheckCommandAvailability(
|
||||
commandName,
|
||||
BridgeCommandStage.Plan).Allowed)
|
||||
return false;
|
||||
return !requiresExecution || CheckCommandAvailability(
|
||||
commandName,
|
||||
BridgeCommandStage.Execute).Allowed;
|
||||
}
|
||||
|
||||
public BridgePolicyDecision CheckCommandAvailability(
|
||||
string commandName,
|
||||
BridgeCommandStage stage)
|
||||
{
|
||||
if (!SafeCommandName.IsMatch(commandName ?? string.Empty))
|
||||
return BridgePolicyDecision.Deny(
|
||||
"command_policy_invalid",
|
||||
"ERP 命令运维策略无法识别该命令。");
|
||||
DateTime now = _clock.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_disabledCommands.Contains(commandName))
|
||||
{
|
||||
return BridgePolicyDecision.Deny(
|
||||
"command_disabled",
|
||||
"该 ERP 命令已被运维策略停用。");
|
||||
}
|
||||
return IsCircuitBlocking(commandName, stage, now)
|
||||
? BridgePolicyDecision.Deny(
|
||||
"command_circuit_open",
|
||||
"该 ERP 命令因连续运行故障暂时停用,请稍后重试。")
|
||||
: BridgePolicyDecision.Allow();
|
||||
}
|
||||
}
|
||||
|
||||
public BridgePolicyDecision TryEnterCommand(
|
||||
string commandName,
|
||||
BridgeCommandStage stage)
|
||||
{
|
||||
if (!SafeCommandName.IsMatch(commandName ?? string.Empty))
|
||||
return BridgePolicyDecision.Deny(
|
||||
"command_policy_invalid",
|
||||
"ERP 命令运维策略无法识别该命令。");
|
||||
DateTime now = _clock.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_disabledCommands.Contains(commandName))
|
||||
{
|
||||
return BridgePolicyDecision.Deny(
|
||||
"command_disabled",
|
||||
"该 ERP 命令已被运维策略停用。");
|
||||
}
|
||||
string key = CircuitKey(commandName, stage);
|
||||
CircuitState state;
|
||||
if (!_circuitStates.TryGetValue(key, out state))
|
||||
return BridgePolicyDecision.Allow();
|
||||
if (state.OpenUntilUtc > now || state.HalfOpenProbe)
|
||||
{
|
||||
return BridgePolicyDecision.Deny(
|
||||
"command_circuit_open",
|
||||
"该 ERP 命令因连续运行故障暂时停用,请稍后重试。");
|
||||
}
|
||||
if (state.OpenUntilUtc != DateTime.MinValue)
|
||||
state.HalfOpenProbe = true;
|
||||
return BridgePolicyDecision.Allow();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldAuditCommandRejection(
|
||||
string commandName,
|
||||
BridgeCommandStage stage,
|
||||
string outcomeCode)
|
||||
{
|
||||
if (!SafeCommandName.IsMatch(commandName ?? string.Empty)
|
||||
|| !SafeOutcomeCode.IsMatch(outcomeCode ?? string.Empty))
|
||||
return false;
|
||||
DateTime now = _clock.UtcNow;
|
||||
string key = CircuitKey(commandName, stage)
|
||||
+ "|" + outcomeCode.ToLowerInvariant();
|
||||
lock (_syncRoot)
|
||||
{
|
||||
PurgeRejectionAudits(now);
|
||||
DateTime previous;
|
||||
if (_rejectionAudits.TryGetValue(key, out previous))
|
||||
{
|
||||
if (now < previous
|
||||
|| now - previous < TimeSpan.FromMinutes(1))
|
||||
return false;
|
||||
_rejectionAudits[key] = now;
|
||||
return true;
|
||||
}
|
||||
if (_rejectionAudits.Count >= MaximumRejectionAuditStates)
|
||||
return false;
|
||||
_rejectionAudits.Add(key, now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RecordCommandSuccess(
|
||||
string commandName,
|
||||
BridgeCommandStage stage)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
string key = CircuitKey(commandName, stage);
|
||||
CircuitState state;
|
||||
bool recovered = _circuitStates.TryGetValue(key, out state)
|
||||
&& (state.OpenUntilUtc != DateTime.MinValue
|
||||
|| state.HalfOpenProbe);
|
||||
_circuitStates.Remove(key);
|
||||
return recovered;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RecordCommandFailure(
|
||||
string commandName,
|
||||
BridgeCommandStage stage,
|
||||
string errorCode)
|
||||
{
|
||||
string key = CircuitKey(commandName, stage);
|
||||
DateTime now = _clock.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
CircuitState existing;
|
||||
if (!OperationalFailureCodes.Contains(errorCode ?? string.Empty))
|
||||
{
|
||||
// Validation, permission and user-cancel outcomes must not
|
||||
// heal an infrastructure circuit. They only release a
|
||||
// half-open probe so a later valid request can test it.
|
||||
if (_circuitStates.TryGetValue(key, out existing))
|
||||
existing.HalfOpenProbe = false;
|
||||
return false;
|
||||
}
|
||||
if (!_circuitStates.TryGetValue(key, out existing))
|
||||
{
|
||||
PurgeCircuitStates(now);
|
||||
if (_circuitStates.Count >= MaximumCircuitStates)
|
||||
return false;
|
||||
existing = new CircuitState
|
||||
{
|
||||
FailureWindowStartedAtUtc = now
|
||||
};
|
||||
_circuitStates.Add(key, existing);
|
||||
}
|
||||
|
||||
if (existing.OpenUntilUtc > now
|
||||
&& !existing.HalfOpenProbe)
|
||||
return false;
|
||||
if (existing.HalfOpenProbe
|
||||
|| existing.OpenUntilUtc != DateTime.MinValue)
|
||||
{
|
||||
existing.FailureCount = _circuitFailureThreshold;
|
||||
existing.FailureWindowStartedAtUtc = now;
|
||||
existing.OpenUntilUtc = now.Add(_circuitOpenDuration);
|
||||
existing.HalfOpenProbe = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sameWindow = now < existing.FailureWindowStartedAtUtc
|
||||
|| now - existing.FailureWindowStartedAtUtc
|
||||
<= _circuitFailureWindow;
|
||||
if (!sameWindow)
|
||||
{
|
||||
existing.FailureWindowStartedAtUtc = now;
|
||||
existing.FailureCount = 0;
|
||||
}
|
||||
existing.FailureCount += 1;
|
||||
if (existing.FailureCount >= _circuitFailureThreshold)
|
||||
{
|
||||
existing.OpenUntilUtc = now.Add(_circuitOpenDuration);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public BridgeOperationalSnapshot Snapshot()
|
||||
{
|
||||
DateTime now = _clock.UtcNow;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
PurgeRateStates(now);
|
||||
PurgeCircuitStates(now);
|
||||
int open = 0;
|
||||
foreach (CircuitState state in _circuitStates.Values)
|
||||
{
|
||||
if (state.OpenUntilUtc > now || state.HalfOpenProbe)
|
||||
open += 1;
|
||||
}
|
||||
return new BridgeOperationalSnapshot
|
||||
{
|
||||
RequestsPerMinute = _requestsPerMinute,
|
||||
MaximumTrackedSessions = _maximumTrackedSessions,
|
||||
TrackedSessionCount = _rateStates.Count,
|
||||
DisabledCommandCount = _disabledCommands.Count,
|
||||
OpenCircuitCount = open,
|
||||
CircuitFailureThreshold = _circuitFailureThreshold,
|
||||
CircuitOpenSeconds = (int)_circuitOpenDuration.TotalSeconds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCircuitBlocking(
|
||||
string commandName,
|
||||
BridgeCommandStage stage,
|
||||
DateTime now)
|
||||
{
|
||||
CircuitState state;
|
||||
return _circuitStates.TryGetValue(
|
||||
CircuitKey(commandName, stage),
|
||||
out state)
|
||||
&& (state.OpenUntilUtc > now || state.HalfOpenProbe);
|
||||
}
|
||||
|
||||
private void PurgeRateStates(DateTime now)
|
||||
{
|
||||
List<string> expired = new List<string>();
|
||||
foreach (KeyValuePair<string, RateState> item in _rateStates)
|
||||
{
|
||||
if (now >= item.Value.LastSeenAtUtc
|
||||
&& now - item.Value.LastSeenAtUtc >= _sessionRetention)
|
||||
expired.Add(item.Key);
|
||||
}
|
||||
foreach (string key in expired) _rateStates.Remove(key);
|
||||
}
|
||||
|
||||
private void PurgeCircuitStates(DateTime now)
|
||||
{
|
||||
List<string> expired = new List<string>();
|
||||
foreach (KeyValuePair<string, CircuitState> item in _circuitStates)
|
||||
{
|
||||
CircuitState state = item.Value;
|
||||
if (state.HalfOpenProbe) continue;
|
||||
if (state.OpenUntilUtc != DateTime.MinValue)
|
||||
{
|
||||
if (now >= state.OpenUntilUtc
|
||||
&& now - state.OpenUntilUtc > _circuitFailureWindow)
|
||||
expired.Add(item.Key);
|
||||
}
|
||||
else if (now >= state.FailureWindowStartedAtUtc
|
||||
&& now - state.FailureWindowStartedAtUtc
|
||||
> _circuitFailureWindow)
|
||||
{
|
||||
expired.Add(item.Key);
|
||||
}
|
||||
}
|
||||
foreach (string key in expired) _circuitStates.Remove(key);
|
||||
}
|
||||
|
||||
private void PurgeRejectionAudits(DateTime now)
|
||||
{
|
||||
List<string> expired = new List<string>();
|
||||
foreach (KeyValuePair<string, DateTime> item in _rejectionAudits)
|
||||
{
|
||||
if (now >= item.Value
|
||||
&& now - item.Value >= TimeSpan.FromMinutes(5))
|
||||
expired.Add(item.Key);
|
||||
}
|
||||
foreach (string key in expired) _rejectionAudits.Remove(key);
|
||||
}
|
||||
|
||||
private bool ShouldAuditCapacityRejection(DateTime now)
|
||||
{
|
||||
if (_capacityAuditWindowStartedAtUtc == DateTime.MinValue
|
||||
|| (now >= _capacityAuditWindowStartedAtUtc
|
||||
&& now - _capacityAuditWindowStartedAtUtc
|
||||
>= TimeSpan.FromMinutes(1)))
|
||||
{
|
||||
_capacityAuditWindowStartedAtUtc = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string CircuitKey(
|
||||
string commandName,
|
||||
BridgeCommandStage stage)
|
||||
{
|
||||
return (commandName ?? string.Empty).Trim().ToLowerInvariant()
|
||||
+ "|" + (stage == BridgeCommandStage.Execute ? "execute" : "plan");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ParseDisabledCommands(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return new string[0];
|
||||
return value.Split(new[] { ',' }, StringSplitOptions.None);
|
||||
}
|
||||
|
||||
private static int ParseBoundedInteger(
|
||||
string name,
|
||||
int defaultValue,
|
||||
int minimum,
|
||||
int maximum)
|
||||
{
|
||||
string raw = Environment.GetEnvironmentVariable(name);
|
||||
if (string.IsNullOrWhiteSpace(raw)) return defaultValue;
|
||||
int value;
|
||||
if (!int.TryParse(
|
||||
raw.Trim(),
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture,
|
||||
out value)
|
||||
|| value < minimum
|
||||
|| value > maximum)
|
||||
throw Invalid("ERP 命令桥运维参数格式或范围无效:" + name);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static CommandKernelException Invalid(string message)
|
||||
{
|
||||
return new CommandKernelException(
|
||||
"bridge_operational_policy_invalid",
|
||||
message,
|
||||
6);
|
||||
}
|
||||
|
||||
private sealed class RateState
|
||||
{
|
||||
public DateTime WindowStartedAtUtc { get; set; }
|
||||
public DateTime LastSeenAtUtc { get; set; }
|
||||
public int RequestCount { get; set; }
|
||||
public bool LimitAuditRecorded { get; set; }
|
||||
}
|
||||
|
||||
private sealed class CircuitState
|
||||
{
|
||||
public DateTime FailureWindowStartedAtUtc { get; set; }
|
||||
public int FailureCount { get; set; }
|
||||
public DateTime OpenUntilUtc { get; set; }
|
||||
public bool HalfOpenProbe { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user