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);
}
}