using System; using System.Collections.Generic; using System.Globalization; using System.Security.Cryptography; using System.Text; using Lskj.CommandKernel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Lskj.AgentBridge { public sealed class BridgeUatGrant { [JsonProperty("authorizationId")] public string AuthorizationId { get; set; } [JsonProperty("caseCode")] public string CaseCode { get; set; } [JsonProperty("token")] public string Token { get; set; } } public sealed class BridgeRequest { public BridgeRequest() { ProtocolVersion = "1.0"; Payload = new JObject(); } [JsonProperty("protocolVersion")] public string ProtocolVersion { get; set; } [JsonProperty("requestId")] public string RequestId { get; set; } [JsonProperty("correlationId")] public string CorrelationId { get; set; } [JsonProperty("clientSessionId")] public string ClientSessionId { get; set; } [JsonProperty("sessionScopeToken")] public string SessionScopeToken { get; set; } [JsonProperty("method")] public string Method { get; set; } [JsonProperty("payload")] public JObject Payload { get; set; } [JsonProperty("uatGrant")] public BridgeUatGrant UatGrant { get; set; } // Set by the named-pipe transport after GetNamedPipeClientProcessId. // It is never accepted from JSON and never echoed in a response. [JsonIgnore] public int TransportClientProcessId { get; set; } } /// /// 由当前 ERP 权威会话重算的 v3 作用域令牌。令牌不是认证秘密;它的 /// 作用是让服务端证明调用方明确绑定了数据库、用户、账套、子系统和 /// 管理员身份,而不是只依赖客户端在调用前后的自检。 /// public static class BridgeSessionScopeToken { public const string Domain = "lserp-pet-session-scope-v3\n"; public static string Compute(CommandExecutionContext context) { if (context == null) throw Unavailable(); string database = NormalizeDatabase( context.DatabaseScopeFingerprint); string userId = ScopeText(context.UserId); string userName = ScopeText(context.UserName); string accountBook = ScopeText(context.AccountBook); string subSystemId = ScopeText(context.SubSystemId); string administrator = AdministratorIdentity.IsBuiltIn( userId, userName) ? "true" : "false"; StringBuilder canonical = new StringBuilder(Domain); Append(canonical, "databaseScopeFingerprint", database); Append(canonical, "userId", userId); Append(canonical, "userName", userName); Append(canonical, "accountBook", accountBook); Append(canonical, "subSystemId", subSystemId); Append(canonical, "isAdministrator", administrator); using (SHA256 algorithm = SHA256.Create()) { byte[] digest = algorithm.ComputeHash( Encoding.UTF8.GetBytes(canonical.ToString())); StringBuilder token = new StringBuilder(32); for (int index = 0; index < 16; index++) token.Append(digest[index].ToString("x2", CultureInfo.InvariantCulture)); return token.ToString(); } } public static bool IsValid(string value) { if (string.IsNullOrEmpty(value) || value.Length != 32) return false; for (int index = 0; index < value.Length; index++) { char item = value[index]; if (!((item >= '0' && item <= '9') || (item >= 'a' && item <= 'f'))) return false; } return true; } public static bool Matches(string expected, CommandExecutionContext context) { if (!IsValid(expected)) return false; string actual = Compute(context); int difference = 0; for (int index = 0; index < actual.Length; index++) difference |= actual[index] ^ expected[index]; return difference == 0; } private static void Append( StringBuilder target, string name, string value) { target.Append(name) .Append('=') .Append(Encoding.UTF8.GetByteCount(value).ToString( CultureInfo.InvariantCulture)) .Append(':') .Append(value) .Append('\n'); } private static string NormalizeDatabase(string value) { string normalized = (value ?? string.Empty).ToLowerInvariant(); if (normalized.Length != 64 || !IsLowerHex(normalized)) throw Unavailable(); return normalized; } private static bool IsLowerHex(string value) { for (int index = 0; index < value.Length; index++) { char item = value[index]; if (!((item >= '0' && item <= '9') || (item >= 'a' && item <= 'f'))) return false; } return true; } private static string ScopeText(string value) { if (string.IsNullOrWhiteSpace(value) || value.Length > 256 || !string.Equals(value, value.Trim(), StringComparison.Ordinal)) throw Unavailable(); foreach (char item in value) if (char.IsControl(item)) throw Unavailable(); return value; } private static CommandKernelException Unavailable() { return new CommandKernelException( "erp_session_scope_unavailable", "当前 ERP 会话缺少数据库、用户、账套、子系统或权限范围。", 8); } } public sealed class BridgeResponse { public BridgeResponse() { ProtocolVersion = "1.0"; Data = new JObject(); } [JsonProperty("protocolVersion")] public string ProtocolVersion { get; set; } [JsonProperty("requestId")] public string RequestId { get; set; } [JsonProperty("correlationId")] public string CorrelationId { get; set; } [JsonProperty("success")] public bool Success { get; set; } [JsonProperty("code")] public string Code { get; set; } [JsonProperty("message")] public string Message { get; set; } [JsonProperty("data")] public JObject Data { get; set; } public static BridgeResponse Ok(BridgeRequest request, object data) { return new BridgeResponse { RequestId = request == null ? null : SafeEcho(request.RequestId), CorrelationId = request == null ? null : SafeEcho(request.CorrelationId), Success = true, Code = "ok", Data = data == null ? new JObject() : JObject.FromObject(data) }; } public static BridgeResponse Error( BridgeRequest request, string code, string message) { string stableCode = string.IsNullOrWhiteSpace(code) ? "bridge_error" : code; return new BridgeResponse { RequestId = request == null ? null : SafeEcho(request.RequestId), CorrelationId = request == null ? null : SafeEcho(request.CorrelationId), Success = false, Code = stableCode, Message = message, Data = BridgeErrorRecoveryContract.Project(stableCode) }; } private static string SafeEcho(string value) { if (string.IsNullOrWhiteSpace(value) || value.Length < 8 || value.Length > 128) return null; foreach (char item in value) { if ((item >= 'a' && item <= 'z') || (item >= 'A' && item <= 'Z') || (item >= '0' && item <= '9') || item == '.' || item == '_' || item == ':' || item == '-') continue; return null; } return value; } } public static class BridgeErrorRecoveryContract { public static JObject Project(string code) { BridgeErrorRecovery recovery = Resolve(code); return new JObject { ["recovery"] = new JObject { ["action"] = recovery.Action, ["retryable"] = recovery.Retryable, ["planInvalidated"] = recovery.PlanInvalidated, ["message"] = recovery.Message } }; } public static bool InvalidatesPlan(string code) { return Resolve(code).PlanInvalidated; } private static BridgeErrorRecovery Resolve(string code) { string value = (code ?? string.Empty).Trim().ToLowerInvariant(); if (value == "user_cancelled") { return Recovery( "review_and_retry", true, false, "本次没有写入;核对原预览后可再次确认执行。"); } if (value == "bridge_timeout" || value == "workflow_database_error") { return Recovery( "reconcile_execution", true, false, "先检查 ERP 确认窗口和审计记录;无法确认结果时使用原预览重试,系统会复用幂等键。"); } if (value == "bridge_unavailable" || value == "erp_bridge_not_running" || value == "erp_bridge_ambiguous" || value == "erp_bridge_target_not_running" || value == "bridge_session_scope_token_required" || value == "bridge_session_scope_token_invalid" || value == "erp_session_scope_unavailable" || value == "erp_session_scope_mismatch" || value == "erp_session_scope_changed") { return Recovery( "restart_erp_pet", false, true, "确认目标 ERP 登录范围后,从该 ERP 重新启动桌宠并重新生成预览。"); } if (value == "duplicate_invoice") { return Recovery( "inspect_existing_record", false, true, "先查询该供应商的现有发票;确认不是重复单据后再重新生成预览。"); } if (IsStalePlan(value)) { return Recovery( "replan", false, true, "当前预览依据已失效;重新读取 ERP 实时数据、生成新预览并再次核对。"); } if (IsInvalidRequest(value)) { return Recovery( "correct_request", false, true, "补充或修正业务输入后重新生成预览,不要直接重复执行旧计划。"); } if (IsCapacityOrTransientPolicy(value)) { return Recovery( "wait_and_retry", true, false, "等待当前受控任务释放后,使用原预览和同一幂等键重试。"); } if (IsAdministratorAction(value)) { return Recovery( "contact_administrator", false, true, "请管理员检查当前登录权限、客户适配器激活状态和验收凭证后重新生成预览。"); } return Recovery( "contact_support", false, true, "不要重复执行旧计划;请使用关联 ID 查询审计记录后再重新发起。"); } private static bool IsStalePlan(string code) { return code == "plan_not_found" || code == "plan_expired" || code == "plan_not_executable" || code == "command_not_found" || code == "plan_owner_mismatch" || code == "plan_correlation_mismatch" || code == "untrusted_plan" || code.EndsWith("_changed", StringComparison.Ordinal) || code.EndsWith("_expired", StringComparison.Ordinal) || code.Contains("_proof_expired") || code.Contains("_proof_invalid"); } private static bool IsInvalidRequest(string code) { return code == "invalid_input" || code == "invalid_request" || code == "input_schema_violation" || code == "module_not_found" || code.EndsWith("_input_invalid", StringComparison.Ordinal) || code.StartsWith("invalid_", StringComparison.Ordinal); } private static bool IsCapacityOrTransientPolicy(string code) { return code.Contains("rate_limit") || code.Contains("capacity_exceeded") || code.Contains("circuit_open") || code.Contains("temporarily_unavailable"); } private static bool IsAdministratorAction(string code) { return code.Contains("permission") || code.Contains("authorization") || code.Contains("acceptance") || code.Contains("adapter_") || code.Contains("_readiness") || code.Contains("_not_ready") || code.Contains("_policy_") || code.StartsWith("workflow_uat_", StringComparison.Ordinal) || code.StartsWith("erp_database_", StringComparison.Ordinal) || code == "execution_context_required"; } private static BridgeErrorRecovery Recovery( string action, bool retryable, bool planInvalidated, string message) { return new BridgeErrorRecovery { Action = action, Retryable = retryable, PlanInvalidated = planInvalidated, Message = message }; } private sealed class BridgeErrorRecovery { public string Action { get; set; } public bool Retryable { get; set; } public bool PlanInvalidated { get; set; } public string Message { get; set; } } } public interface IAgentBridgeRuntime { BridgeResponse Handle(BridgeRequest request); } public interface IBridgeOperationalAuditSink { void RecordOperationalEvent( string eventName, string method, string commandName, string stage, string outcomeCode, CommandExecutionContext context); } public sealed class NullBridgeOperationalAuditSink : IBridgeOperationalAuditSink { public void RecordOperationalEvent( string eventName, string method, string commandName, string stage, string outcomeCode, CommandExecutionContext context) { } } public interface IBridgeExecutionContextFactory { CommandExecutionContext Create(BridgeRequest request); } public interface IBridgeContextProvider { IDictionary Snapshot(CommandExecutionContext context); } public interface ICommandConfirmationPrompt { bool Confirm(CommandDescriptor descriptor, CommandPlan plan, CommandExecutionContext context); } public interface IServerPlanStore { void Save(CommandPlan plan); bool TryGet(string planId, out CommandPlan plan); void Remove(string planId); void PurgeExpired(DateTime utcNow); } }