From b02b3754493e0a7aa58ce5fa0fa7c8f0056d884b Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 10 May 2026 02:29:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20GPC=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E8=BF=87=E6=9C=9F=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BB=BB=E5=8A=A1=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E7=9A=84=E7=A8=B3=E5=AE=9A=E6=80=A7=E5=92=8C=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background/steps/fill-plus-checkout.js | 61 +++++++++++++++++ ...us-checkout-billing-tab-resolution.test.js | 67 ++++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index bdfea34..34afd99 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -14,6 +14,7 @@ const GPC_HELPER_PHONE_MODE_AUTO = 'auto'; const GPC_HELPER_PHONE_MODE_MANUAL = 'manual'; const GPC_TASK_POLL_INTERVAL_MS = 3000; + const GPC_TASK_STALE_STATUS_TIMEOUT_MS = 60000; const GPC_REMOTE_STAGE_LABELS = { auto_otp_wait: '等待自动 OTP', checkout_order_start: '创建订单', @@ -786,6 +787,49 @@ 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 = {}) { const status = String(task?.status || '').trim().toLowerCase(); const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim(); @@ -927,6 +971,9 @@ let lastSubmittedOtp = ''; let pinSubmitted = false; let terminalReached = false; + let lastProgressSignature = ''; + let lastProgressAt = Date.now(); + const staleStatusTimeoutMs = getGpcTaskStaleStatusTimeoutMs(state); if (!taskId) { throw new Error('步骤 7:GPC 模式缺少 task_id,请先执行步骤 6。'); @@ -975,6 +1022,20 @@ 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 (isGpcTaskInputDeadlineExpired(task)) { throw buildGpcInputDeadlineError(task, 'OTP'); diff --git a/tests/plus-checkout-billing-tab-resolution.test.js b/tests/plus-checkout-billing-tab-resolution.test.js index b209238..c793b08 100644 --- a/tests/plus-checkout-billing-tab-resolution.test.js +++ b/tests/plus-checkout-billing-tab-resolution.test.js @@ -85,6 +85,7 @@ function createExecutorHarness({ markCurrentRegistrationAccountUsed = async () => {}, probeIpProxyExit = null, onSetState = null, + sleepWithStop = null, submitRedirectUrl = 'https://www.paypal.com/checkoutnow', }) { const api = loadPlusCheckoutBillingModule(); @@ -166,7 +167,7 @@ function createExecutorHarness({ await onSetState(updates, events); } }, - sleepWithStop: async (ms) => events.sleeps.push(ms), + sleepWithStop: sleepWithStop || (async (ms) => events.sleeps.push(ms)), waitForTabCompleteUntilStopped: async () => checkoutTab, waitForTabUrlMatchUntilStopped: async (tabId, matcher) => { 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); }); +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 () => { const fetchCalls = []; let pollCount = 0;