148 lines
5.6 KiB
TypeScript
148 lines
5.6 KiB
TypeScript
/** AI 助手 · 三大 MCP 能力所需信息引导 */
|
||
|
||
export type McpCapabilityId = 'price' | 'guide' | 'track'
|
||
|
||
export type McpFieldHint = {
|
||
key: string
|
||
required: boolean
|
||
/** i18n key under nanaAssistant.mcp.* */
|
||
labelKey: string
|
||
}
|
||
|
||
export type McpCapabilityGuide = {
|
||
id: McpCapabilityId
|
||
toolKey: string
|
||
/** 快捷入口 i18n */
|
||
titleKey: string
|
||
descKey: string
|
||
fields: McpFieldHint[]
|
||
/** 引导话术 i18n */
|
||
coachKey: string
|
||
/** 输入框示例 */
|
||
exampleKey: string
|
||
/** 预填到输入框的示例文案 i18n(可编辑后再发) */
|
||
draftKey: string
|
||
}
|
||
|
||
export const MCP_CAPABILITY_GUIDES: McpCapabilityGuide[] = [
|
||
{
|
||
id: 'price',
|
||
toolKey: 'oms.price_check',
|
||
titleKey: 'nanaAssistant.mcp.priceTitle',
|
||
descKey: 'nanaAssistant.mcp.priceDesc',
|
||
coachKey: 'nanaAssistant.mcp.priceCoach',
|
||
exampleKey: 'nanaAssistant.mcp.priceExample',
|
||
draftKey: 'nanaAssistant.mcp.priceDraft',
|
||
fields: [
|
||
{ key: 'country', required: true, labelKey: 'nanaAssistant.mcp.field.country' },
|
||
{ key: 'weight', required: true, labelKey: 'nanaAssistant.mcp.field.weight' },
|
||
{ key: 'postal', required: false, labelKey: 'nanaAssistant.mcp.field.postal' },
|
||
{ key: 'dims', required: false, labelKey: 'nanaAssistant.mcp.field.dims' },
|
||
{ key: 'cargoType', required: false, labelKey: 'nanaAssistant.mcp.field.cargoType' },
|
||
],
|
||
},
|
||
{
|
||
id: 'guide',
|
||
toolKey: 'oms.channel_guide',
|
||
titleKey: 'nanaAssistant.mcp.guideTitle',
|
||
descKey: 'nanaAssistant.mcp.guideDesc',
|
||
coachKey: 'nanaAssistant.mcp.guideCoach',
|
||
exampleKey: 'nanaAssistant.mcp.guideExample',
|
||
draftKey: 'nanaAssistant.mcp.guideDraft',
|
||
fields: [
|
||
{ key: 'product', required: true, labelKey: 'nanaAssistant.mcp.field.product' },
|
||
{ key: 'cargoName', required: true, labelKey: 'nanaAssistant.mcp.field.cargoName' },
|
||
],
|
||
},
|
||
{
|
||
id: 'track',
|
||
toolKey: 'oms.track_query',
|
||
titleKey: 'nanaAssistant.mcp.trackTitle',
|
||
descKey: 'nanaAssistant.mcp.trackDesc',
|
||
coachKey: 'nanaAssistant.mcp.trackCoach',
|
||
exampleKey: 'nanaAssistant.mcp.trackExample',
|
||
draftKey: 'nanaAssistant.mcp.trackDraft',
|
||
fields: [
|
||
{ key: 'orderNo', required: true, labelKey: 'nanaAssistant.mcp.field.orderNo' },
|
||
],
|
||
},
|
||
]
|
||
|
||
const COUNTRY_HINT =
|
||
/美国|英國|英国|德国|法国|澳洲|澳大利亚|加拿大|日本|韩国|意大利|西班牙|荷兰|墨西哥|巴西|新西兰|\b(US|GB|UK|DE|FR|AU|CA|JP|KR|IT|ES|NL|MX|BR|NZ)\b/i
|
||
|
||
/** 0.5kg / 0.5 kg / 重量0.5 / 大概0.5公斤 — 避免把运单号里的纯数字当成重量 */
|
||
const WEIGHT_HINT =
|
||
/(\d+(\.\d+)?)\s*(kg|KG|公斤|千克)|(?:重量|实重)\s*[::]?\s*\d+(\.\d+)?|(?:约|大概)\s*\d+(\.\d+)?\s*(kg|KG|公斤|千克)?/i
|
||
|
||
/** IDE 运单、跟踪号等 */
|
||
const ORDER_NO_LOOSE = /[A-Za-z][A-Za-z0-9\-_]{5,}/
|
||
const ORDER_NO_STRICT =
|
||
/\b(IDE\d{8,}|(?:YD|SF|YT|NF|LT)[A-Za-z0-9]{6,}|(?:[A-Z]{2,}\d{8,}))\b/i
|
||
|
||
const PRODUCT_HINT = /产品|渠道|线路|运输方式|[A-Z]{2,}[_-][A-Z0-9_]+/i
|
||
const CARGO_HINT =
|
||
/品名|货物|普货|敏感|衣服|服装|鞋|化妆品|lipstick|手机|电池|粉末|液体|食品|寄|发/i
|
||
|
||
export function extractOrderNo(text: string): string | null {
|
||
const strict = text.match(ORDER_NO_STRICT)
|
||
if (strict?.[1] || strict?.[0]) return (strict[1] || strict[0]).trim()
|
||
// 「运单号:XXXX」类
|
||
const labeled = text.match(
|
||
/(?:运单号|跟踪号|客户单号|物流单号|单号)[::\s]*([A-Za-z0-9][A-Za-z0-9\-_]{5,})/i,
|
||
)
|
||
if (labeled?.[1]) return labeled[1].trim()
|
||
const loose = text.match(ORDER_NO_LOOSE)
|
||
return loose?.[0]?.trim() || null
|
||
}
|
||
|
||
function joinedContext(text: string, priorTexts?: string[]) {
|
||
const parts = [...(priorTexts ?? []), text].map((s) => String(s || '').trim()).filter(Boolean)
|
||
return parts.join('\n')
|
||
}
|
||
|
||
/** 粗判用户这句话还缺哪些必填信息(用于先追问再调工具);会合并多轮用户原文 */
|
||
export function detectMissingFields(
|
||
capability: McpCapabilityId,
|
||
text: string,
|
||
opts?: { hasWaybillContext?: boolean; priorTexts?: string[] },
|
||
): string[] {
|
||
const t = joinedContext(text, opts?.priorTexts)
|
||
const missing: string[] = []
|
||
if (capability === 'price') {
|
||
if (!COUNTRY_HINT.test(t)) missing.push('country')
|
||
if (!WEIGHT_HINT.test(t)) missing.push('weight')
|
||
} else if (capability === 'guide') {
|
||
if (!PRODUCT_HINT.test(t)) missing.push('product')
|
||
if (!CARGO_HINT.test(t) && t.replace(/\s/g, '').length < 4) missing.push('cargoName')
|
||
} else if (capability === 'track') {
|
||
if (!opts?.hasWaybillContext && !extractOrderNo(t)) missing.push('orderNo')
|
||
}
|
||
return missing
|
||
}
|
||
|
||
export function inferCapabilityFromText(text: string): McpCapabilityId | null {
|
||
const raw = text.trim()
|
||
if (!raw) return null
|
||
const lower = raw.toLowerCase()
|
||
|
||
// 轨迹优先:关键词或明显单号
|
||
if (/track|tracking|轨迹|物流|运单|查单|单号|查下|查一下/.test(lower)) return 'track'
|
||
if (extractOrderNo(raw) && !COUNTRY_HINT.test(raw)) return 'track'
|
||
if (ORDER_NO_STRICT.test(raw) && raw.length < 40) return 'track'
|
||
|
||
if (/品名|能发|禁运|可寄|对客说明|承运|敏感货/.test(lower)) return 'guide'
|
||
|
||
if (/price|cost|quote|报价|价格|多少钱|运费|查价|怎么走|走得动/.test(lower)) return 'price'
|
||
|
||
// 隐式查价:目的国 + 重量 / 「走|寄|发」+ 国家
|
||
if (COUNTRY_HINT.test(raw) && WEIGHT_HINT.test(raw)) return 'price'
|
||
if (/(?:走|寄|发往|寄到|发到|到)/.test(raw) && COUNTRY_HINT.test(raw)) return 'price'
|
||
|
||
return null
|
||
}
|
||
|
||
export function getCapability(id: McpCapabilityId): McpCapabilityGuide {
|
||
return MCP_CAPABILITY_GUIDES.find((c) => c.id === id)!
|
||
}
|