Merge pull request '20261028' (#16) from test into master
All checks were successful
Build and Deploy / deploy (push) Successful in 2m0s
All checks were successful
Build and Deploy / deploy (push) Successful in 2m0s
Reviewed-on: #16
This commit is contained in:
commit
46455b2ae9
@ -16,6 +16,9 @@ public class AiProviderOutput
|
||||
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认模型名(历史字段名 ApiSecret,实际不当密钥脱敏)。
|
||||
/// </summary>
|
||||
public string? ApiSecret { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
@ -39,6 +42,9 @@ public class AiProviderSaveInput
|
||||
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认文字模型 ID;空表示保持原值。
|
||||
/// </summary>
|
||||
public string? ApiSecret { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
@ -40,20 +40,16 @@ public static class McpToolCatalog
|
||||
new(
|
||||
ChannelGuide,
|
||||
"查渠道对客说明",
|
||||
"按销售产品代码或名称查询对客说明,判断客户品名是否可承运(禁运/可寄)。",
|
||||
"客户问某品名能不能寄、走什么渠道时调用。必填 cargoName;productCode/productName 可选(已知渠道时传入)。仅品名时按对客说明检索可走渠道。",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"productCode": { "type": "string", "description": "销售产品代码,优先使用" },
|
||||
"productName": { "type": "string", "description": "销售产品名称关键词(无代码时用)" },
|
||||
"cargoName": { "type": "string", "description": "客户品名,用于对照说明判断可否承运" }
|
||||
"productCode": { "type": "string", "description": "销售产品代码,可选;客户已指定渠道时优先" },
|
||||
"productName": { "type": "string", "description": "销售产品/渠道名称关键词,可选" },
|
||||
"cargoName": { "type": "string", "description": "客户品名,如 lipstick、化妆品;必填" }
|
||||
},
|
||||
"required": ["cargoName"],
|
||||
"anyOf": [
|
||||
{ "required": ["productCode"] },
|
||||
{ "required": ["productName"] }
|
||||
]
|
||||
"required": ["cargoName"]
|
||||
}
|
||||
""",
|
||||
20),
|
||||
|
||||
@ -153,6 +153,13 @@ public class McpToolExecutor(
|
||||
var productName = GetString(root, "productName", "ProductName");
|
||||
var cargoName = GetString(root, "cargoName", "CargoName");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cargoName)
|
||||
&& string.IsNullOrWhiteSpace(productCode)
|
||||
&& string.IsNullOrWhiteSpace(productName))
|
||||
{
|
||||
return (false, "请至少提供 cargoName(客户品名),或 productCode / productName");
|
||||
}
|
||||
|
||||
var products = new List<Oms.Domain.ChannelsAggregate.Entities.SalesProduct>();
|
||||
if (!string.IsNullOrWhiteSpace(productCode))
|
||||
{
|
||||
@ -166,14 +173,51 @@ public class McpToolExecutor(
|
||||
var list = await salesProductRepository.GetListAsync(
|
||||
x => x.IsActive == EnableStatus.Enabled
|
||||
&& (x.ProductName.Contains(keyword) || x.ProductCode.Contains(keyword)));
|
||||
products.AddRange(list.Take(5));
|
||||
products.AddRange(list.Take(8));
|
||||
}
|
||||
|
||||
// 仅品名:在已启用且配置了对客说明的产品中检索可走渠道
|
||||
if (products.Count == 0 && !string.IsNullOrWhiteSpace(cargoName))
|
||||
{
|
||||
var keyword = cargoName.Trim();
|
||||
var withGuide = await salesProductRepository.GetListAsync(
|
||||
x => x.IsActive == EnableStatus.Enabled
|
||||
&& x.CustomerGuideMarkdown != null
|
||||
&& x.CustomerGuideMarkdown != "");
|
||||
|
||||
static bool ContainsIgnoreCase(string? haystack, string needle) =>
|
||||
!string.IsNullOrEmpty(haystack)
|
||||
&& haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var matched = withGuide
|
||||
.Where(p =>
|
||||
ContainsIgnoreCase(p.CustomerGuideMarkdown, keyword)
|
||||
|| ContainsIgnoreCase(p.ProductName, keyword)
|
||||
|| ContainsIgnoreCase(p.ProductCode, keyword))
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
if (matched.Count > 0)
|
||||
{
|
||||
products.AddRange(matched);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (true, JsonSerializer.Serialize(new
|
||||
{
|
||||
items = Array.Empty<object>(),
|
||||
cargoName = keyword,
|
||||
hint = "未在已配置对客说明的渠道中命中该品名。请换关键词,或补充销售产品代码/名称后再查;不要擅自判定可寄。"
|
||||
}, JsonOptions));
|
||||
}
|
||||
}
|
||||
|
||||
if (products.Count == 0)
|
||||
{
|
||||
return (false, "未找到销售产品,请提供 productCode 或 productName");
|
||||
return (false, "未找到销售产品,请提供 productCode、productName 或 cargoName");
|
||||
}
|
||||
|
||||
var cargo = string.IsNullOrWhiteSpace(cargoName) ? null : cargoName.Trim();
|
||||
var items = products.Select(p =>
|
||||
{
|
||||
var guide = p.CustomerGuideMarkdown?.Trim() ?? string.Empty;
|
||||
@ -184,14 +228,19 @@ public class McpToolExecutor(
|
||||
productName = p.ProductName,
|
||||
hasGuide = !string.IsNullOrWhiteSpace(guide),
|
||||
customerGuideMarkdown = string.IsNullOrWhiteSpace(guide) ? null : guide,
|
||||
cargoNameToCheck = string.IsNullOrWhiteSpace(cargoName) ? null : cargoName.Trim(),
|
||||
cargoNameToCheck = cargo,
|
||||
hint = string.IsNullOrWhiteSpace(guide)
|
||||
? "该产品尚未配置对客说明,无法据此判断品名可否承运。"
|
||||
: "请根据对客说明判断 cargoName 是否可寄;说明未提及的品名不要擅自判定可寄。"
|
||||
: "请根据对客说明判断 cargoName 是否可寄、是否适合该渠道;说明未提及的品名不要擅自判定可寄。"
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return (true, JsonSerializer.Serialize(new { items }, JsonOptions));
|
||||
return (true, JsonSerializer.Serialize(new
|
||||
{
|
||||
items,
|
||||
cargoName = cargo,
|
||||
hint = "用一两句中文告诉客户:哪些渠道对客说明支持该品名、哪些不支持;可列出产品代码/名称。"
|
||||
}, JsonOptions));
|
||||
}
|
||||
|
||||
private async Task<(bool, string)> TrackQueryAsync(string argumentsJson, CancellationToken cancellationToken)
|
||||
|
||||
@ -127,7 +127,8 @@ public class AiService : ApplicationService
|
||||
DisplayName = entity.DisplayName,
|
||||
BaseUrl = entity.BaseUrl,
|
||||
ApiKey = maskSecret ? MaskSecret(entity.ApiKeyEnc) : entity.ApiKeyEnc,
|
||||
ApiSecret = maskSecret ? MaskSecret(entity.ApiSecretEnc) : entity.ApiSecretEnc,
|
||||
// ApiSecret 实际承载默认模型名,列表需明文回传以便卡片展示/编辑
|
||||
ApiSecret = entity.ApiSecretEnc,
|
||||
IsDefault = entity.IsDefault,
|
||||
Enabled = entity.Enabled
|
||||
};
|
||||
|
||||
@ -149,7 +149,10 @@ public class McpToolService : ApplicationService
|
||||
"不要索要起运地、发货城市、仓库(用户提到深圳等可忽略或作备注)。" +
|
||||
"国家可用中文,调工具时转二字码(美国→US、英国→GB)。" +
|
||||
"用户已给目的国+重量时,必须立刻调用 oms.price_check,用一两句中文总结结果。" +
|
||||
"\n2) 对客说明/可承运:必填销售产品代码或名称 + 客户品名;缺一再追问。" +
|
||||
"\n2) 对客说明/可承运/走什么渠道:用户问某品名能不能寄、能不能走、走什么渠道时,必须立刻调用 oms.channel_guide。" +
|
||||
"参数示例:{\"productCode\":\"\",\"productName\":\"\",\"cargoName\":\"lipstick\"}。" +
|
||||
"cargoName 必填(从用户话里提取品名);productCode/productName 仅在客户已点名渠道时填写,否则留空由工具按品名检索。" +
|
||||
"有结果后用一两句中文说明可走/不可走的渠道;未命中时如实说,不要编造。" +
|
||||
"\n3) 查轨迹:消息里已有运单号/跟踪号/客户单号(如 IDE 开头)时,立刻调用 oms.track_query,禁止再向用户要单号。" +
|
||||
"\n4) 禁止空回复;信息不足时用一两句中文追问缺项;不要复述 JSON 或大段工具原文。" +
|
||||
"\n【省 token】回复尽量短;一次只调必需工具。"
|
||||
|
||||
@ -80,7 +80,15 @@ public class AiLlmClient
|
||||
throw new BusinessException($"请先配置 {provider.DisplayName} 的 API Key");
|
||||
}
|
||||
|
||||
var model = modelOverride ?? provider.ApiSecretEnc;
|
||||
var model = modelOverride;
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
model = ResolveConfiguredModelId(provider.ApiSecretEnc);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
model = DefaultModelForProviderType(provider.ProviderType);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
throw new BusinessException($"请先配置 {provider.DisplayName} 的模型名(ApiSecret 字段)或在请求中传入 model");
|
||||
@ -256,6 +264,97 @@ public class AiLlmClient
|
||||
|
||||
return body.Length <= 300 ? body : body[..300];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 未配置 ApiSecret(模型名)时按提供商类型回落默认模型。
|
||||
/// </summary>
|
||||
private static string? DefaultModelForProviderType(string? providerType) =>
|
||||
(providerType ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"chatgpt" => "gpt-4o-mini",
|
||||
"doubao" => "doubao-1-5-pro-32k-250115",
|
||||
"deepseek" => "deepseek-chat",
|
||||
"zhipu" => "glm-5.1",
|
||||
_ => null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 解析 ApiSecret:兼容纯模型名,或多模型 JSON
|
||||
/// {"defaultModelId":"...","models":[{"modelId":"...","role":"文字"}]}。
|
||||
/// </summary>
|
||||
private static string? ResolveConfiguredModelId(string? apiSecret)
|
||||
{
|
||||
var secret = (apiSecret ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!secret.StartsWith('{'))
|
||||
{
|
||||
return secret;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(secret);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("defaultModelId", out var defaultModel) &&
|
||||
defaultModel.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var id = defaultModel.GetString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("models", out var models) &&
|
||||
models.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in models.EnumerateArray())
|
||||
{
|
||||
if (!item.TryGetProperty("modelId", out var modelIdEl) ||
|
||||
modelIdEl.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var modelId = modelIdEl.GetString()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.TryGetProperty("role", out var roleEl) &&
|
||||
roleEl.ValueKind == JsonValueKind.String &&
|
||||
string.Equals(roleEl.GetString(), "文字", StringComparison.Ordinal))
|
||||
{
|
||||
return modelId;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in models.EnumerateArray())
|
||||
{
|
||||
if (item.TryGetProperty("modelId", out var modelIdEl) &&
|
||||
modelIdEl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var modelId = modelIdEl.GetString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
return modelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return secret;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AiLlmServiceExtensions
|
||||
|
||||
Loading…
Reference in New Issue
Block a user