You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chajia/lib/mothership/portal-quote-messages.ts

294 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 从上游 dashboard 页面正文提取「官网业务拒价/阻断」文案,供前端如实展示。
* 非 RPA/系统错误,而是承运商或官网规则导致无法出价。
* 对外统一译为中文,避免英文原文与上游品牌直出给客户。
*/
import { scrubProviderBrandForCustomer } from "@/lib/frontend/scrub-provider-brand";
const ACCESSORIAL_BLOCK_PATTERNS: RegExp[] = [
/不支持附加服务[^\n。]{0,160}/,
/(?:承运|运输)合作伙伴[^\n。]{0,80}不提供[^\n。]{0,120}/,
/住宅地址[^\n。]{0,80}上门取件[^\n。]{0,80}/,
/unsupported accessorial[^\n.]{0,200}/i,
/accessorial(?:s)?[^\n.]{0,120}(?:not supported|not available|cannot be)/i,
/our carrier partners? do not provide[^\n.]{0,220}/i,
/carrier partners? (?:don'?t|do not) provide[^\n.]{0,220}/i,
/(?:pickup|pick-up)[^\n.]{0,80}residential[^\n.]{0,120}/i,
/residential[^\n.]{0,80}(?:pickup|pick-up)[^\n.]{0,120}/i,
];
/** 线路/地址/运力等官网业务阻断(底部红色提示常见) */
const LANE_ADDRESS_BLOCK_PATTERNS: RegExp[] = [
/该线路[^\n。]{0,120}/,
/无法[^\n。]{0,40}报价[^\n。]{0,80}/,
/暂无[^\n。]{0,40}运力[^\n。]{0,80}/,
/地址[^\n。]{0,40}(?:错误|无效|无法|不支持)[^\n。]{0,80}/,
/不在[^\n。]{0,40}服务[^\n。]{0,40}范围[^\n。]{0,40}/,
/please enter the address of your (?:pick-?up|delivery) location[^\n.]{0,80}/i,
/(?:invalid|incorrect|unsupported)\s+address[^\n.]{0,120}/i,
/address[^\n.]{0,80}(?:not supported|not found|could not be|is invalid)[^\n.]{0,120}/i,
// 官网红色 toast:Unfortunately we were unable to find any rates for this lane...
/unable to find any rates for this lane[^\n.]{0,200}/i,
/unfortunately we were unable to find any rates[^\n.]{0,220}/i,
/(?:we )?(?:could|can) ?not (?:find|get|obtain)[^\n.]{0,40}(?:quote|rates?)[^\n.]{0,120}/i,
/unable to (?:get|find|obtain|provide)[^\n.]{0,40}(?:quote|rates?)[^\n.]{0,120}/i,
/no carriers? (?:available|found|able)[^\n.]{0,120}/i,
/(?:this )?lane[^\n.]{0,80}(?:not supported|unavailable|not available)[^\n.]{0,120}/i,
/outside (?:of )?(?:our )?service (?:area|region)[^\n.]{0,120}/i,
/temporarily unable to (?:obtain|get|fetch) quotes?[^\n.]{0,80}/i,
/carriers? (?:are )?(?:unable|not able) to[^\n.]{0,160}/i,
];
/** 官网英文阻断 → 中文(客户可见) */
const PORTAL_EN_TO_ZH: Array<[RegExp, string]> = [
[
/unable to find any rates for this lane|unfortunately we were unable to find any rates/i,
"该线路与货物组合暂无可用报价,请检查货物信息后重试或联系客服",
],
[
/no rates found[\s\S]*please review all required fields to proceed/i,
"未找到可用报价。请检查并填写所有必填项后再继续",
],
[/no rates found/i, "未找到可用报价"],
[
/please review all required fields to proceed/i,
"请检查并填写所有必填项后再继续",
],
[/unsupported accessorial/i, "不支持该附加服务"],
[
/our carrier partners? do not provide[^\n.]*/i,
"承运合作伙伴不提供此项服务",
],
[
/carrier partners? (?:don'?t|do not) provide[^\n.]*/i,
"承运合作伙伴不提供此项服务",
],
[
/(?:pickup|pick-up)[^\n.]{0,80}residential|residential[^\n.]{0,80}(?:pickup|pick-up)/i,
"不支持住宅地址上门取件",
],
[/accessorial(?:s)?[^\n.]{0,80}(?:not supported|not available)/i, "不支持该附加服务"],
[
/please enter the address of your pick-?up location/i,
"请填写并确认提货地址",
],
[
/please enter the address of your delivery location/i,
"请填写并确认派送地址",
],
[/invalid address|address is invalid/i, "地址无效,请重新选择"],
[
/address[^\n.]{0,40}(?:not supported|not found)/i,
"该地址不受支持或无法识别,请重新选择",
],
[
/unable to (?:get|find|obtain|provide)[^\n.]{0,40}(?:quote|rates?)|could not (?:find|get)[^\n.]{0,40}(?:quote|rates?)/i,
"官网无法为该线路提供报价",
],
[/no carriers? (?:available|found)/i, "暂无可用承运商"],
[
/(?:this )?lane[^\n.]{0,40}(?:not supported|unavailable|not available)/i,
"该线路暂不支持报价",
],
[
/outside (?:of )?(?:our )?service (?:area|region)/i,
"该地址不在服务范围内",
],
[
/temporarily unable to (?:obtain|get|fetch) quotes?/i,
"暂时无法获取报价,请稍后重试",
],
[
/carriers? (?:are )?(?:unable|not able) to/i,
"承运商无法承运该票货物",
],
[
/freight ready time is before pickup location operation hours/i,
"货运就绪时刻不能早于提货点开门时间,请调整就绪时刻或开门时间",
],
[
/ready time is before.{0,40}operation hours/i,
"货运就绪时刻不能早于提货点开门时间,请调整就绪时刻或开门时间",
],
];
function normalizePortalLine(line: string, keepNewlines = false): string {
if (keepNewlines) {
return line
.replace(/\r/g, "")
.replace(/[ \t]+/g, " ")
.replace(/\n+/g, "\n")
.trim();
}
return line.replace(/\s+/g, " ").trim();
}
/** 将单条官网提示译为中文;已是中文则原样返回 */
export function translatePortalMessageToZh(message: string): string {
const trimmed = message.trim();
if (!trimmed) return trimmed;
if (/[\u4e00-\u9fff]/.test(trimmed)) {
return trimmed;
}
for (const [pattern, zh] of PORTAL_EN_TO_ZH) {
if (pattern.test(trimmed)) {
return zh;
}
}
// 未知英文:仍回传内容,加前缀避免客户只看到笼统失败
const brief = trimmed.replace(/\s+/g, " ").slice(0, 220);
return `承运商提示:${brief}`;
}
function pushMatch(
found: string[],
msg: string,
keepNewlines = false,
): void {
const n = normalizePortalLine(msg, keepNewlines);
if (n && !found.includes(n)) found.push(n);
}
function extractByPatterns(text: string, patterns: RegExp[]): string[] {
const found: string[] = [];
for (const p of patterns) {
const m = text.match(p);
if (m?.[0]) pushMatch(found, m[0]);
}
return found;
}
/** 提取页面可见的官网阻断/无价提示(去重、保序) */
export function extractMothershipPortalQuoteMessages(body: string): string[] {
const text = body.replace(/\r/g, "");
const found: string[] = [];
// 优先抓红色 toast:lane/freight 无价(比「必填项」更准确)
const laneToast = text.match(
/Unfortunately we were unable to find any rates for this lane[^\n.]{0,220}\.?/i,
);
if (laneToast?.[0]) {
pushMatch(found, laneToast[0]);
}
const hasNoRates = /No rates found/i.test(text);
const hasRequired =
/Please review all required fields to proceed/i.test(text);
// 已有「线路无价」硬拒时,不再附带 required fields(避免误导用户去补二级详情)
if (!laneToast?.[0]) {
if (hasNoRates && hasRequired) {
pushMatch(
found,
"No rates found\nPlease review all required fields to proceed",
true,
);
} else if (hasNoRates) {
pushMatch(found, "No rates found");
} else if (hasRequired) {
pushMatch(found, "Please review all required fields to proceed");
}
}
for (const msg of extractByPatterns(text, ACCESSORIAL_BLOCK_PATTERNS)) {
pushMatch(found, msg);
}
for (const msg of extractByPatterns(text, LANE_ADDRESS_BLOCK_PATTERNS)) {
pushMatch(found, msg);
}
return found;
}
/** 附加服务/承运商规则类阻断(应立刻如实返回,不等 patch/超时) */
export function extractMothershipAccessorialBlockMessages(
body: string,
): string[] {
return extractByPatterns(body.replace(/\r/g, ""), ACCESSORIAL_BLOCK_PATTERNS);
}
/** DOM 红色/alert 片段是否像业务报错(过滤 Inbox/导航噪声) */
export function looksLikePortalBusinessAlert(text: string): boolean {
const t = text.replace(/\s+/g, " ").trim();
if (t.length < 6 || t.length > 400) return false;
if (/Got it|Mark all as read|Stay up-to-date|Refer and get/i.test(t)) {
return false;
}
return /rate|quote|address|accessorial|carrier|residential|unsupported|unable|invalid|required field|lane|service area|线路|报价|地址|附加|不支持|必填|运力|承运|住宅|上门/i.test(
t,
);
}
/**
* 将 DOM 红色提示片段转为可展示的官网业务文案列表。
* 优先走模式提取;否则保留像业务报错的原文。
*/
export function extractMothershipPortalMessagesFromAlerts(
snippets: string[],
): string[] {
const joined = snippets.join("\n");
const patterned = extractMothershipPortalQuoteMessages(joined);
if (patterned.length > 0) return patterned;
const found: string[] = [];
for (const s of snippets) {
if (looksLikePortalBusinessAlert(s)) pushMatch(found, s);
}
return found.slice(0, 4);
}
export function formatMothershipPortalQuoteMessage(
messages: string[],
): string | null {
if (messages.length === 0) return null;
return scrubProviderBrandForCustomer(
messages.map(translatePortalMessageToZh).join("\n\n"),
);
}
/**
* 官网红色 toast:该线路/货物组合明确无价。
* 与「请填必填」常同时出现;此类必须硬拒,禁止引导用户白填二级详情。
*/
export function isPortalLaneNoRatesMessage(msg: string): boolean {
return /unable to find any rates(?: for this lane)?|unfortunately we were unable to find any rates|该线路与货物组合暂无可用报价|for this lane and freight/i.test(
msg,
);
}
/** 官网提示:缺二级必填(公司名/联系人等),应立刻引导填二级,禁止空等到超时 */
export function isPortalNeedsDetailsMessage(msg: string): boolean {
// 线路无价优先:即使同屏有 required fields,也不再判为「去补详情」
if (isPortalLaneNoRatesMessage(msg)) return false;
return /必填|required fields|请检查并填写所有必填/i.test(msg);
}
/**
* 官网明确业务拒价(线路/地址/附加服务等)。
* 与「缺必填」同时出现时:线路无价/硬拒优先,禁止误导补二级。
*/
export function isPortalHardBusinessBlock(msg: string): boolean {
if (isPortalLaneNoRatesMessage(msg)) return true;
if (isPortalNeedsDetailsMessage(msg)) return false;
return /附加|住宅|上门|不支持该附加|承运合作伙伴|线路|服务范围|地址无效|地址不|运力|承运商无法|无法为该线路|暂无可用承运|该线路暂不|不在服务|地址错误|该线路与货物|invalid address|unsupported accessorial|no carriers|outside.*service|unable to .*quote|unable to find any rates|unfortunately we were unable|for this lane/i.test(
msg,
);
}
/** 从整页正文解析;无则 null */
export function parseMothershipPortalQuoteMessageFromBody(
body: string,
): string | null {
return formatMothershipPortalQuoteMessage(
extractMothershipPortalQuoteMessages(body),
);
}
/** 从 DOM 红色/alert 片段解析 */
export function parseMothershipPortalQuoteMessageFromAlerts(
snippets: string[],
): string | null {
return formatMothershipPortalQuoteMessage(
extractMothershipPortalMessagesFromAlerts(snippets),
);
}