feat: 添加 GPC 任务过期状态处理,增强任务执行的稳定性和错误提示
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||||
const GPC_TASK_POLL_INTERVAL_MS = 3000;
|
const GPC_TASK_POLL_INTERVAL_MS = 3000;
|
||||||
|
const GPC_TASK_STALE_STATUS_TIMEOUT_MS = 60000;
|
||||||
const GPC_REMOTE_STAGE_LABELS = {
|
const GPC_REMOTE_STAGE_LABELS = {
|
||||||
auto_otp_wait: '等待自动 OTP',
|
auto_otp_wait: '等待自动 OTP',
|
||||||
checkout_order_start: '创建订单',
|
checkout_order_start: '创建订单',
|
||||||
@@ -786,6 +787,49 @@
|
|||||||
return ['completed', 'failed', 'expired', 'discarded'].includes(String(status || '').trim().toLowerCase());
|
return ['completed', 'failed', 'expired', 'discarded'].includes(String(status || '').trim().toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGpcTaskProgressSignature(task = {}) {
|
||||||
|
return [
|
||||||
|
task?.status,
|
||||||
|
task?.status_text,
|
||||||
|
task?.remote_stage,
|
||||||
|
task?.api_waiting_for,
|
||||||
|
task?.last_input_error,
|
||||||
|
task?.otp_invalid_count,
|
||||||
|
task?.reference_id || task?.referenceId,
|
||||||
|
task?.redirect_url || task?.redirectUrl,
|
||||||
|
task?.flow_id || task?.flowId,
|
||||||
|
task?.challenge_id || task?.challengeId,
|
||||||
|
task?.gopay_guid || task?.gopayGuid,
|
||||||
|
].map((value) => String(value ?? '').trim()).join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldWatchGpcTaskProgress(task = {}, state = {}) {
|
||||||
|
if (!task || isGpcTaskTerminal(task.status)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isGpcTaskOtpWait(task, state) || isGpcTaskPinWait(task, state)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGpcTaskStaleStatusTimeoutMs(state = {}) {
|
||||||
|
const configuredSeconds = Number(state?.gopayHelperTaskStaleSeconds);
|
||||||
|
if (Number.isFinite(configuredSeconds) && configuredSeconds > 0) {
|
||||||
|
return Math.max(15000, Math.min(600000, Math.floor(configuredSeconds * 1000)));
|
||||||
|
}
|
||||||
|
return GPC_TASK_STALE_STATUS_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGpcTaskStaleStatusError(task = {}, staleTimeoutMs = GPC_TASK_STALE_STATUS_TIMEOUT_MS) {
|
||||||
|
const seconds = Math.max(1, Math.round(staleTimeoutMs / 1000));
|
||||||
|
const label = formatGpcRemoteStageLabel(task?.remote_stage)
|
||||||
|
|| task?.status_text
|
||||||
|
|| task?.status
|
||||||
|
|| '未知状态';
|
||||||
|
return new Error(`GPC_TASK_ENDED::GPC 任务状态超过 ${seconds} 秒无进展(${label}),请重新创建任务。`);
|
||||||
|
}
|
||||||
|
|
||||||
function buildGpcTaskTerminalError(task = {}) {
|
function buildGpcTaskTerminalError(task = {}) {
|
||||||
const status = String(task?.status || '').trim().toLowerCase();
|
const status = String(task?.status || '').trim().toLowerCase();
|
||||||
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
|
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
|
||||||
@@ -927,6 +971,9 @@
|
|||||||
let lastSubmittedOtp = '';
|
let lastSubmittedOtp = '';
|
||||||
let pinSubmitted = false;
|
let pinSubmitted = false;
|
||||||
let terminalReached = false;
|
let terminalReached = false;
|
||||||
|
let lastProgressSignature = '';
|
||||||
|
let lastProgressAt = Date.now();
|
||||||
|
const staleStatusTimeoutMs = getGpcTaskStaleStatusTimeoutMs(state);
|
||||||
|
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
throw new Error('步骤 7:GPC 模式缺少 task_id,请先执行步骤 6。');
|
throw new Error('步骤 7:GPC 模式缺少 task_id,请先执行步骤 6。');
|
||||||
@@ -975,6 +1022,20 @@
|
|||||||
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
|
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldWatchGpcTaskProgress(task, state)) {
|
||||||
|
const progressSignature = buildGpcTaskProgressSignature(task);
|
||||||
|
const now = Date.now();
|
||||||
|
if (progressSignature && progressSignature !== lastProgressSignature) {
|
||||||
|
lastProgressSignature = progressSignature;
|
||||||
|
lastProgressAt = now;
|
||||||
|
} else if (progressSignature && now - lastProgressAt >= staleStatusTimeoutMs) {
|
||||||
|
throw buildGpcTaskStaleStatusError(task, staleStatusTimeoutMs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastProgressSignature = '';
|
||||||
|
lastProgressAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
if (isGpcTaskOtpWait(task, state)) {
|
if (isGpcTaskOtpWait(task, state)) {
|
||||||
if (isGpcTaskInputDeadlineExpired(task)) {
|
if (isGpcTaskInputDeadlineExpired(task)) {
|
||||||
throw buildGpcInputDeadlineError(task, 'OTP');
|
throw buildGpcInputDeadlineError(task, 'OTP');
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ function createExecutorHarness({
|
|||||||
markCurrentRegistrationAccountUsed = async () => {},
|
markCurrentRegistrationAccountUsed = async () => {},
|
||||||
probeIpProxyExit = null,
|
probeIpProxyExit = null,
|
||||||
onSetState = null,
|
onSetState = null,
|
||||||
|
sleepWithStop = null,
|
||||||
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
|
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
|
||||||
}) {
|
}) {
|
||||||
const api = loadPlusCheckoutBillingModule();
|
const api = loadPlusCheckoutBillingModule();
|
||||||
@@ -166,7 +167,7 @@ function createExecutorHarness({
|
|||||||
await onSetState(updates, events);
|
await onSetState(updates, events);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sleepWithStop: async (ms) => events.sleeps.push(ms),
|
sleepWithStop: sleepWithStop || (async (ms) => events.sleeps.push(ms)),
|
||||||
waitForTabCompleteUntilStopped: async () => checkoutTab,
|
waitForTabCompleteUntilStopped: async () => checkoutTab,
|
||||||
waitForTabUrlMatchUntilStopped: async (tabId, matcher) => {
|
waitForTabUrlMatchUntilStopped: async (tabId, matcher) => {
|
||||||
events.waitedUrls.push({ tabId });
|
events.waitedUrls.push({ tabId });
|
||||||
@@ -1016,6 +1017,70 @@ test('GPC billing logs checkout order stage in Chinese', async () => {
|
|||||||
assert.equal(events.logs.some((entry) => /checkout_order_start/.test(entry.message)), false);
|
assert.equal(events.logs.some((entry) => /checkout_order_start/.test(entry.message)), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('GPC billing fails repeated checkout stage as stale so auto-run can recreate task', async () => {
|
||||||
|
const originalNow = Date.now;
|
||||||
|
let now = 1710000000000;
|
||||||
|
const fetchCalls = [];
|
||||||
|
const { events, executor } = createExecutorHarness({
|
||||||
|
frames: [],
|
||||||
|
stateByFrame: {},
|
||||||
|
sleepWithStop: async (ms) => {
|
||||||
|
events.sleeps.push(ms);
|
||||||
|
now += ms;
|
||||||
|
},
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
fetchCalls.push({ url, options });
|
||||||
|
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_stale') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({
|
||||||
|
task_id: 'task_stale',
|
||||||
|
phone_mode: 'auto',
|
||||||
|
status: 'active',
|
||||||
|
status_text: '处理中',
|
||||||
|
remote_stage: 'checkout_order_start',
|
||||||
|
api_waiting_for: '',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_stale/stop')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({
|
||||||
|
task_id: 'task_stale',
|
||||||
|
status: 'discarded',
|
||||||
|
status_text: '已停止',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected url: ${url}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Date.now = () => now;
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
() => executor.executePlusCheckoutBilling({
|
||||||
|
plusPaymentMethod: 'gpc-helper',
|
||||||
|
plusCheckoutSource: 'gpc-helper',
|
||||||
|
gopayHelperTaskId: 'task_stale',
|
||||||
|
gopayHelperPhoneMode: 'auto',
|
||||||
|
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||||
|
gopayHelperApiKey: 'gpc_auto',
|
||||||
|
gopayHelperTaskStaleSeconds: 15,
|
||||||
|
}),
|
||||||
|
/GPC_TASK_ENDED::GPC 任务状态超过 15 秒无进展(创建订单),请重新创建任务。/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
Date.now = originalNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_stale/stop')), true);
|
||||||
|
assert.equal(events.logs.some((entry) => entry.message === '步骤 7:GPC 任务状态:创建订单'), true);
|
||||||
|
});
|
||||||
|
|
||||||
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
|
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user