659 lines
26 KiB
C#
659 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace Lskj.CommandKernel
|
|
{
|
|
public sealed class DynamicModuleLookupCandidate
|
|
{
|
|
public string Value { get; set; }
|
|
public string Display { get; set; }
|
|
}
|
|
|
|
public sealed class DynamicModuleLookupContextValue
|
|
{
|
|
public string ParameterId { get; set; }
|
|
public string Scope { get; set; }
|
|
public int? RowNumber { get; set; }
|
|
public string Value { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 传给客户只读 Lookup 适配器的固定请求。只包含不透明参数 ID、配置
|
|
/// 指纹、当前行上下文和用户原文,不包含表、列、SQL 或过程名。
|
|
/// </summary>
|
|
public sealed class DynamicModuleLookupRequest
|
|
{
|
|
public DynamicModuleLookupRequest()
|
|
{
|
|
ContextValues = new List<DynamicModuleLookupContextValue>();
|
|
}
|
|
|
|
public string ModuleCode { get; set; }
|
|
public string ModuleKind { get; set; }
|
|
public string ContractFingerprint { get; set; }
|
|
public string ConfigurationFingerprint { get; set; }
|
|
public string FieldConfigurationFingerprint { get; set; }
|
|
public string ParameterId { get; set; }
|
|
public string Scope { get; set; }
|
|
public int? RowNumber { get; set; }
|
|
public string Reference { get; set; }
|
|
public IList<DynamicModuleLookupContextValue> ContextValues
|
|
{ get; private set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 客户实现必须走固定、参数化、只读且经过验收的查询入口;禁止执行
|
|
/// FieldSpec 中的原始 Lookup SQL,也禁止让模型提供表、列或过程名。
|
|
/// </summary>
|
|
public interface IDynamicModuleLookupResolver
|
|
{
|
|
IList<DynamicModuleLookupCandidate> Resolve(
|
|
DynamicModuleLookupRequest request,
|
|
CommandExecutionContext context);
|
|
}
|
|
|
|
public interface IDynamicModuleLookupProofService
|
|
{
|
|
string Issue(
|
|
JObject resolvedInput,
|
|
ModuleInspection inspection,
|
|
CommandExecutionContext context,
|
|
TimeSpan lifetime);
|
|
bool Validate(
|
|
string proof,
|
|
JObject resolvedInput,
|
|
ModuleInspection inspection,
|
|
CommandExecutionContext context);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 把服务端唯一解析后的完整输入绑定到当前用户、账套、子系统、模块和
|
|
/// 私有低代码配置。凭证不授予写权限,也不能替代原生确认或幂等键。
|
|
/// </summary>
|
|
public sealed class HmacDynamicModuleLookupProofService :
|
|
IDynamicModuleLookupProofService
|
|
{
|
|
private const string Version = "mlp1";
|
|
private readonly byte[] _secret;
|
|
private readonly ISystemClock _clock;
|
|
|
|
public HmacDynamicModuleLookupProofService(
|
|
byte[] secret,
|
|
ISystemClock clock)
|
|
{
|
|
if (secret == null || secret.Length < 32)
|
|
throw new ArgumentException(
|
|
"Lookup 解析凭证密钥至少需要 32 字节。",
|
|
"secret");
|
|
if (clock == null) throw new ArgumentNullException("clock");
|
|
_secret = (byte[])secret.Clone();
|
|
_clock = clock;
|
|
}
|
|
|
|
public static HmacDynamicModuleLookupProofService Create(
|
|
ISystemClock clock)
|
|
{
|
|
byte[] secret = new byte[32];
|
|
using (RandomNumberGenerator random = RandomNumberGenerator.Create())
|
|
random.GetBytes(secret);
|
|
return new HmacDynamicModuleLookupProofService(secret, clock);
|
|
}
|
|
|
|
public string Issue(
|
|
JObject resolvedInput,
|
|
ModuleInspection inspection,
|
|
CommandExecutionContext context,
|
|
TimeSpan lifetime)
|
|
{
|
|
ValidateArguments(resolvedInput, inspection, context);
|
|
if (lifetime <= TimeSpan.Zero || lifetime > TimeSpan.FromMinutes(10))
|
|
throw new ArgumentOutOfRangeException(
|
|
"lifetime",
|
|
"Lookup 解析凭证有效期必须在 10 分钟以内。");
|
|
long ticks = _clock.UtcNow.Add(lifetime).ToUniversalTime().Ticks;
|
|
byte[] nonceBytes = new byte[16];
|
|
using (RandomNumberGenerator random = RandomNumberGenerator.Create())
|
|
random.GetBytes(nonceBytes);
|
|
string nonce = Hex(nonceBytes);
|
|
string fingerprint = Fingerprint(
|
|
resolvedInput,
|
|
inspection,
|
|
context);
|
|
string payload = Payload(ticks, nonce, fingerprint);
|
|
return Version + "."
|
|
+ ticks.ToString(CultureInfo.InvariantCulture) + "."
|
|
+ nonce + "." + fingerprint + "." + Sign(payload);
|
|
}
|
|
|
|
public bool Validate(
|
|
string proof,
|
|
JObject resolvedInput,
|
|
ModuleInspection inspection,
|
|
CommandExecutionContext context)
|
|
{
|
|
try
|
|
{
|
|
ValidateArguments(resolvedInput, inspection, context);
|
|
string[] parts = (proof ?? string.Empty).Split('.');
|
|
long ticks;
|
|
if (parts.Length != 5 || parts[0] != Version
|
|
|| !long.TryParse(
|
|
parts[1],
|
|
NumberStyles.None,
|
|
CultureInfo.InvariantCulture,
|
|
out ticks)
|
|
|| parts[2].Length != 32
|
|
|| !parts[2].All(IsLowerHex)
|
|
|| !CommandInputFingerprint.IsValid(parts[3])
|
|
|| parts[4].Length < 40 || parts[4].Length > 64)
|
|
return false;
|
|
DateTime expiresAt = new DateTime(ticks, DateTimeKind.Utc);
|
|
DateTime now = _clock.UtcNow.ToUniversalTime();
|
|
if (expiresAt <= now || expiresAt > now.AddMinutes(10))
|
|
return false;
|
|
string fingerprint = Fingerprint(
|
|
resolvedInput,
|
|
inspection,
|
|
context);
|
|
if (!FixedEquals(parts[3], fingerprint)) return false;
|
|
return FixedEquals(
|
|
parts[4],
|
|
Sign(Payload(ticks, parts[2], fingerprint)));
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string Fingerprint(
|
|
JObject resolvedInput,
|
|
ModuleInspection inspection,
|
|
CommandExecutionContext context)
|
|
{
|
|
JObject exactInput = (JObject)resolvedInput.DeepClone();
|
|
exactInput.Remove("lookupResolutionProof");
|
|
return CommandInputFingerprint.Create(
|
|
"module.lookup-resolution-proof",
|
|
new Dictionary<string, object>
|
|
{
|
|
{ "moduleCode", inspection.ModuleCode },
|
|
{ "configurationFingerprint",
|
|
ModuleInspector.PrivateConfigurationFingerprint(
|
|
inspection) },
|
|
{ "accountBook", context.AccountBook.Trim() },
|
|
{ "subSystemId", context.SubSystemId.Trim() },
|
|
{ "userId", context.UserId.Trim() },
|
|
{ "userName", context.UserName.Trim() },
|
|
{ "databaseScopeFingerprint",
|
|
context.DatabaseScopeFingerprint.Trim() },
|
|
{ "resolvedInput", exactInput }
|
|
});
|
|
}
|
|
|
|
private static void ValidateArguments(
|
|
JObject resolvedInput,
|
|
ModuleInspection inspection,
|
|
CommandExecutionContext context)
|
|
{
|
|
if (resolvedInput == null)
|
|
throw new ArgumentNullException("resolvedInput");
|
|
if (inspection == null)
|
|
throw new ArgumentNullException("inspection");
|
|
if (context == null
|
|
|| string.IsNullOrWhiteSpace(context.AccountBook)
|
|
|| string.IsNullOrWhiteSpace(context.SubSystemId)
|
|
|| string.IsNullOrWhiteSpace(context.UserId)
|
|
|| string.IsNullOrWhiteSpace(context.UserName)
|
|
|| !CommandInputFingerprint.IsValid(
|
|
context.DatabaseScopeFingerprint))
|
|
throw new ArgumentException(
|
|
"Lookup 解析凭证需要完整 ERP 作用域。",
|
|
"context");
|
|
}
|
|
|
|
private string Sign(string payload)
|
|
{
|
|
using (HMACSHA256 hmac = new HMACSHA256(_secret))
|
|
{
|
|
string value = Convert.ToBase64String(
|
|
hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)));
|
|
return value.TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
|
}
|
|
}
|
|
|
|
private static string Payload(
|
|
long ticks,
|
|
string nonce,
|
|
string fingerprint)
|
|
{
|
|
return Version + "\u001f"
|
|
+ ticks.ToString(CultureInfo.InvariantCulture) + "\u001f"
|
|
+ nonce + "\u001f" + fingerprint;
|
|
}
|
|
|
|
private static string Hex(byte[] value)
|
|
{
|
|
StringBuilder result = new StringBuilder(value.Length * 2);
|
|
foreach (byte item in value)
|
|
result.Append(item.ToString("x2", CultureInfo.InvariantCulture));
|
|
return result.ToString();
|
|
}
|
|
|
|
private static bool IsLowerHex(char value)
|
|
{
|
|
return (value >= '0' && value <= '9')
|
|
|| (value >= 'a' && value <= 'f');
|
|
}
|
|
|
|
private static bool FixedEquals(string left, string right)
|
|
{
|
|
byte[] first = Encoding.ASCII.GetBytes(left ?? string.Empty);
|
|
byte[] second = Encoding.ASCII.GetBytes(right ?? string.Empty);
|
|
int difference = first.Length ^ second.Length;
|
|
int count = Math.Max(first.Length, second.Length);
|
|
for (int index = 0; index < count; index++)
|
|
{
|
|
byte a = index < first.Length ? first[index] : (byte)0;
|
|
byte b = index < second.Length ? second[index] : (byte)0;
|
|
difference |= a ^ b;
|
|
}
|
|
return difference == 0;
|
|
}
|
|
}
|
|
|
|
public static class DynamicModuleLookupResolution
|
|
{
|
|
private const int MaximumCandidates = 20;
|
|
private const int MaximumCandidateValueCharacters = 2048;
|
|
private const int MaximumCandidateDisplayCharacters = 512;
|
|
|
|
public static CommandPlan ResolveCreate(
|
|
ModuleInspection inspection,
|
|
string menuName,
|
|
CommandExecutionContext context,
|
|
IDictionary<string, object> input,
|
|
IDynamicModuleLookupResolver resolver,
|
|
IDynamicModuleLookupProofService proofs,
|
|
string resolvedCommand = "module.record.prepare-create")
|
|
{
|
|
if (inspection == null) throw new ArgumentNullException("inspection");
|
|
if (context == null) throw new ArgumentNullException("context");
|
|
if (resolver == null) throw new ArgumentNullException("resolver");
|
|
if (proofs == null) throw new ArgumentNullException("proofs");
|
|
if (resolvedCommand != "module.record.prepare-create"
|
|
&& resolvedCommand != "module.record.create")
|
|
throw new ArgumentException(
|
|
"动态 Lookup 后续命令必须是固定的预演或写计划命令。",
|
|
"resolvedCommand");
|
|
|
|
JObject source;
|
|
try
|
|
{
|
|
source = JObject.FromObject(
|
|
input ?? new Dictionary<string, object>());
|
|
}
|
|
catch
|
|
{
|
|
throw Error(
|
|
"module_parameter_input_invalid",
|
|
"模块 Lookup 解析输入不是有效 JSON 对象。",
|
|
2);
|
|
}
|
|
if (source.Property(
|
|
"lookupResolutionProof",
|
|
StringComparison.Ordinal) != null)
|
|
{
|
|
throw Error(
|
|
"module_lookup_resolution_proof_unexpected",
|
|
"Lookup 解析命令不能接受调用方提供的解析凭证。",
|
|
2);
|
|
}
|
|
|
|
CommandPlan initial = DynamicModuleOperationPlanner.PrepareCreate(
|
|
inspection,
|
|
menuName,
|
|
context,
|
|
input);
|
|
IList<object> initialIssues = (IList<object>)initial.Data["issues"];
|
|
if (initialIssues.Any(item => !string.Equals(
|
|
IssueCode(item),
|
|
"module_parameter_lookup_resolution_required",
|
|
StringComparison.Ordinal)))
|
|
{
|
|
initial.Data["title"] = "低代码模块 Lookup 解析前校验";
|
|
initial.Data["outcomeCode"] =
|
|
"module_lookup_resolution_input_invalid";
|
|
initial.Data["requiresFollowupPlan"] = false;
|
|
return initial;
|
|
}
|
|
|
|
IDictionary<string, object> contract =
|
|
inspection.ToParameterContract(menuName, context);
|
|
string contractFingerprint = Convert.ToString(
|
|
contract["contractFingerprint"]);
|
|
string configurationFingerprint = Convert.ToString(
|
|
contract["configurationFingerprint"]);
|
|
Dictionary<string, FieldSpec> master = LookupFields(
|
|
inspection.MasterFields,
|
|
"master");
|
|
Dictionary<string, FieldSpec> detail = LookupFields(
|
|
inspection.DetailFields,
|
|
"detail");
|
|
List<object> resolutionIssues = new List<object>();
|
|
List<object> resolvedLookups = new List<object>();
|
|
|
|
ResolveEntries(
|
|
source,
|
|
(JArray)source["masterValues"],
|
|
master,
|
|
"master",
|
|
null,
|
|
inspection,
|
|
context,
|
|
contractFingerprint,
|
|
configurationFingerprint,
|
|
resolver,
|
|
resolutionIssues,
|
|
resolvedLookups);
|
|
JArray detailRows = (JArray)source["detailRows"];
|
|
for (int index = 0; index < detailRows.Count; index++)
|
|
{
|
|
JObject row = (JObject)detailRows[index];
|
|
ResolveEntries(
|
|
source,
|
|
(JArray)row["values"],
|
|
detail,
|
|
"detail",
|
|
index + 1,
|
|
inspection,
|
|
context,
|
|
contractFingerprint,
|
|
configurationFingerprint,
|
|
resolver,
|
|
resolutionIssues,
|
|
resolvedLookups);
|
|
}
|
|
|
|
if (resolvedLookups.Count == 0 && resolutionIssues.Count == 0)
|
|
{
|
|
initial.Valid = false;
|
|
initial.Data["outcomeCode"] =
|
|
"module_lookup_resolution_not_required";
|
|
initial.Data["issues"] = new List<object>
|
|
{
|
|
new Dictionary<string, object>
|
|
{
|
|
{ "code", "module_lookup_resolution_not_required" }
|
|
}
|
|
};
|
|
initial.Data["requiresFollowupPlan"] = false;
|
|
return initial;
|
|
}
|
|
if (resolutionIssues.Count != 0)
|
|
{
|
|
initial.Valid = false;
|
|
initial.Data["outcomeCode"] =
|
|
"module_lookup_resolution_incomplete";
|
|
initial.Data["issues"] = resolutionIssues;
|
|
initial.Data["resolvedLookups"] = resolvedLookups;
|
|
initial.Data["requiresFollowupPlan"] = false;
|
|
return initial;
|
|
}
|
|
|
|
source["lookupResolutionProof"] = proofs.Issue(
|
|
source,
|
|
inspection,
|
|
context,
|
|
TimeSpan.FromMinutes(5));
|
|
IDictionary<string, object> resolvedInput = source.ToObject<
|
|
Dictionary<string, object>>();
|
|
CommandPlan resolved = DynamicModuleOperationPlanner.PrepareCreate(
|
|
inspection,
|
|
menuName,
|
|
context,
|
|
resolvedInput,
|
|
proofs);
|
|
if (!resolved.Valid)
|
|
{
|
|
throw Error(
|
|
"module_lookup_resolver_contract_invalid",
|
|
"Lookup 解析结果不符合当前模块参数合同。",
|
|
6);
|
|
}
|
|
resolved.Data["title"] = "低代码模块 Lookup 唯一解析结果";
|
|
resolved.Data["outcomeCode"] = "module_lookup_resolution_ready";
|
|
resolved.Data["resolvedLookups"] = resolvedLookups;
|
|
resolved.Data["requiresFollowupPlan"] = true;
|
|
resolved.Data["resolvedCommand"] =
|
|
resolvedCommand;
|
|
resolved.Data["fallbackResolvedCommand"] =
|
|
"module.record.prepare-create";
|
|
resolved.Data["resolvedInput"] = resolvedInput;
|
|
return resolved;
|
|
}
|
|
|
|
private static void ResolveEntries(
|
|
JObject fullInput,
|
|
JArray entries,
|
|
IDictionary<string, FieldSpec> lookupFields,
|
|
string scope,
|
|
int? rowNumber,
|
|
ModuleInspection inspection,
|
|
CommandExecutionContext context,
|
|
string contractFingerprint,
|
|
string configurationFingerprint,
|
|
IDynamicModuleLookupResolver resolver,
|
|
IList<object> issues,
|
|
IList<object> resolved)
|
|
{
|
|
foreach (JObject entry in entries.Cast<JObject>())
|
|
{
|
|
string parameterId = entry.Value<string>("parameterId");
|
|
string reference = entry.Value<string>("value") ?? string.Empty;
|
|
FieldSpec field;
|
|
if (reference.Length == 0
|
|
|| !lookupFields.TryGetValue(parameterId, out field))
|
|
continue;
|
|
if (!CommandInputFingerprint.IsValid(
|
|
field.PrivateConfigurationFingerprint))
|
|
{
|
|
throw Error(
|
|
"module_lookup_configuration_unbound",
|
|
"Lookup 字段缺少当前数据库配置指纹,不能进入只读解析适配器。",
|
|
6);
|
|
}
|
|
DynamicModuleLookupRequest request =
|
|
new DynamicModuleLookupRequest
|
|
{
|
|
ModuleCode = inspection.ModuleCode,
|
|
ModuleKind = inspection.Kind,
|
|
ContractFingerprint = contractFingerprint,
|
|
ConfigurationFingerprint = configurationFingerprint,
|
|
FieldConfigurationFingerprint =
|
|
field.PrivateConfigurationFingerprint,
|
|
ParameterId = parameterId,
|
|
Scope = scope,
|
|
RowNumber = rowNumber,
|
|
Reference = reference
|
|
};
|
|
foreach (DynamicModuleLookupContextValue value in
|
|
ContextValues(fullInput, rowNumber))
|
|
request.ContextValues.Add(value);
|
|
|
|
IList<DynamicModuleLookupCandidate> candidates;
|
|
try
|
|
{
|
|
candidates = ValidateCandidates(
|
|
resolver.Resolve(request, context));
|
|
}
|
|
catch (CommandKernelException) { throw; }
|
|
catch
|
|
{
|
|
throw Error(
|
|
"module_lookup_resolver_failed",
|
|
"客户 Lookup 只读适配器执行失败。",
|
|
6);
|
|
}
|
|
if (candidates.Count != 1)
|
|
{
|
|
issues.Add(new Dictionary<string, object>
|
|
{
|
|
{ "code", candidates.Count == 0
|
|
? "module_parameter_lookup_not_resolved"
|
|
: "module_parameter_lookup_ambiguous" },
|
|
{ "parameterId", parameterId },
|
|
{ "scope", scope },
|
|
{ "rowNumber", rowNumber },
|
|
{ "candidates", candidates.Select(
|
|
(item, index) => (object)new Dictionary<string, object>
|
|
{
|
|
{ "choiceNumber", index + 1 },
|
|
{ "display", item.Display }
|
|
}).ToList() }
|
|
});
|
|
continue;
|
|
}
|
|
DynamicModuleLookupCandidate candidate = candidates[0];
|
|
entry["value"] = candidate.Value;
|
|
resolved.Add(new Dictionary<string, object>
|
|
{
|
|
{ "parameterId", parameterId },
|
|
{ "scope", scope },
|
|
{ "rowNumber", rowNumber },
|
|
{ "display", candidate.Display }
|
|
});
|
|
}
|
|
}
|
|
|
|
private static IList<DynamicModuleLookupContextValue> ContextValues(
|
|
JObject source,
|
|
int? rowNumber)
|
|
{
|
|
List<DynamicModuleLookupContextValue> result =
|
|
new List<DynamicModuleLookupContextValue>();
|
|
AddContextValues(
|
|
result,
|
|
(JArray)source["masterValues"],
|
|
"master",
|
|
null);
|
|
if (rowNumber.HasValue)
|
|
{
|
|
JArray rows = (JArray)source["detailRows"];
|
|
JObject row = (JObject)rows[rowNumber.Value - 1];
|
|
AddContextValues(
|
|
result,
|
|
(JArray)row["values"],
|
|
"detail",
|
|
rowNumber);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static void AddContextValues(
|
|
IList<DynamicModuleLookupContextValue> target,
|
|
JArray entries,
|
|
string scope,
|
|
int? rowNumber)
|
|
{
|
|
foreach (JObject entry in entries.Cast<JObject>())
|
|
{
|
|
target.Add(new DynamicModuleLookupContextValue
|
|
{
|
|
ParameterId = entry.Value<string>("parameterId"),
|
|
Scope = scope,
|
|
RowNumber = rowNumber,
|
|
Value = entry.Value<string>("value") ?? string.Empty
|
|
});
|
|
}
|
|
}
|
|
|
|
private static Dictionary<string, FieldSpec> LookupFields(
|
|
IEnumerable<FieldSpec> fields,
|
|
string scope)
|
|
{
|
|
Dictionary<string, FieldSpec> result =
|
|
new Dictionary<string, FieldSpec>(StringComparer.Ordinal);
|
|
foreach (FieldSpec field in fields ?? Enumerable.Empty<FieldSpec>())
|
|
{
|
|
if (field == null || !field.Exposed
|
|
|| !ModuleInspection.FieldRequiresLookup(field))
|
|
continue;
|
|
string parameterId = ModuleInspection.ParameterId(
|
|
scope,
|
|
field.Name);
|
|
if (result.ContainsKey(parameterId))
|
|
throw Error(
|
|
"module_parameter_contract_invalid",
|
|
"模块 Lookup 参数合同存在冲突。",
|
|
6);
|
|
result.Add(parameterId, field);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static IList<DynamicModuleLookupCandidate> ValidateCandidates(
|
|
IList<DynamicModuleLookupCandidate> candidates)
|
|
{
|
|
if (candidates == null || candidates.Count > MaximumCandidates)
|
|
throw ResolverProtocol();
|
|
List<DynamicModuleLookupCandidate> result =
|
|
new List<DynamicModuleLookupCandidate>();
|
|
HashSet<string> values = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (DynamicModuleLookupCandidate candidate in candidates)
|
|
{
|
|
if (candidate == null
|
|
|| !Clean(candidate.Value, MaximumCandidateValueCharacters)
|
|
|| !Clean(candidate.Display, MaximumCandidateDisplayCharacters)
|
|
|| !values.Add(candidate.Value))
|
|
throw ResolverProtocol();
|
|
result.Add(new DynamicModuleLookupCandidate
|
|
{
|
|
Value = candidate.Value,
|
|
Display = candidate.Display
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static bool Clean(string value, int maximum)
|
|
{
|
|
return !string.IsNullOrWhiteSpace(value)
|
|
&& value.Length <= maximum
|
|
&& !value.Any(char.IsControl)
|
|
&& string.Equals(value, value.Trim(), StringComparison.Ordinal);
|
|
}
|
|
|
|
private static string IssueCode(object issue)
|
|
{
|
|
IDictionary<string, object> value =
|
|
issue as IDictionary<string, object>;
|
|
object code;
|
|
return value != null && value.TryGetValue("code", out code)
|
|
? Convert.ToString(code)
|
|
: string.Empty;
|
|
}
|
|
|
|
private static CommandKernelException ResolverProtocol()
|
|
{
|
|
return Error(
|
|
"module_lookup_resolver_contract_invalid",
|
|
"客户 Lookup 适配器返回了重复、越界或不完整的候选。",
|
|
6);
|
|
}
|
|
|
|
private static CommandKernelException Error(
|
|
string code,
|
|
string message,
|
|
int exitCode)
|
|
{
|
|
return new CommandKernelException(code, message, exitCode);
|
|
}
|
|
}
|
|
}
|