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