import { getSalesProductDetail } from '@/api/channel/salesProduct' import { chatXhsAi, listXhsAiProviders } from '@/api/xhs' export type QuoteCandidate = { rowKey: string productId?: number | string shippingMethod: string cargoTypeName?: string warehouse?: string deliveryTime?: string totalCostNum: number maxDeliveryDays: number isPriceOptimal?: boolean isTimeOptimal?: boolean guide?: string } export type RecommendResult = { rowKey: string reason: string source: 'ai' | 'local' } const FORBIDDEN_HINTS = ['禁运', '不收', '不可走', '不能走', '拒收', '禁止', '不得寄', '无法承运', '不接受'] const ALLOWED_HINTS = ['可走', '可寄', '可发', '接受', '适用', '支持', '允许', '可承运', '建议'] const CARGO_KEYWORDS: Array<{ keys: string[]; tags: string[] }> = [ { keys: ['电池', '带电', '锂电', '充电宝', '移动电源'], tags: ['带电', '电池', '锂电', '敏感'] }, { keys: ['液体', '化妆', '护肤', '香水', '精油'], tags: ['液体', '化妆', '敏感'] }, { keys: ['粉末', '奶粉', '蛋白粉'], tags: ['粉末', '敏感'] }, { keys: ['食品', '零食', '茶叶', '保健'], tags: ['食品', '敏感'] }, { keys: ['服装', '衣服', '鞋子', '箱包'], tags: ['普货', '服装'] }, { keys: ['数码', '手机', '电脑', '耳机'], tags: ['普货', '带电', '数码'] }, ] function normalizeGuide(detail: Record | undefined) { return String(detail?.customerGuideMarkdown ?? detail?.CustomerGuideMarkdown ?? '').trim() } export async function loadGuidesForCandidates(candidates: QuoteCandidate[]) { const idSet = new Map() candidates.forEach((item) => { const id = item.productId == null || item.productId === '' ? '' : String(item.productId) if (!id) return const list = idSet.get(id) ?? [] list.push(item) idSet.set(id, list) }) const guideMap = new Map() await Promise.all( [...idSet.keys()].map(async (id) => { try { const detail = await getSalesProductDetail(id) guideMap.set(id, normalizeGuide(detail as Record)) } catch { guideMap.set(id, '') } }), ) return candidates.map((item) => ({ ...item, guide: item.productId == null ? '' : guideMap.get(String(item.productId)) || '', })) } function isForbidden(guide: string, productName: string) { const text = guide.toLowerCase() const name = productName.toLowerCase() if (!guide) return false // 品名附近带禁运语义 for (const hint of FORBIDDEN_HINTS) { if (!text.includes(hint.toLowerCase())) continue if (text.includes(name) || name.split(/\s+/).some((w) => w.length > 1 && text.includes(w))) { return true } // 全局禁运词 + 品类标签同时命中 const hitTag = CARGO_KEYWORDS.some( (group) => group.keys.some((k) => name.includes(k.toLowerCase())) && group.tags.some((tag) => text.includes(tag.toLowerCase())), ) if (hitTag) return true } return false } function scoreCandidate(item: QuoteCandidate, productName: string) { const guide = String(item.guide || '') const name = productName.trim() let score = 0 const reasons: string[] = [] if (isForbidden(guide, name)) { return { score: -1000, reasons: ['线路说明提示该品类可能禁运/不收'] } } if (!guide) { score += 5 reasons.push('暂无线路说明,仅按报价择优') } else { score += 20 const lowerGuide = guide.toLowerCase() const lowerName = name.toLowerCase() if (lowerName && lowerGuide.includes(lowerName)) { score += 40 reasons.push('线路说明中直接提到该品名') } for (const hint of ALLOWED_HINTS) { if (lowerGuide.includes(hint.toLowerCase())) { score += 8 reasons.push(`说明含“${hint}”`) break } } for (const group of CARGO_KEYWORDS) { const nameHit = group.keys.some((k) => lowerName.includes(k.toLowerCase())) if (!nameHit) continue const tagHit = group.tags.some((tag) => lowerGuide.includes(tag.toLowerCase())) if (tagHit) { score += 25 reasons.push(`品名与线路说明品类匹配(${group.keys[0]})`) } } } if (item.isPriceOptimal) { score += 30 reasons.push('价格最优') } if (item.isTimeOptimal) { score += 16 reasons.push('时效最优') } // 越便宜分越高 if (Number.isFinite(item.totalCostNum)) { score += Math.max(0, 20 - Math.min(20, item.totalCostNum / 50)) } if (Number.isFinite(item.maxDeliveryDays)) { score += Math.max(0, 10 - Math.min(10, item.maxDeliveryDays)) } return { score, reasons } } export function recommendLocally(productName: string, candidates: QuoteCandidate[]): RecommendResult | null { if (!candidates.length) return null const ranked = candidates .map((item) => { const { score, reasons } = scoreCandidate(item, productName) return { item, score, reasons } }) .filter((x) => x.score > -500) .sort((a, b) => b.score - a.score || a.item.totalCostNum - b.item.totalCostNum) const best = ranked[0] if (!best) return null const reason = `推荐「${best.item.shippingMethod}」:${best.reasons.slice(0, 3).join(';') || '综合报价与线路说明择优'}` return { rowKey: best.item.rowKey, reason, source: 'local' } } function extractJsonObject(text: string): Record | null { const raw = String(text || '').trim() if (!raw) return null try { return JSON.parse(raw) } catch { const match = raw.match(/\{[\s\S]*\}/) if (!match) return null try { return JSON.parse(match[0]) } catch { return null } } } export async function recommendWithAi( productName: string, candidates: QuoteCandidate[], ): Promise { const withGuides = await loadGuidesForCandidates(candidates) const local = recommendLocally(productName, withGuides) try { const providers = await listXhsAiProviders() const list = Array.isArray(providers) ? providers : [] const provider = list.find((p: any) => (p.isDefault || p.IsDefault) && (p.enabled ?? p.Enabled ?? true)) || list.find((p: any) => p.enabled ?? p.Enabled ?? true) const providerKey = String(provider?.providerKey ?? provider?.ProviderKey ?? '').trim() if (!providerKey) return local const compact = withGuides.slice(0, 12).map((item) => ({ rowKey: item.rowKey, channel: item.shippingMethod, cargo: item.cargoTypeName, warehouse: item.warehouse, deliveryTime: item.deliveryTime, totalCost: item.totalCostNum, priceOptimal: !!item.isPriceOptimal, timeOptimal: !!item.isTimeOptimal, guide: String(item.guide || '').slice(0, 1200), })) const content = await chatXhsAi({ providerKey, messages: [ { role: 'system', content: '你是国际物流渠道顾问。根据用户品名与各渠道线路说明,选出最合适的一条渠道。只返回 JSON:{"rowKey":"...","reason":"..."},不要其它文字。若说明明确禁运则不要选该渠道。同等适合时优先更便宜、其次更快。', }, { role: 'user', content: JSON.stringify({ productName, candidates: compact, }), }, ], }) const parsed = extractJsonObject(content?.content ?? '') const rowKey = String(parsed?.rowKey ?? parsed?.RowKey ?? '').trim() const reason = String(parsed?.reason ?? parsed?.Reason ?? '').trim() if (rowKey && withGuides.some((c) => c.rowKey === rowKey)) { return { rowKey, reason: reason || `AI 推荐渠道:${withGuides.find((c) => c.rowKey === rowKey)?.shippingMethod || rowKey}`, source: 'ai', } } } catch { // fallback local } return local }