front/src/composables/assistantMcpGuide.ts
2026-07-29 12:47:32 +08:00

148 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 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)!
}