feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Lskj.CommandKernel
|
||||
{
|
||||
public enum CommandRisk
|
||||
{
|
||||
Read = 0,
|
||||
Navigate = 1,
|
||||
Draft = 2,
|
||||
Write = 3,
|
||||
Critical = 4
|
||||
}
|
||||
|
||||
public sealed class CommandDescriptor
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string SchemaVersion { get; set; }
|
||||
public IDictionary<string, object> InputSchema { get; set; }
|
||||
public string RequiredPermission { get; set; }
|
||||
public CommandRisk Risk { get; set; }
|
||||
public bool RequiresConfirmation { get; set; }
|
||||
public bool RequiresIdempotencyKey { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CommandExecutionContext
|
||||
{
|
||||
public CommandExecutionContext()
|
||||
{
|
||||
CorrelationId = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
public string CorrelationId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string AccountBook { get; set; }
|
||||
public string SubSystemId { get; set; }
|
||||
// 由受信任的 ERP 登录运行时根据实际数据库提供者、服务器和库名
|
||||
// 计算;模型和命令输入均不得提供或覆盖该值。
|
||||
public string DatabaseScopeFingerprint { get; set; }
|
||||
public string IdempotencyKey { get; set; }
|
||||
public string ConfirmationToken { get; set; }
|
||||
public string ClientSessionId { get; set; }
|
||||
// These fields are populated only by the in-process bridge after a
|
||||
// customer-UAT grant has been verified. The lease is deliberately
|
||||
// never projected or audited; guarded workflow gateways revalidate it
|
||||
// immediately before every read/write procedure call.
|
||||
public string UatAuthorizationIdSha256 { get; set; }
|
||||
public string UatCaseCode { get; set; }
|
||||
public string UatCommandName { get; set; }
|
||||
public string UatExecutionLease { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CommandPlan
|
||||
{
|
||||
private readonly Dictionary<string, object> _serverData;
|
||||
|
||||
public CommandPlan()
|
||||
{
|
||||
PlanId = Guid.NewGuid().ToString("N");
|
||||
CreatedAtUtc = DateTime.UtcNow;
|
||||
ExpiresAtUtc = CreatedAtUtc.AddMinutes(10);
|
||||
Data = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
Warnings = new List<string>();
|
||||
_serverData = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public string PlanId { get; set; }
|
||||
public string CommandName { get; set; }
|
||||
public string CommandVersion { get; set; }
|
||||
public string ModuleCode { get; set; }
|
||||
public string OwnerClientSessionId { get; set; }
|
||||
public string OwnerUserId { get; set; }
|
||||
public string OwnerUserName { get; set; }
|
||||
public string OwnerAccountBook { get; set; }
|
||||
public string OwnerSubSystemId { get; set; }
|
||||
public string OwnerDatabaseScopeFingerprint { get; set; }
|
||||
public string CorrelationId { get; set; }
|
||||
public string InputFingerprint { get; set; }
|
||||
public CommandRisk Risk { get; set; }
|
||||
public DateTime CreatedAtUtc { get; set; }
|
||||
public DateTime ExpiresAtUtc { get; set; }
|
||||
public bool Valid { get; set; }
|
||||
public IDictionary<string, object> Data { get; private set; }
|
||||
public IList<string> Warnings { get; private set; }
|
||||
|
||||
public void SetServerData(string key, object value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("服务端计划数据键不能为空。", "key");
|
||||
_serverData[key] = value;
|
||||
}
|
||||
|
||||
public T GetServerData<T>(string key)
|
||||
{
|
||||
object value;
|
||||
if (string.IsNullOrWhiteSpace(key) || !_serverData.TryGetValue(key, out value))
|
||||
throw new CommandKernelException("plan_state_missing", "执行计划缺少服务端状态,请重新生成预览。", 6);
|
||||
if (!(value is T))
|
||||
throw new CommandKernelException("plan_state_invalid", "执行计划服务端状态类型无效,请重新生成预览。", 6);
|
||||
return (T)value;
|
||||
}
|
||||
|
||||
public bool TryGetServerData<T>(string key, out T value)
|
||||
{
|
||||
object stored;
|
||||
if (!string.IsNullOrWhiteSpace(key)
|
||||
&& _serverData.TryGetValue(key, out stored)
|
||||
&& stored is T)
|
||||
{
|
||||
value = (T)stored;
|
||||
return true;
|
||||
}
|
||||
value = default(T);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CommandResult
|
||||
{
|
||||
public CommandResult()
|
||||
{
|
||||
Data = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool Success { get; set; }
|
||||
public string Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string RecordId { get; set; }
|
||||
public bool Replayed { get; set; }
|
||||
public string TransactionEvidenceId { get; set; }
|
||||
public string BusinessAuditId { get; set; }
|
||||
public IDictionary<string, object> Data { get; private set; }
|
||||
}
|
||||
|
||||
public interface ICommandHandler
|
||||
{
|
||||
CommandDescriptor Descriptor { get; }
|
||||
CommandPlan Plan(IDictionary<string, object> input, CommandExecutionContext context);
|
||||
CommandResult Execute(CommandPlan plan, CommandExecutionContext context);
|
||||
}
|
||||
|
||||
public sealed class CommandFollowupRequest
|
||||
{
|
||||
public CommandFollowupRequest()
|
||||
{
|
||||
Input = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public string CommandName { get; set; }
|
||||
public IDictionary<string, object> Input { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写命令成功后可生成一个新的、仍需独立预览和确认的后续计划。
|
||||
/// 返回值只描述下一条命令,绝不能在此接口内执行副作用。
|
||||
/// </summary>
|
||||
public interface ICommandExecutionFollowupProvider
|
||||
{
|
||||
bool TryCreateFollowup(
|
||||
CommandPlan completedPlan,
|
||||
CommandResult completedResult,
|
||||
CommandExecutionContext context,
|
||||
out CommandFollowupRequest followup);
|
||||
}
|
||||
|
||||
public interface ICommandAuditSink
|
||||
{
|
||||
void Planned(CommandDescriptor descriptor, CommandPlan plan, CommandExecutionContext context);
|
||||
void Completed(CommandDescriptor descriptor, CommandPlan plan, CommandResult result, CommandExecutionContext context);
|
||||
void Failed(CommandDescriptor descriptor, CommandPlan plan, Exception exception, CommandExecutionContext context);
|
||||
}
|
||||
|
||||
public enum IdempotencyClaimState
|
||||
{
|
||||
Acquired = 0,
|
||||
Replay = 1,
|
||||
InProgress = 2,
|
||||
Conflict = 3
|
||||
}
|
||||
|
||||
public sealed class IdempotencyClaim
|
||||
{
|
||||
public IdempotencyClaimState State { get; set; }
|
||||
public CommandResult Result { get; set; }
|
||||
}
|
||||
|
||||
public interface IIdempotencyStore
|
||||
{
|
||||
IdempotencyClaim Claim(
|
||||
string commandName,
|
||||
string idempotencyKey,
|
||||
CommandExecutionContext context,
|
||||
string inputFingerprint);
|
||||
void Complete(
|
||||
string commandName,
|
||||
string idempotencyKey,
|
||||
CommandExecutionContext context,
|
||||
string inputFingerprint,
|
||||
CommandResult result);
|
||||
void Abandon(
|
||||
string commandName,
|
||||
string idempotencyKey,
|
||||
CommandExecutionContext context,
|
||||
string inputFingerprint);
|
||||
}
|
||||
|
||||
public interface IConfirmationValidator
|
||||
{
|
||||
bool Validate(CommandPlan plan, CommandExecutionContext context, out string failureReason);
|
||||
}
|
||||
|
||||
public interface IConfirmationTokenIssuer
|
||||
{
|
||||
string Issue(CommandPlan plan, CommandExecutionContext context, TimeSpan lifetime);
|
||||
}
|
||||
|
||||
public sealed class CommandAuthorizationDecision
|
||||
{
|
||||
public static CommandAuthorizationDecision Allow()
|
||||
{
|
||||
return new CommandAuthorizationDecision { Allowed = true };
|
||||
}
|
||||
|
||||
public static CommandAuthorizationDecision Deny(string code, string message)
|
||||
{
|
||||
return new CommandAuthorizationDecision
|
||||
{
|
||||
Allowed = false,
|
||||
Code = code,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public bool Allowed { get; set; }
|
||||
public string Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public interface ICommandAuthorizer
|
||||
{
|
||||
CommandAuthorizationDecision Authorize(
|
||||
CommandDescriptor descriptor,
|
||||
CommandExecutionContext context,
|
||||
bool execution);
|
||||
}
|
||||
|
||||
public interface ISystemClock
|
||||
{
|
||||
DateTime UtcNow { get; }
|
||||
}
|
||||
|
||||
public sealed class CommandRegistry
|
||||
{
|
||||
private readonly object _syncRoot = new object();
|
||||
private readonly Dictionary<string, ICommandHandler> _handlers =
|
||||
new Dictionary<string, ICommandHandler>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void Register(ICommandHandler handler)
|
||||
{
|
||||
RegisterMany(new[] { handler });
|
||||
}
|
||||
|
||||
public void RegisterMany(IEnumerable<ICommandHandler> handlers)
|
||||
{
|
||||
if (handlers == null) throw new ArgumentNullException("handlers");
|
||||
List<ICommandHandler> items = handlers.ToList();
|
||||
if (items.Count == 0 || items.Any(handler => handler == null))
|
||||
{
|
||||
throw new ArgumentException("命令处理器不能为空。", "handlers");
|
||||
}
|
||||
List<ICommandHandler> registrations = items.Select(handler =>
|
||||
(ICommandHandler)new RegisteredCommandHandler(
|
||||
handler,
|
||||
CommandDescriptorContract.ValidateAndClone(handler.Descriptor)))
|
||||
.ToList();
|
||||
if (registrations.GroupBy(
|
||||
handler => handler.Descriptor.Name,
|
||||
StringComparer.OrdinalIgnoreCase)
|
||||
.Any(group => group.Count() > 1))
|
||||
{
|
||||
throw new InvalidOperationException("批量注册包含重复命令。");
|
||||
}
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
foreach (ICommandHandler handler in registrations)
|
||||
{
|
||||
if (_handlers.ContainsKey(handler.Descriptor.Name))
|
||||
throw new InvalidOperationException("命令已注册:" + handler.Descriptor.Name);
|
||||
}
|
||||
foreach (ICommandHandler handler in registrations)
|
||||
_handlers.Add(handler.Descriptor.Name, handler);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommandHandler Resolve(string commandName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandName)) return null;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
ICommandHandler handler;
|
||||
return _handlers.TryGetValue(commandName, out handler) ? handler : null;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<CommandDescriptor> List()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
List<CommandDescriptor> result = new List<CommandDescriptor>();
|
||||
foreach (ICommandHandler handler in _handlers.Values)
|
||||
result.Add(handler.Descriptor);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RegisteredCommandHandler :
|
||||
ICommandHandler,
|
||||
ICommandExecutionFollowupProvider
|
||||
{
|
||||
private readonly ICommandHandler _inner;
|
||||
private readonly CommandDescriptor _descriptor;
|
||||
|
||||
public RegisteredCommandHandler(
|
||||
ICommandHandler inner,
|
||||
CommandDescriptor descriptor)
|
||||
{
|
||||
_inner = inner;
|
||||
_descriptor = descriptor;
|
||||
}
|
||||
|
||||
public CommandDescriptor Descriptor
|
||||
{
|
||||
get { return CommandDescriptorContract.Clone(_descriptor); }
|
||||
}
|
||||
|
||||
public CommandPlan Plan(
|
||||
IDictionary<string, object> input,
|
||||
CommandExecutionContext context)
|
||||
{
|
||||
return _inner.Plan(input, context);
|
||||
}
|
||||
|
||||
public CommandResult Execute(
|
||||
CommandPlan plan,
|
||||
CommandExecutionContext context)
|
||||
{
|
||||
return _inner.Execute(plan, context);
|
||||
}
|
||||
|
||||
public bool TryCreateFollowup(
|
||||
CommandPlan completedPlan,
|
||||
CommandResult completedResult,
|
||||
CommandExecutionContext context,
|
||||
out CommandFollowupRequest followup)
|
||||
{
|
||||
ICommandExecutionFollowupProvider provider =
|
||||
_inner as ICommandExecutionFollowupProvider;
|
||||
if (provider == null)
|
||||
{
|
||||
followup = null;
|
||||
return false;
|
||||
}
|
||||
return provider.TryCreateFollowup(
|
||||
completedPlan,
|
||||
completedResult,
|
||||
context,
|
||||
out followup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user