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.

203 lines
6.3 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.

/**
* Flock RPA 可观测进度(入库 quotes_json.__flock_progress轮询回传前端
*/
export const FLOCK_RPA_STAGES = [
"queued",
"lock",
"login",
"open_quote",
"fill_form",
"submit",
"wait_quote",
"done",
] as const;
export type FlockRpaStage = (typeof FLOCK_RPA_STAGES)[number];
export const FLOCK_RPA_STAGE_LABELS: Record<FlockRpaStage, string> = {
queued: "任务已入队,等待 Worker 领取",
lock: "获取 Flock 官网互斥锁",
login: "登录 Flock 官网(填写账密)",
open_quote: "打开登录态查价页",
fill_form: "填写 Quick 查价表单",
submit: "点击生成报价",
wait_quote: "等待并解析官网报价",
done: "报价完成",
};
export type FlockProgressPayload = {
stage: FlockRpaStage;
label: string;
at: string;
};
export type FlockStageReporter = (
stage: FlockRpaStage,
) => void | Promise<void>;
export async function reportFlockStage(
onStage: FlockStageReporter | undefined,
stage: FlockRpaStage,
): Promise<void> {
try {
await onStage?.(stage);
} catch {
/* 进度回写失败不阻断查价 */
}
}
export function buildFlockProgressPayload(
stage: FlockRpaStage,
): { __flock_progress: FlockProgressPayload } {
return {
__flock_progress: {
stage,
label: FLOCK_RPA_STAGE_LABELS[stage],
at: new Date().toISOString(),
},
};
}
export function parseFlockProgress(
quotesJson: unknown,
): FlockProgressPayload | null {
if (!quotesJson || typeof quotesJson !== "object") return null;
const raw = (quotesJson as Record<string, unknown>).__flock_progress;
if (!raw || typeof raw !== "object") return null;
const o = raw as Record<string, unknown>;
const stage = o.stage;
if (typeof stage !== "string") return null;
if (!(FLOCK_RPA_STAGES as readonly string[]).includes(stage)) return null;
const label =
typeof o.label === "string" && o.label
? o.label
: FLOCK_RPA_STAGE_LABELS[stage as FlockRpaStage];
return {
stage: stage as FlockRpaStage,
label,
at: typeof o.at === "string" ? o.at : "",
};
}
/** 根据失败文案定位卡在哪一步matched=false 表示未识别,勿用返回 stage 覆盖 Worker 进度 */
export function mapFlockErrorToStage(errorMessage?: string | null): {
stage: FlockRpaStage;
hint: string;
matched: boolean;
} {
const msg = errorMessage ?? "";
if (/FLOCK_LOGIN_FAILED|无法填写登录表单|账密但解密失败|账号或密码错误|PROVIDER_LOGIN_FAILED/i.test(msg)) {
return {
stage: "login",
hint: "卡在登录:账号或密码可能不正确。请确认承运商账密是否正确,以及修改后是否已在本系统即时保存更新。",
matched: true,
};
}
if (/SESSION_EXPIRED/i.test(msg)) {
return {
stage: "login",
hint: "卡在登录:会话失效且重登失败。请确认承运商账密是否正确,以及修改后是否已在本系统即时保存更新。",
matched: true,
};
}
if (/QUOTE_ENTRY_UNAVAILABLE|STRUCT_CHANGE.*查价|表单未加载/i.test(msg)) {
return {
stage: "open_quote",
hint: "卡在打开查价页:登录后未进入 Let's build a quote请确认 quote URL 与账密权限。",
matched: true,
};
}
if (/无法选择|无法填写|无法定位|QUOTE_ENTRY_UNAVAILABLE无法/i.test(msg)) {
return {
stage: "fill_form",
hint: "卡在填表:某个字段定位失败,请对照官网字段或更新 RPA 选项映射。",
matched: true,
};
}
if (/未找到提交按钮|Generate my quote/i.test(msg)) {
return {
stage: "submit",
hint: "卡在提交未找到「Generate my quote」按钮。",
matched: true,
};
}
if (/FLOCK_RATES_PENDING|still looking for the best rates|约需\s*15\s*分钟/i.test(msg)) {
return {
stage: "wait_quote",
hint: "官网已出单但尚未给出即时报价(仍在寻价约 15 分钟);本单已结束,请稍后用参考编号回查,勿重复提交。",
matched: true,
};
}
if (
/RPA_DATA_INVALID|未拿到报价|flock-stalls|未返回 Flock|报价档位/i.test(msg)
) {
return {
stage: "wait_quote",
hint: "卡在取价:已提交但未解析到报价,可查看 .rpa/flock-stalls/ 截图。",
matched: true,
};
}
if (/FLOCK_QUOTE_LIMIT|Thank you for your interest/i.test(msg)) {
return {
stage: "wait_quote",
hint: "卡在取价:官网报价次数达上限,请换账号或稍后重试。",
matched: true,
};
}
if (
/Target (page|closed)|browser.*(closed|disconnected)|Page crashed|Execution context was destroyed|locator\.count|has been closed|BROWSER_CLOSED/i.test(
msg,
)
) {
return {
stage: "wait_quote",
hint: "卡在取价:浏览器会话中断(关窗竞态/页跳转销毁上下文),请勿手动关窗并重试。",
matched: true,
};
}
if (/询价超时|请稍后重试(可在进度条|POLL|timeout/i.test(msg)) {
return {
stage: "wait_quote",
hint: "卡在取价前端轮询超时Worker 可能仍在跑;请稍候或刷新后查看历史询价。",
matched: true,
};
}
if (/互斥|exclusive|busy|占锁|防互踩/i.test(msg)) {
return {
stage: "lock",
hint: "卡在互斥锁:其他 Flock 任务正在占用浏览器,请稍候。",
matched: true,
};
}
// 未识别:禁止默认钉在「互斥锁」(会误导);保留 wait_quote 作展示用matched=false 不覆盖 Worker 阶段
return {
stage: "wait_quote",
hint: "查价失败,请根据下方错误明细排查 Worker 与官网状态。",
matched: false,
};
}
/** 失败时应否把 progress 写成 mapped.stage禁止把已走过的阶段回退到 lock */
export function shouldOverwriteFlockFailureStage(
currentStage: string | null | undefined,
mapped: { stage: FlockRpaStage; matched: boolean },
): boolean {
if (!mapped.matched) {
return false;
}
if (
!currentStage ||
!(FLOCK_RPA_STAGES as readonly string[]).includes(currentStage)
) {
return true;
}
return (
flockStageIndex(mapped.stage) >=
flockStageIndex(currentStage as FlockRpaStage)
);
}
export function flockStageIndex(stage: FlockRpaStage): number {
return FLOCK_RPA_STAGES.indexOf(stage);
}