|
|
/**
|
|
|
* 从上游 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),
|
|
|
);
|
|
|
}
|