1937 lines
81 KiB
JavaScript
1937 lines
81 KiB
JavaScript
(function attachBackgroundPlusCheckoutBilling(root, factory) {
|
||
root.MultiPageBackgroundPlusCheckoutBilling = factory();
|
||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusCheckoutBillingModule() {
|
||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
|
||
const PLUS_CHECKOUT_URL_PATTERN = /^https:\/\/chatgpt\.com\/checkout(?:\/|$)/i;
|
||
const PLUS_CHECKOUT_FRAME_READY_DELAY_MS = 500;
|
||
const PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS = 3;
|
||
const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
|
||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||
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: '创建订单',
|
||
checkout_start: '创建订单',
|
||
completed: '充值完成',
|
||
gopay_validate_pin: '校验 PIN',
|
||
otp_ready: '等待 PIN',
|
||
otp_submitted_local: 'OTP 已提交',
|
||
payment_processing: '支付处理中',
|
||
pin_submitted_local: 'PIN 已提交',
|
||
sms_otp_wait: '等待短信 OTP',
|
||
whatsapp_otp_wait: '等待 WhatsApp OTP',
|
||
};
|
||
const GPC_WAITING_FOR_LABELS = {
|
||
auto_otp: '自动 OTP',
|
||
otp: 'OTP',
|
||
pin: 'PIN',
|
||
};
|
||
const PAYMENT_METHOD_CONFIGS = {
|
||
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
||
id: PLUS_PAYMENT_METHOD_PAYPAL,
|
||
label: 'PayPal',
|
||
selectMessageType: 'PLUS_CHECKOUT_SELECT_PAYPAL',
|
||
redirectPattern: /paypal\./i,
|
||
},
|
||
[PLUS_PAYMENT_METHOD_GOPAY]: {
|
||
id: PLUS_PAYMENT_METHOD_GOPAY,
|
||
label: 'GoPay',
|
||
selectMessageType: 'PLUS_CHECKOUT_SELECT_GOPAY',
|
||
redirectPattern: /gopay|gojek|midtrans|xendit|stripe|checkout/i,
|
||
},
|
||
};
|
||
const MEIGUODIZHI_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz';
|
||
const MEIGUODIZHI_COUNTRY_CONFIG = {
|
||
AR: { path: '/ar-address', city: 'Buenos Aires', aliases: ['ar', 'argentina', '阿根廷'] },
|
||
AU: { path: '/au-address', city: 'Sydney', aliases: ['au', 'aus', 'australia', '澳大利亚'] },
|
||
CA: { path: '/ca-address', city: 'Toronto', aliases: ['ca', 'canada', '加拿大'] },
|
||
CN: { path: '/cn-address', city: 'Shanghai', aliases: ['cn', 'china', '中国'] },
|
||
DE: { path: '/de-address', city: 'Berlin', aliases: ['de', 'deu', 'germany', 'deutschland', '德国'] },
|
||
ES: { path: '/es-address', city: 'Madrid', aliases: ['es', 'esp', 'spain', '西班牙'] },
|
||
FR: { path: '/fr-address', city: 'Paris', aliases: ['fr', 'fra', 'france', '法国'] },
|
||
GB: { path: '/uk-address', city: 'London', aliases: ['gb', 'uk', 'united kingdom', 'britain', 'england', '英国'] },
|
||
HK: { path: '/hk-address', city: 'Hong Kong', aliases: ['hk', 'hong kong', '香港'] },
|
||
ID: { path: '/id-address', city: 'Jakarta', aliases: ['id', 'indonesia', '印度尼西亚', '印尼'] },
|
||
IT: { path: '/it-address', city: 'Rome', aliases: ['it', 'ita', 'italy', '意大利'] },
|
||
JP: { path: '/jp-address', city: 'Tokyo', aliases: ['jp', 'jpn', 'japan', '日本', '日本国'] },
|
||
KR: { path: '/kr-address', city: 'Seoul', aliases: ['kr', 'kor', 'korea', 'south korea', '韩国'] },
|
||
MY: { path: '/my-address', city: 'Kuala Lumpur', aliases: ['my', 'malaysia', '马来西亚'] },
|
||
NL: { path: '/nl-address', city: 'Amsterdam', aliases: ['nl', 'netherlands', 'holland', '荷兰'] },
|
||
PH: { path: '/ph-address', city: 'Manila', aliases: ['ph', 'philippines', '菲律宾'] },
|
||
RU: { path: '/ru-address', city: 'Moscow', aliases: ['ru', 'russia', '俄罗斯'] },
|
||
SG: { path: '/sg-address', city: 'Singapore', aliases: ['sg', 'singapore', '新加坡'] },
|
||
TH: { path: '/th-address', city: 'Bangkok', aliases: ['th', 'thailand', '泰国'] },
|
||
TR: { path: '/tr-address', city: 'Istanbul', aliases: ['tr', 'turkey', 'turkiye', '土耳其'] },
|
||
TW: { path: '/tw-address', city: 'Taipei', aliases: ['tw', 'taiwan', '台湾'] },
|
||
US: { path: '/', city: 'New York', aliases: ['us', 'usa', 'united states', 'united states of america', 'america', '美国'] },
|
||
VN: { path: '/vn-address', city: 'Ho Chi Minh City', aliases: ['vn', 'vietnam', '越南'] },
|
||
};
|
||
|
||
function createPlusCheckoutBillingExecutor(deps = {}) {
|
||
const {
|
||
addLog: rawAddLog = async () => {},
|
||
broadcastDataUpdate,
|
||
chrome,
|
||
completeNodeFromBackground,
|
||
ensureContentScriptReadyOnTabUntilStopped,
|
||
fetch: fetchImpl = null,
|
||
generateRandomName,
|
||
getAddressSeedForCountry,
|
||
getState,
|
||
getTabId,
|
||
isTabAlive,
|
||
markCurrentRegistrationAccountUsed,
|
||
queryTabsInAutomationWindow = null,
|
||
setState,
|
||
sleepWithStop,
|
||
waitForTabCompleteUntilStopped,
|
||
probeIpProxyExit = null,
|
||
throwIfStopped = () => {},
|
||
} = deps;
|
||
|
||
function addLog(message, level = 'info', options = {}) {
|
||
return rawAddLog(message, level, {
|
||
step: 7,
|
||
stepKey: 'plus-checkout-billing',
|
||
...(options && typeof options === 'object' ? options : {}),
|
||
});
|
||
}
|
||
|
||
function isPlusCheckoutUrl(url = '') {
|
||
return PLUS_CHECKOUT_URL_PATTERN.test(String(url || ''));
|
||
}
|
||
|
||
function normalizeText(value = '') {
|
||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function isGpcHelperCheckout(state = {}) {
|
||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER
|
||
|| (normalizeText(state?.plusCheckoutSource) === PLUS_PAYMENT_METHOD_GPC_HELPER
|
||
&& Boolean(state?.gopayHelperTaskId || state?.gopayHelperReferenceId));
|
||
}
|
||
|
||
function compactCountryText(value = '') {
|
||
return normalizeText(value).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||
}
|
||
|
||
function normalizePlusPaymentMethod(value = '') {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
|
||
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
|
||
}
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||
return PLUS_PAYMENT_METHOD_GPC_HELPER;
|
||
}
|
||
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
|
||
}
|
||
|
||
function normalizeGpcHelperPhoneMode(value = '') {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
|
||
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
|
||
}
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||
? GPC_HELPER_PHONE_MODE_AUTO
|
||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||
}
|
||
|
||
function formatGpcRemoteStageLabel(stage = '') {
|
||
const normalized = String(stage || '').trim().toLowerCase();
|
||
if (!normalized) {
|
||
return '';
|
||
}
|
||
return GPC_REMOTE_STAGE_LABELS[normalized] || normalized;
|
||
}
|
||
|
||
function formatGpcWaitingForLabel(waitingFor = '') {
|
||
const normalized = String(waitingFor || '').trim().toLowerCase();
|
||
if (!normalized) {
|
||
return '';
|
||
}
|
||
return GPC_WAITING_FOR_LABELS[normalized] || normalized.toUpperCase();
|
||
}
|
||
|
||
function formatGpcTaskStatusLog(task = {}) {
|
||
const statusText = String(task?.status_text || task?.statusText || '').trim();
|
||
const status = String(task?.status || '').trim();
|
||
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
|
||
const stageText = formatGpcRemoteStageLabel(remoteStage);
|
||
const waitingForText = formatGpcWaitingForLabel(task?.api_waiting_for || task?.apiWaitingFor || '');
|
||
const mainText = stageText || statusText || status || '处理中';
|
||
const parts = [`步骤 7:GPC 任务状态:${mainText}`];
|
||
if (waitingForText && !mainText.includes(waitingForText)) {
|
||
parts.push(`,等待 ${waitingForText}`);
|
||
}
|
||
return parts.join('');
|
||
}
|
||
|
||
function getGpcHelperPhoneMode(state = {}, task = null) {
|
||
return normalizeGpcHelperPhoneMode(
|
||
task?.phone_mode
|
||
|| task?.phoneMode
|
||
|| state?.gopayHelperPhoneMode
|
||
|| state?.phoneMode
|
||
);
|
||
}
|
||
|
||
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
|
||
}
|
||
|
||
function normalizeGpcHelperBaseUrl(apiUrl = '') {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.normalizeGpcHelperBaseUrl) {
|
||
return rootScope.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl);
|
||
}
|
||
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
||
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
||
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
|
||
normalized = normalized.replace(/\/api\/gp\/tasks(?:\/[^/?#]+)?(?:\/(?:otp|pin|stop))?(?:\?.*)?$/i, '');
|
||
normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, '');
|
||
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
|
||
normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, '');
|
||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||
}
|
||
|
||
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
|
||
const fetcher = typeof fetchImpl === 'function'
|
||
? fetchImpl
|
||
: (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
|
||
if (typeof fetcher !== 'function') {
|
||
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
|
||
}
|
||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||
const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000);
|
||
let didTimeout = false;
|
||
let timer = null;
|
||
const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`);
|
||
const timeoutPromise = new Promise((_, reject) => {
|
||
timer = setTimeout(() => {
|
||
didTimeout = true;
|
||
reject(buildTimeoutError());
|
||
if (controller) {
|
||
controller.abort();
|
||
}
|
||
}, effectiveTimeoutMs);
|
||
});
|
||
try {
|
||
const response = await Promise.race([
|
||
fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }),
|
||
timeoutPromise,
|
||
]);
|
||
const data = await Promise.race([
|
||
response.json().catch(() => ({})),
|
||
timeoutPromise,
|
||
]);
|
||
return { response, data };
|
||
} catch (error) {
|
||
if (didTimeout || error?.name === 'AbortError') {
|
||
throw buildTimeoutError();
|
||
}
|
||
throw error;
|
||
} finally {
|
||
if (timer) clearTimeout(timer);
|
||
}
|
||
}
|
||
|
||
function buildGpcOtpPayload(input = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcOtpPayload) {
|
||
return rootScope.GoPayUtils.buildGpcOtpPayload(input);
|
||
}
|
||
const payload = {
|
||
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
|
||
otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''),
|
||
};
|
||
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
|
||
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
||
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
||
if (flowId) payload.flow_id = flowId;
|
||
if (gopayGuid) payload.gopay_guid = gopayGuid;
|
||
if (redirectUrl) payload.redirect_url = redirectUrl;
|
||
return payload;
|
||
}
|
||
|
||
function buildGpcOtpRetryPayload(input = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcOtpRetryPayload) {
|
||
return rootScope.GoPayUtils.buildGpcOtpRetryPayload(input);
|
||
}
|
||
const basePayload = buildGpcOtpPayload(input);
|
||
return { ...basePayload, code: basePayload.otp };
|
||
}
|
||
|
||
function buildGpcPinPayload(input = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcPinPayload) {
|
||
return rootScope.GoPayUtils.buildGpcPinPayload(input);
|
||
}
|
||
const payload = {
|
||
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
|
||
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
|
||
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
|
||
pin: String(input.pin ?? '').trim().replace(/[^\d]/g, ''),
|
||
};
|
||
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
||
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
||
if (flowId) payload.flow_id = flowId;
|
||
if (redirectUrl) payload.redirect_url = redirectUrl;
|
||
return payload;
|
||
}
|
||
|
||
function buildGpcPinRetryPayload(input = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcPinRetryPayload) {
|
||
return rootScope.GoPayUtils.buildGpcPinRetryPayload(input);
|
||
}
|
||
const basePayload = buildGpcPinPayload(input);
|
||
return { ...basePayload, challengeId: basePayload.challenge_id };
|
||
}
|
||
|
||
function getGpcResponseErrorDetail(payload = {}, status = 0) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.extractGpcResponseErrorDetail) {
|
||
return rootScope.GoPayUtils.extractGpcResponseErrorDetail(payload, status);
|
||
}
|
||
if (payload && typeof payload === 'object') {
|
||
return payload?.data?.detail || payload.detail || payload.message || payload.error || payload.error_description || payload.reason || `HTTP ${status || 0}`;
|
||
}
|
||
return `HTTP ${status || 0}`;
|
||
}
|
||
|
||
function unwrapGpcResponse(payload = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.unwrapGpcResponse) {
|
||
return rootScope.GoPayUtils.unwrapGpcResponse(payload);
|
||
}
|
||
if (payload && typeof payload === 'object' && !Array.isArray(payload)
|
||
&& Object.prototype.hasOwnProperty.call(payload, 'data')
|
||
&& (Object.prototype.hasOwnProperty.call(payload, 'code') || Object.prototype.hasOwnProperty.call(payload, 'message'))) {
|
||
return payload.data ?? {};
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
function isGpcUnifiedResponseOk(payload = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.isGpcUnifiedResponseOk) {
|
||
return rootScope.GoPayUtils.isGpcUnifiedResponseOk(payload);
|
||
}
|
||
if (!payload || typeof payload !== 'object' || !Object.prototype.hasOwnProperty.call(payload, 'code')) {
|
||
return true;
|
||
}
|
||
const code = Number(payload.code);
|
||
return Number.isFinite(code) ? code >= 200 && code < 300 : String(payload.code || '').trim() === '200';
|
||
}
|
||
|
||
function buildGpcTaskQueryUrl(apiUrl = '', taskId = '') {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcTaskQueryUrl) {
|
||
return rootScope.GoPayUtils.buildGpcTaskQueryUrl(apiUrl, taskId);
|
||
}
|
||
const baseUrl = normalizeGpcHelperBaseUrl(apiUrl);
|
||
return `${baseUrl}/api/gp/tasks/${encodeURIComponent(String(taskId || '').trim())}`;
|
||
}
|
||
|
||
function buildGpcTaskActionUrl(apiUrl = '', taskId = '', action = '') {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcTaskActionUrl) {
|
||
return rootScope.GoPayUtils.buildGpcTaskActionUrl(apiUrl, taskId, action);
|
||
}
|
||
const baseUrl = normalizeGpcHelperBaseUrl(apiUrl);
|
||
const normalizedAction = String(action || '').trim().replace(/^\/+|\/+$/g, '');
|
||
return `${baseUrl}/api/gp/tasks/${encodeURIComponent(String(taskId || '').trim())}/${normalizedAction}`;
|
||
}
|
||
|
||
function buildGpcTaskOtpPayload(input = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcTaskOtpPayload) {
|
||
return rootScope.GoPayUtils.buildGpcTaskOtpPayload(input);
|
||
}
|
||
return {
|
||
otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''),
|
||
};
|
||
}
|
||
|
||
function buildGpcTaskPinPayload(input = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcTaskPinPayload) {
|
||
return rootScope.GoPayUtils.buildGpcTaskPinPayload(input);
|
||
}
|
||
return {
|
||
pin: String(input.pin ?? '').trim().replace(/[^\d]/g, ''),
|
||
};
|
||
}
|
||
|
||
function buildGpcApiKeyHeaders(apiKey = '', extraHeaders = {}) {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.buildGpcApiKeyHeaders) {
|
||
return rootScope.GoPayUtils.buildGpcApiKeyHeaders(apiKey, extraHeaders);
|
||
}
|
||
const headers = {
|
||
...(extraHeaders && typeof extraHeaders === 'object' ? extraHeaders : {}),
|
||
};
|
||
const normalizedApiKey = String(apiKey || '').trim();
|
||
if (normalizedApiKey) {
|
||
headers['X-API-Key'] = normalizedApiKey;
|
||
}
|
||
return headers;
|
||
}
|
||
|
||
function normalizeGpcTaskData(payload = {}) {
|
||
const data = unwrapGpcResponse(payload);
|
||
const task = data && typeof data === 'object' && !Array.isArray(data) ? { ...data } : {};
|
||
task.task_id = String(task.task_id || task.taskId || '').trim();
|
||
task.status = String(task.status || '').trim().toLowerCase();
|
||
task.status_text = String(task.status_text || task.statusText || '').trim();
|
||
task.phone_mode = normalizeGpcHelperPhoneMode(task.phone_mode || task.phoneMode || '');
|
||
task.remote_stage = String(task.remote_stage || task.remoteStage || '').trim().toLowerCase();
|
||
task.api_waiting_for = String(task.api_waiting_for || task.apiWaitingFor || '').trim().toLowerCase();
|
||
task.api_input_deadline_at = String(task.api_input_deadline_at || task.apiInputDeadlineAt || '').trim();
|
||
task.api_input_wait_seconds = Math.max(0, Number(task.api_input_wait_seconds ?? task.apiInputWaitSeconds) || 0);
|
||
task.last_input_error = String(task.last_input_error || task.lastInputError || '').trim();
|
||
task.otp_invalid_count = Math.max(0, Number(task.otp_invalid_count ?? task.otpInvalidCount) || 0);
|
||
task.failure_stage = String(task.failure_stage || task.failureStage || '').trim();
|
||
task.failure_detail = String(task.failure_detail || task.failureDetail || '').trim();
|
||
task.error_message = String(task.error_message || task.errorMessage || '').trim();
|
||
return task;
|
||
}
|
||
|
||
async function setGpcTaskState(taskData = {}) {
|
||
const task = normalizeGpcTaskData(taskData);
|
||
const updates = {
|
||
gopayHelperTaskId: task.task_id,
|
||
gopayHelperTaskStatus: task.status,
|
||
gopayHelperStatusText: task.status_text,
|
||
gopayHelperRemoteStage: task.remote_stage,
|
||
gopayHelperApiWaitingFor: task.api_waiting_for,
|
||
gopayHelperApiInputDeadlineAt: task.api_input_deadline_at,
|
||
gopayHelperApiInputWaitSeconds: task.api_input_wait_seconds,
|
||
gopayHelperLastInputError: task.last_input_error,
|
||
gopayHelperOtpInvalidCount: task.otp_invalid_count,
|
||
gopayHelperFailureStage: task.failure_stage,
|
||
gopayHelperFailureDetail: task.failure_detail,
|
||
gopayHelperTaskPayload: task && typeof task === 'object' && !Array.isArray(task) ? task : null,
|
||
};
|
||
await setState(updates);
|
||
if (typeof broadcastDataUpdate === 'function') {
|
||
broadcastDataUpdate(updates);
|
||
}
|
||
return task;
|
||
}
|
||
|
||
async function fetchGpcTaskStatus(apiUrl, taskId, apiKey) {
|
||
const requestUrl = buildGpcTaskQueryUrl(apiUrl, taskId);
|
||
const { response, data } = await fetchJsonWithTimeout(requestUrl, {
|
||
method: 'GET',
|
||
headers: buildGpcApiKeyHeaders(apiKey, { Accept: 'application/json' }),
|
||
}, 30000);
|
||
if (!response?.ok || !isGpcUnifiedResponseOk(data)) {
|
||
throw new Error(getGpcResponseErrorDetail(data, response?.status || 0));
|
||
}
|
||
return setGpcTaskState(data);
|
||
}
|
||
|
||
async function postGpcTaskAction(apiUrl, taskId, action, payload = {}, apiKey = '', timeoutMs = 30000) {
|
||
const requestUrl = buildGpcTaskActionUrl(apiUrl, taskId, action);
|
||
const { response, data } = await fetchJsonWithTimeout(requestUrl, {
|
||
method: 'POST',
|
||
headers: buildGpcApiKeyHeaders(apiKey, {
|
||
Accept: 'application/json',
|
||
'Content-Type': 'application/json',
|
||
}),
|
||
body: JSON.stringify(payload),
|
||
}, timeoutMs);
|
||
if (!response?.ok || !isGpcUnifiedResponseOk(data)) {
|
||
throw new Error(getGpcResponseErrorDetail(data, response?.status || 0));
|
||
}
|
||
return setGpcTaskState(data);
|
||
}
|
||
|
||
async function postGpcJsonWithFallback(apiUrl, endpointPath, primaryPayload, fallbackPayload, timeoutMs = 30000) {
|
||
const requestUrl = `${apiUrl}${endpointPath}`;
|
||
const send = (payload) => fetchJsonWithTimeout(requestUrl, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
}, timeoutMs);
|
||
const firstResponse = await send(primaryPayload);
|
||
if (firstResponse?.response?.ok || !fallbackPayload) {
|
||
return { ...firstResponse, retried: false, payload: primaryPayload };
|
||
}
|
||
const status = Number(firstResponse?.response?.status || 0);
|
||
if (status !== 400 && status !== 422) {
|
||
return { ...firstResponse, retried: false, payload: primaryPayload };
|
||
}
|
||
const firstDetail = getGpcResponseErrorDetail(firstResponse?.data, status);
|
||
await addLog(`步骤 7:GPC 接口返回 ${status}(${firstDetail}),使用兼容字段重试。`, 'warn');
|
||
const secondResponse = await send(fallbackPayload);
|
||
return {
|
||
...secondResponse,
|
||
retried: true,
|
||
payload: fallbackPayload,
|
||
firstError: firstDetail,
|
||
firstStatus: status,
|
||
};
|
||
}
|
||
|
||
function getStateInternal() {
|
||
if (typeof getState === 'function') {
|
||
return getState();
|
||
}
|
||
return Promise.resolve({});
|
||
}
|
||
|
||
function normalizeLocalSmsHelperBaseUrl(value = '') {
|
||
const fallback = 'http://127.0.0.1:18767';
|
||
const rawValue = String(value || fallback).trim();
|
||
try {
|
||
const parsed = new URL(rawValue);
|
||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||
return fallback;
|
||
}
|
||
const endpointPath = parsed.pathname.replace(/\/+$/g, '') || '/';
|
||
if (['/otp', '/latest-otp', '/health'].includes(endpointPath)) {
|
||
parsed.pathname = '';
|
||
parsed.search = '';
|
||
parsed.hash = '';
|
||
}
|
||
return parsed.toString().replace(/\/$/, '');
|
||
} catch {
|
||
return fallback;
|
||
}
|
||
}
|
||
|
||
function normalizeIncomingGpcSmsOtp(payload = {}) {
|
||
const candidates = [
|
||
payload?.otp,
|
||
payload?.code,
|
||
payload?.sms_code,
|
||
payload?.smsCode,
|
||
payload?.verification_code,
|
||
payload?.verificationCode,
|
||
];
|
||
for (const candidate of candidates) {
|
||
const normalized = String(candidate || '').trim().replace(/[^\d]/g, '');
|
||
if (/^\d{4,8}$/.test(normalized)) {
|
||
return normalized;
|
||
}
|
||
}
|
||
const messageText = String(payload?.message_text || payload?.messageText || payload?.text || '').trim();
|
||
if (messageText) {
|
||
const match = messageText.match(/(?:OTP\s*[::]?\s*|#)(\d{4,8})\b|\b(\d{6})\b/i);
|
||
if (match) {
|
||
return String(match[1] || match[2] || '').trim();
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function normalizeGpcOtpChannel(value = '') {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
|
||
return rootScope.GoPayUtils.normalizeGpcOtpChannel(value);
|
||
}
|
||
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
|
||
}
|
||
|
||
function normalizeEpochMilliseconds(value = 0) {
|
||
const rawValue = String(value ?? '').trim();
|
||
if (!rawValue) {
|
||
return 0;
|
||
}
|
||
const numeric = Number(rawValue);
|
||
if (Number.isFinite(numeric) && numeric > 0) {
|
||
return Math.floor(numeric < 100000000000 ? numeric * 1000 : numeric);
|
||
}
|
||
const parsed = Date.parse(rawValue);
|
||
return Number.isFinite(parsed) ? parsed : 0;
|
||
}
|
||
|
||
function normalizeLocalSmsHelperCountryCode(value = '') {
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
if (rootScope.GoPayUtils?.normalizeGoPayCountryCode) {
|
||
return rootScope.GoPayUtils.normalizeGoPayCountryCode(value || '+86');
|
||
}
|
||
const digits = String(value || '+86').replace(/\D/g, '');
|
||
return digits ? `+${digits}` : '+86';
|
||
}
|
||
|
||
function normalizeLocalSmsHelperPhoneE164(phone = '', countryCode = '+86') {
|
||
const rawPhone = String(phone || '').trim();
|
||
if (!rawPhone) {
|
||
return '';
|
||
}
|
||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||
const normalizedCountryCode = normalizeLocalSmsHelperCountryCode(countryCode);
|
||
const normalizedPhone = rootScope.GoPayUtils?.normalizeGoPayPhone
|
||
? rootScope.GoPayUtils.normalizeGoPayPhone(rawPhone)
|
||
: rawPhone.replace(/[^\d+]/g, '');
|
||
const phoneDigits = normalizedPhone.replace(/\D/g, '');
|
||
if (!phoneDigits) {
|
||
return '';
|
||
}
|
||
if (normalizedPhone.startsWith('+')) {
|
||
return `+${phoneDigits}`;
|
||
}
|
||
const countryDigits = normalizedCountryCode.replace(/\D/g, '') || '86';
|
||
let nationalNumber = phoneDigits;
|
||
if (countryDigits && nationalNumber.startsWith(countryDigits) && nationalNumber.length > countryDigits.length) {
|
||
nationalNumber = nationalNumber.slice(countryDigits.length);
|
||
}
|
||
return `+${countryDigits}${nationalNumber}`;
|
||
}
|
||
|
||
function buildLocalSmsHelperOtpUrl(state = {}, taskId = '', options = {}) {
|
||
const baseUrl = normalizeLocalSmsHelperBaseUrl(state?.gopayHelperLocalSmsHelperUrl);
|
||
const url = new URL(`${baseUrl}/latest-otp`);
|
||
const normalizedTaskId = String(taskId || '').trim();
|
||
const phoneNumber = normalizeLocalSmsHelperPhoneE164(
|
||
state?.gopayHelperPhoneNumber,
|
||
state?.gopayHelperCountryCode || '+86'
|
||
);
|
||
const afterOverrideMs = normalizeEpochMilliseconds(options?.afterMs || options?.after_ms || 0);
|
||
const orderCreatedAt = normalizeEpochMilliseconds(
|
||
state?.gopayHelperOrderCreatedAt
|
||
|| state?.gopayHelperTaskPayload?.created_at
|
||
|| state?.gopayHelperTaskPayload?.createdAt
|
||
|| state?.gopayHelperStartPayload?.order_created_at
|
||
|| state?.gopayHelperStartPayload?.orderCreatedAt
|
||
|| state?.gopayHelperStartPayload?.created_at
|
||
|| state?.gopayHelperStartPayload?.createdAt
|
||
);
|
||
if (normalizedTaskId) {
|
||
url.searchParams.set('task_id', normalizedTaskId);
|
||
url.searchParams.set('reference_id', normalizedTaskId);
|
||
}
|
||
if (phoneNumber) {
|
||
url.searchParams.set('phone', phoneNumber);
|
||
}
|
||
url.searchParams.set('consume', '1');
|
||
const effectiveAfterMs = Math.max(orderCreatedAt, afterOverrideMs);
|
||
if (effectiveAfterMs > 0) {
|
||
url.searchParams.set('after_ms', String(effectiveAfterMs));
|
||
}
|
||
return url.toString();
|
||
}
|
||
|
||
async function pollLocalSmsHelperOtp(state = {}, taskId = '', options = {}) {
|
||
const timeoutSeconds = Math.max(1, Math.min(300, Number(options?.timeoutSeconds ?? options?.timeout_seconds ?? state?.gopayHelperLocalSmsTimeoutSeconds) || 90));
|
||
const pollIntervalSeconds = Math.max(1, Math.min(30, Number(state?.gopayHelperLocalSmsPollIntervalSeconds) || 2));
|
||
const singleAttempt = Boolean(options?.singleAttempt || options?.single_attempt);
|
||
const requestTimeoutMs = Math.max(1000, Math.min(8000, Number(options?.requestTimeoutMs ?? options?.request_timeout_ms) || pollIntervalSeconds * 1000));
|
||
const deadline = Date.now() + timeoutSeconds * 1000;
|
||
const requestUrl = buildLocalSmsHelperOtpUrl(state, taskId, options);
|
||
let lastMessage = '';
|
||
while (Date.now() <= deadline) {
|
||
throwIfStopped();
|
||
try {
|
||
const { response, data } = await fetchJsonWithTimeout(requestUrl, {
|
||
method: 'GET',
|
||
headers: { Accept: 'application/json' },
|
||
}, requestTimeoutMs);
|
||
const otp = normalizeIncomingGpcSmsOtp(data || {});
|
||
if (response?.ok && otp) {
|
||
await setState({
|
||
gopayHelperResolvedOtp: otp,
|
||
gopayHelperSmsOtpPayload: data && typeof data === 'object' && !Array.isArray(data) ? data : null,
|
||
});
|
||
if (typeof broadcastDataUpdate === 'function') {
|
||
broadcastDataUpdate({ gopayHelperResolvedOtp: otp });
|
||
}
|
||
return otp;
|
||
}
|
||
lastMessage = String(data?.message || data?.status || '').trim();
|
||
} catch (error) {
|
||
lastMessage = error?.message || String(error || '未知错误');
|
||
}
|
||
if (singleAttempt) {
|
||
break;
|
||
}
|
||
await sleepWithStop(pollIntervalSeconds * 1000);
|
||
}
|
||
throw new Error(lastMessage || '本地 SMS Helper 等待 OTP 超时。');
|
||
}
|
||
|
||
function buildGpcTaskEndedError(task = {}, fallbackMessage = '') {
|
||
const detail = buildGpcTaskTerminalError(task) || fallbackMessage || 'GPC 任务已结束,请重新创建任务。';
|
||
return new Error(`GPC_TASK_ENDED::${detail}`);
|
||
}
|
||
|
||
function isGpcTaskInputDeadlineExpired(task = {}) {
|
||
const deadlineMs = normalizeEpochMilliseconds(task?.api_input_deadline_at || task?.apiInputDeadlineAt || '');
|
||
return deadlineMs > 0 && Date.now() > deadlineMs;
|
||
}
|
||
|
||
function buildGpcInputDeadlineError(task = {}, label = '输入') {
|
||
const stage = String(task?.remote_stage || '').trim();
|
||
const detail = `${label}提交已超时,请重新创建任务。`;
|
||
return new Error(`GPC_TASK_ENDED::${detail}${stage ? `(${stage})` : ''}`);
|
||
}
|
||
|
||
function normalizeSixDigitOtp(value = '') {
|
||
const otp = String(value || '').trim().replace(/[^\d]/g, '');
|
||
return /^\d{6}$/.test(otp) ? otp : '';
|
||
}
|
||
|
||
function normalizeSixDigitPin(value = '') {
|
||
const pin = String(value || '').trim().replace(/[^\d]/g, '');
|
||
return /^\d{6}$/.test(pin) ? pin : '';
|
||
}
|
||
|
||
function isGpcOtpFormatConflict(error) {
|
||
return /OTP\s*必须是\s*6\s*位数字|OTP.*6.*digit|task_conflict/i.test(error?.message || String(error || ''));
|
||
}
|
||
|
||
async function requestGpcOtpInput({ title = '', message = '', taskId = '', lastInputError = '', inputDeadlineAt = '' }) {
|
||
const existingState = await getStateInternal();
|
||
const existingRequestId = String(existingState?.plusManualConfirmationRequestId || '').trim();
|
||
if (
|
||
existingState?.plusManualConfirmationPending
|
||
&& existingRequestId
|
||
&& String(existingState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay-otp'
|
||
&& String(existingState?.gopayHelperOtpReferenceId || '').trim() === String(taskId || '').trim()
|
||
) {
|
||
return waitForGpcOtpInput(existingRequestId, { inputDeadlineAt });
|
||
}
|
||
|
||
const requestId = `otp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
const payload = {
|
||
plusManualConfirmationPending: true,
|
||
plusManualConfirmationRequestId: requestId,
|
||
plusManualConfirmationStep: 7,
|
||
plusManualConfirmationMethod: 'gopay-otp',
|
||
plusManualConfirmationTitle: title || 'GPC OTP 验证',
|
||
plusManualConfirmationMessage: message || '请输入 OTP 验证码',
|
||
gopayHelperLastInputError: String(lastInputError || '').trim(),
|
||
gopayHelperApiInputDeadlineAt: String(inputDeadlineAt || '').trim(),
|
||
gopayHelperResolvedOtp: '',
|
||
gopayHelperOtpRequestId: requestId,
|
||
gopayHelperOtpReferenceId: taskId,
|
||
};
|
||
await setState(payload);
|
||
if (typeof broadcastDataUpdate === 'function') {
|
||
broadcastDataUpdate(payload);
|
||
}
|
||
return waitForGpcOtpInput(requestId, { inputDeadlineAt });
|
||
}
|
||
|
||
function waitForGpcOtpInput(requestId = '', options = {}) {
|
||
const deadlineMs = normalizeEpochMilliseconds(options?.inputDeadlineAt || options?.input_deadline_at || 0);
|
||
return new Promise((resolve, reject) => {
|
||
const checkInterval = setInterval(async () => {
|
||
try {
|
||
throwIfStopped();
|
||
if (deadlineMs > 0 && Date.now() > deadlineMs) {
|
||
clearInterval(checkInterval);
|
||
const clearPayload = {
|
||
plusManualConfirmationPending: false,
|
||
plusManualConfirmationRequestId: '',
|
||
plusManualConfirmationStep: 0,
|
||
plusManualConfirmationMethod: '',
|
||
plusManualConfirmationTitle: '',
|
||
plusManualConfirmationMessage: '',
|
||
gopayHelperResolvedOtp: '',
|
||
gopayHelperOtpRequestId: '',
|
||
gopayHelperOtpReferenceId: '',
|
||
};
|
||
await setState(clearPayload);
|
||
if (typeof broadcastDataUpdate === 'function') {
|
||
broadcastDataUpdate(clearPayload);
|
||
}
|
||
reject(new Error('OTP 输入超时'));
|
||
return;
|
||
}
|
||
const currentState = await getStateInternal();
|
||
if (!currentState?.plusManualConfirmationPending || currentState?.plusManualConfirmationRequestId !== requestId) {
|
||
clearInterval(checkInterval);
|
||
const resolvedOtp = String(currentState?.gopayHelperResolvedOtp || '').trim().replace(/[^\d]/g, '');
|
||
if (resolvedOtp) {
|
||
resolve(resolvedOtp);
|
||
} else {
|
||
reject(new Error('OTP 输入已取消'));
|
||
}
|
||
}
|
||
} catch (error) {
|
||
clearInterval(checkInterval);
|
||
reject(error);
|
||
}
|
||
}, 500);
|
||
});
|
||
}
|
||
|
||
function isGpcTaskManualMode(task = {}, state = {}) {
|
||
return getGpcHelperPhoneMode(state, task) === GPC_HELPER_PHONE_MODE_MANUAL;
|
||
}
|
||
|
||
function isGpcTaskOtpWait(task = {}, state = {}) {
|
||
return isGpcTaskManualMode(task, state) && task?.api_waiting_for === 'otp';
|
||
}
|
||
|
||
function isGpcTaskPinWait(task = {}, state = {}) {
|
||
return isGpcTaskManualMode(task, state)
|
||
&& (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready');
|
||
}
|
||
|
||
function isGpcTaskTerminal(status = '') {
|
||
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?.failure_stage || task?.failureStage,
|
||
task?.failure_detail || task?.failureDetail,
|
||
task?.error_message || task?.errorMessage,
|
||
].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();
|
||
const failureStage = String(task?.failure_stage || task?.failureStage || '').trim();
|
||
const detail = String(
|
||
task?.error_message
|
||
|| task?.errorMessage
|
||
|| task?.failure_detail
|
||
|| task?.failureDetail
|
||
|| task?.last_input_error
|
||
|| task?.lastInputError
|
||
|| task?.status_text
|
||
|| task?.statusText
|
||
|| task?.message
|
||
|| task?.detail
|
||
|| task?.data?.detail
|
||
|| ''
|
||
).trim();
|
||
if (detail) {
|
||
return failureStage && !detail.includes(failureStage)
|
||
? `${detail}(${failureStage})`
|
||
: detail;
|
||
}
|
||
if (/api_otp_timeout/i.test(remoteStage)) {
|
||
return 'GPC OTP 超时,请重新创建任务';
|
||
}
|
||
if (/api_pin_timeout/i.test(remoteStage)) {
|
||
return 'GPC PIN 超时,请重新创建任务';
|
||
}
|
||
if (failureStage) {
|
||
return `GPC 任务失败:${failureStage}`;
|
||
}
|
||
return `任务状态 ${status || '未知'}`;
|
||
}
|
||
|
||
async function stopGpcTaskBestEffort(apiUrl, taskId, apiKey, reason = '') {
|
||
if (!apiUrl || !taskId || !apiKey) {
|
||
return;
|
||
}
|
||
try {
|
||
const task = await postGpcTaskAction(apiUrl, taskId, 'stop', {}, apiKey, 15000);
|
||
const statusText = task?.status_text || task?.status || '已停止';
|
||
await addLog(`步骤 7:已请求停止 GPC 任务(${statusText})。`, 'warn');
|
||
} catch (error) {
|
||
await addLog(`步骤 7:停止 GPC 任务失败${reason ? `(${reason})` : ''}:${error?.message || String(error || '未知错误')}`, 'warn');
|
||
}
|
||
}
|
||
|
||
async function clearGpcTaskRuntimeState() {
|
||
const updates = {
|
||
plusManualConfirmationPending: false,
|
||
plusManualConfirmationRequestId: '',
|
||
plusManualConfirmationStep: 0,
|
||
plusManualConfirmationMethod: '',
|
||
plusManualConfirmationTitle: '',
|
||
plusManualConfirmationMessage: '',
|
||
gopayHelperTaskId: '',
|
||
gopayHelperTaskStatus: '',
|
||
gopayHelperStatusText: '',
|
||
gopayHelperRemoteStage: '',
|
||
gopayHelperApiWaitingFor: '',
|
||
gopayHelperApiInputDeadlineAt: '',
|
||
gopayHelperApiInputWaitSeconds: 0,
|
||
gopayHelperLastInputError: '',
|
||
gopayHelperOtpInvalidCount: 0,
|
||
gopayHelperFailureStage: '',
|
||
gopayHelperFailureDetail: '',
|
||
gopayHelperTaskPayload: null,
|
||
gopayHelperTaskProgressSignature: '',
|
||
gopayHelperTaskProgressAt: 0,
|
||
gopayHelperTaskProgressTaskId: '',
|
||
gopayHelperPinPayload: null,
|
||
gopayHelperResolvedOtp: '',
|
||
gopayHelperOtpRequestId: '',
|
||
gopayHelperOtpReferenceId: '',
|
||
};
|
||
await setState(updates);
|
||
if (typeof broadcastDataUpdate === 'function') {
|
||
broadcastDataUpdate(updates);
|
||
}
|
||
}
|
||
|
||
function isGpcTaskEndedError(error) {
|
||
return /^GPC_TASK_ENDED::/i.test(error?.message || String(error || ''));
|
||
}
|
||
|
||
async function resolveGpcTaskOtp(state = {}, taskId = '', options = {}) {
|
||
let otp = '';
|
||
const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled);
|
||
const retryCount = Math.max(0, Number(options?.retryCount) || 0);
|
||
const helperAfterMs = normalizeEpochMilliseconds(options?.afterMs || options?.after_ms || 0);
|
||
const lastInputError = String(options?.lastInputError || options?.last_input_error || '').trim();
|
||
if (useLocalSmsHelper) {
|
||
try {
|
||
await addLog(
|
||
retryCount > 0 || lastInputError
|
||
? `步骤 7:${lastInputError || 'OTP 校验未通过'},正在从本地 OTP Helper 等待新的 GPC OTP...`
|
||
: '步骤 7:正在从本地 OTP Helper 等待 GPC OTP...',
|
||
'info'
|
||
);
|
||
otp = await pollLocalSmsHelperOtp(state, taskId, {
|
||
afterMs: helperAfterMs,
|
||
singleAttempt: true,
|
||
requestTimeoutMs: 2000,
|
||
timeoutSeconds: 2,
|
||
});
|
||
await addLog('步骤 7:本地 OTP Helper 已读取到 GPC OTP,准备提交验证。', 'ok');
|
||
} catch (error) {
|
||
await addLog(`步骤 7:本地 OTP Helper 暂未读取到新 OTP:${error?.message || String(error || '未知错误')},继续等待远端任务状态更新。`, 'warn');
|
||
}
|
||
}
|
||
if (otp) {
|
||
return otp;
|
||
}
|
||
if (useLocalSmsHelper) {
|
||
return '';
|
||
}
|
||
await addLog('步骤 7:等待用户输入 OTP...', 'info');
|
||
return requestGpcOtpInput({
|
||
title: 'GPC OTP 验证',
|
||
message: retryCount > 0 || lastInputError
|
||
? `${lastInputError || '上一次 OTP 校验未通过'},请重新输入正确的 OTP 验证码(task_id: ${taskId})`
|
||
: `请输入收到的 OTP 验证码(task_id: ${taskId})`,
|
||
lastInputError,
|
||
inputDeadlineAt: options?.inputDeadlineAt || options?.input_deadline_at || '',
|
||
taskId,
|
||
});
|
||
}
|
||
|
||
async function executeGpcHelperBilling(state = {}) {
|
||
const taskId = String(state?.gopayHelperTaskId || '').trim();
|
||
const apiUrl = normalizeGpcHelperBaseUrl(state?.gopayHelperApiUrl || '');
|
||
const apiKey = String(
|
||
state?.gopayHelperApiKey
|
||
|| state?.gpcApiKey
|
||
|| state?.apiKey
|
||
|| ''
|
||
).trim();
|
||
const deadline = Date.now() + Math.max(120, Math.min(1200, Number(state?.gopayHelperTaskTimeoutSeconds) || 900)) * 1000;
|
||
let otpSubmitCount = 0;
|
||
let otpLastSubmittedAt = 0;
|
||
let lastSubmittedOtp = '';
|
||
let pinSubmitted = false;
|
||
let terminalReached = false;
|
||
let lastProgressSignature = String(state?.gopayHelperTaskProgressSignature || '').trim();
|
||
let lastProgressAt = normalizeEpochMilliseconds(state?.gopayHelperTaskProgressAt || 0) || Date.now();
|
||
let lastProgressTaskId = String(state?.gopayHelperTaskProgressTaskId || '').trim();
|
||
if (lastProgressTaskId !== taskId) {
|
||
lastProgressSignature = '';
|
||
lastProgressAt = Date.now();
|
||
lastProgressTaskId = '';
|
||
}
|
||
const staleStatusTimeoutMs = getGpcTaskStaleStatusTimeoutMs(state);
|
||
|
||
if (!taskId) {
|
||
throw new Error('步骤 7:GPC 模式缺少 task_id,请先执行步骤 6。');
|
||
}
|
||
if (!apiUrl) {
|
||
throw new Error('步骤 7:GPC 模式缺少 API 地址。');
|
||
}
|
||
if (!apiKey) {
|
||
throw new Error('步骤 7:GPC 模式缺少 API Key。');
|
||
}
|
||
|
||
const configuredPhoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode || GPC_HELPER_PHONE_MODE_MANUAL);
|
||
const rawPin = String(state?.gopayHelperPin || '').trim();
|
||
const pinDigits = rawPin.replace(/[^\d]/g, '');
|
||
const pin = normalizeSixDigitPin(rawPin);
|
||
if (configuredPhoneMode === GPC_HELPER_PHONE_MODE_MANUAL && !pin) {
|
||
if (taskId && apiUrl && apiKey) {
|
||
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
|
||
}
|
||
throw new Error(pinDigits
|
||
? '步骤 7:GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
|
||
: '步骤 7:GPC 手动模式缺少 PIN 配置。');
|
||
}
|
||
|
||
await addLog(`步骤 7:GPC ${configuredPhoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式开始轮询任务(task_id: ${taskId})...`, 'info');
|
||
try {
|
||
while (Date.now() <= deadline) {
|
||
throwIfStopped();
|
||
const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey);
|
||
await addLog(formatGpcTaskStatusLog(task), 'info');
|
||
|
||
if (task.status === 'completed') {
|
||
terminalReached = true;
|
||
await setState({
|
||
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||
});
|
||
await addLog('步骤 7:GPC 任务已完成,准备继续下一步。', 'ok');
|
||
await completeNodeFromBackground('plus-checkout-billing', {
|
||
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (['failed', 'expired', 'discarded'].includes(task.status)) {
|
||
terminalReached = true;
|
||
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
|
||
}
|
||
|
||
if (shouldWatchGpcTaskProgress(task, state)) {
|
||
const progressSignature = buildGpcTaskProgressSignature(task);
|
||
const now = Date.now();
|
||
if (progressSignature && (progressSignature !== lastProgressSignature || lastProgressTaskId !== taskId)) {
|
||
lastProgressSignature = progressSignature;
|
||
lastProgressAt = now;
|
||
lastProgressTaskId = taskId;
|
||
await setState({
|
||
gopayHelperTaskProgressSignature: progressSignature,
|
||
gopayHelperTaskProgressAt: now,
|
||
gopayHelperTaskProgressTaskId: taskId,
|
||
});
|
||
} else if (progressSignature && now - lastProgressAt >= staleStatusTimeoutMs) {
|
||
throw buildGpcTaskStaleStatusError(task, staleStatusTimeoutMs);
|
||
}
|
||
} else {
|
||
lastProgressSignature = '';
|
||
lastProgressAt = Date.now();
|
||
lastProgressTaskId = '';
|
||
await setState({
|
||
gopayHelperTaskProgressSignature: '',
|
||
gopayHelperTaskProgressAt: 0,
|
||
gopayHelperTaskProgressTaskId: taskId,
|
||
});
|
||
}
|
||
|
||
if (isGpcTaskOtpWait(task, state)) {
|
||
if (isGpcTaskInputDeadlineExpired(task)) {
|
||
throw buildGpcInputDeadlineError(task, 'OTP');
|
||
}
|
||
if (task.last_input_error) {
|
||
await addLog(
|
||
`步骤 7:${task.last_input_error}${task.otp_invalid_count ? `(OTP 错误 ${task.otp_invalid_count} 次)` : ''}`,
|
||
'warn'
|
||
);
|
||
}
|
||
let otp = '';
|
||
try {
|
||
otp = await resolveGpcTaskOtp(state, taskId, {
|
||
retryCount: otpSubmitCount,
|
||
afterMs: otpLastSubmittedAt,
|
||
lastInputError: task.last_input_error,
|
||
inputDeadlineAt: task.api_input_deadline_at,
|
||
});
|
||
} catch (error) {
|
||
if (/OTP\s*输入(?:已取消|超时)/i.test(error?.message || String(error || ''))) {
|
||
throw new Error(`GPC_TASK_ENDED::${error?.message || 'OTP 输入已取消'},已结束当前 GPC 任务。`);
|
||
}
|
||
throw error;
|
||
}
|
||
if (!otp) {
|
||
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
|
||
continue;
|
||
}
|
||
const normalizedOtp = normalizeSixDigitOtp(otp);
|
||
if (!normalizedOtp) {
|
||
await addLog('步骤 7:OTP 必须是 6 位数字,等待重新输入。', 'warn');
|
||
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
|
||
continue;
|
||
}
|
||
if (task.last_input_error && lastSubmittedOtp && normalizedOtp === lastSubmittedOtp) {
|
||
await addLog('步骤 7:本地 OTP Helper 返回的仍是上次已失败 OTP,等待新的验证码。', 'warn');
|
||
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
|
||
continue;
|
||
}
|
||
await addLog('步骤 7:正在提交 OTP...', 'info');
|
||
try {
|
||
await postGpcTaskAction(
|
||
apiUrl,
|
||
taskId,
|
||
'otp',
|
||
buildGpcTaskOtpPayload({ otp: normalizedOtp }),
|
||
apiKey,
|
||
30000
|
||
);
|
||
} catch (error) {
|
||
if (isGpcOtpFormatConflict(error)) {
|
||
await addLog(`步骤 7:OTP 提交被拒绝:${error?.message || String(error || 'OTP 格式错误')},等待重新输入。`, 'warn');
|
||
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
|
||
continue;
|
||
}
|
||
throw error;
|
||
}
|
||
otpSubmitCount += 1;
|
||
otpLastSubmittedAt = Date.now();
|
||
lastSubmittedOtp = normalizedOtp;
|
||
await addLog('步骤 7:OTP 已提交,继续等待 GPC 任务状态更新。', 'ok');
|
||
} else if (isGpcTaskPinWait(task, state) && !pinSubmitted) {
|
||
if (isGpcTaskInputDeadlineExpired(task)) {
|
||
throw buildGpcInputDeadlineError(task, 'PIN');
|
||
}
|
||
await addLog('步骤 7:正在提交 PIN...', 'info');
|
||
let pinTask = null;
|
||
try {
|
||
pinTask = await postGpcTaskAction(
|
||
apiUrl,
|
||
taskId,
|
||
'pin',
|
||
buildGpcTaskPinPayload({ pin }),
|
||
apiKey,
|
||
30000
|
||
);
|
||
} catch (error) {
|
||
throw new Error(`GPC_TASK_ENDED::${error?.message || String(error || 'PIN 提交失败,请重新创建任务。')}`);
|
||
}
|
||
pinSubmitted = true;
|
||
await setState({
|
||
gopayHelperPinPayload: pinTask,
|
||
});
|
||
await addLog('步骤 7:PIN 已提交,继续轮询直到任务完成。', 'ok');
|
||
}
|
||
|
||
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
|
||
}
|
||
throw new Error('步骤 7:GPC 任务轮询超时。');
|
||
} catch (error) {
|
||
if (!terminalReached) {
|
||
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, error?.message || '流程中断');
|
||
}
|
||
if (isGpcTaskEndedError(error)) {
|
||
await clearGpcTaskRuntimeState();
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
function resolveMeiguodizhiCountryCode(value = '') {
|
||
const normalized = normalizeText(value);
|
||
const upper = normalized.toUpperCase();
|
||
if (MEIGUODIZHI_COUNTRY_CONFIG[upper]) {
|
||
return upper;
|
||
}
|
||
const compact = compactCountryText(normalized);
|
||
const match = Object.entries(MEIGUODIZHI_COUNTRY_CONFIG).find(([code, config]) => (
|
||
compact === code.toLowerCase()
|
||
|| (config.aliases || []).some((alias) => {
|
||
const compactAlias = compactCountryText(alias);
|
||
return compact === compactAlias || (compactAlias.length >= 4 && compact.includes(compactAlias));
|
||
})
|
||
));
|
||
return match?.[0] || '';
|
||
}
|
||
|
||
function hasCompleteAddressFallback(seed) {
|
||
const fallback = seed?.fallback || {};
|
||
return Boolean(
|
||
normalizeText(fallback.address1)
|
||
&& normalizeText(fallback.city)
|
||
&& normalizeText(fallback.postalCode)
|
||
);
|
||
}
|
||
|
||
function normalizePostalCodeForCountry(countryCode, rawPostalCode = '', fallbackPostalCode = '') {
|
||
const normalizedCountry = resolveMeiguodizhiCountryCode(countryCode) || normalizeText(countryCode).toUpperCase();
|
||
const postalCode = normalizeText(rawPostalCode);
|
||
const fallback = normalizeText(fallbackPostalCode);
|
||
if (normalizedCountry !== 'KR') {
|
||
return postalCode;
|
||
}
|
||
if (/^\d{5}$/.test(postalCode)) {
|
||
return postalCode;
|
||
}
|
||
if (/^\d{5}$/.test(fallback)) {
|
||
return fallback;
|
||
}
|
||
return '04524';
|
||
}
|
||
|
||
function buildDirectAddressSeed(countryCode, apiAddress, fallbackSeed) {
|
||
const address1 = normalizeText(apiAddress?.Trans_Address || apiAddress?.Address);
|
||
const city = normalizeText(apiAddress?.City);
|
||
const region = normalizeText(apiAddress?.State_Full || apiAddress?.State);
|
||
const postalCode = normalizePostalCodeForCountry(
|
||
countryCode,
|
||
apiAddress?.Zip_Code,
|
||
fallbackSeed?.fallback?.postalCode
|
||
);
|
||
if (!address1 || !city || !postalCode) {
|
||
return null;
|
||
}
|
||
return {
|
||
...(fallbackSeed || {}),
|
||
countryCode,
|
||
query: [address1, city].filter(Boolean).join(', '),
|
||
source: 'meiguodizhi',
|
||
skipAutocomplete: true,
|
||
fallback: {
|
||
...(fallbackSeed?.fallback || {}),
|
||
address1,
|
||
city,
|
||
region,
|
||
postalCode,
|
||
},
|
||
};
|
||
}
|
||
|
||
async function fetchMeiguodizhiAddressSeed(countryCode, fallbackSeed) {
|
||
if (typeof fetchImpl !== 'function') {
|
||
return null;
|
||
}
|
||
const countryConfig = MEIGUODIZHI_COUNTRY_CONFIG[countryCode];
|
||
if (!countryConfig?.path) {
|
||
return null;
|
||
}
|
||
const path = countryConfig.path;
|
||
const city = normalizeText(fallbackSeed?.fallback?.city || fallbackSeed?.query || countryConfig.city);
|
||
const response = await fetchImpl(MEIGUODIZHI_ADDRESS_ENDPOINT, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
city,
|
||
path,
|
||
method: 'refresh',
|
||
}),
|
||
});
|
||
if (!response?.ok) {
|
||
throw new Error(`HTTP ${response?.status || 0}`);
|
||
}
|
||
const data = await response.json();
|
||
if (data?.status !== 'ok') {
|
||
throw new Error(data?.message || data?.status || 'unknown response');
|
||
}
|
||
return buildDirectAddressSeed(countryCode, data.address || {}, fallbackSeed);
|
||
}
|
||
|
||
function getLocalAddressSeed(countryCode) {
|
||
if (typeof getAddressSeedForCountry !== 'function') {
|
||
return null;
|
||
}
|
||
const seed = getAddressSeedForCountry(countryCode, {
|
||
fallbackCountry: 'DE',
|
||
});
|
||
return seed?.countryCode === countryCode ? seed : null;
|
||
}
|
||
|
||
function buildMeiguodizhiLookupSeed(countryCode) {
|
||
const config = MEIGUODIZHI_COUNTRY_CONFIG[countryCode];
|
||
if (!config) {
|
||
return null;
|
||
}
|
||
return {
|
||
countryCode,
|
||
query: config.city,
|
||
fallback: {
|
||
address1: '',
|
||
city: config.city,
|
||
region: '',
|
||
postalCode: '',
|
||
},
|
||
};
|
||
}
|
||
|
||
function resolveBillingAddressCountry(state = {}, countryOverride = '', paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||
const normalizedPaymentMethod = normalizePlusPaymentMethod(paymentMethod || state?.plusPaymentMethod);
|
||
const checkoutCountry = resolveMeiguodizhiCountryCode(countryOverride);
|
||
const savedCheckoutCountry = resolveMeiguodizhiCountryCode(state.plusCheckoutCountry);
|
||
const exitCountry = resolveMeiguodizhiCountryCode(
|
||
state.ipProxyAppliedExitRegion
|
||
|| state.ipProxyExitRegion
|
||
|| ''
|
||
);
|
||
|
||
if (normalizedPaymentMethod === PLUS_PAYMENT_METHOD_GOPAY) {
|
||
const countryCode = exitCountry || checkoutCountry || savedCheckoutCountry || 'ID';
|
||
return {
|
||
countryCode,
|
||
requestedCountry: exitCountry
|
||
|| normalizeText(countryOverride)
|
||
|| normalizeText(state.plusCheckoutCountry)
|
||
|| 'ID',
|
||
source: exitCountry ? 'proxy_exit' : (checkoutCountry ? 'checkout_page' : (savedCheckoutCountry ? 'checkout_state' : 'gopay_fallback')),
|
||
};
|
||
}
|
||
|
||
const countryCode = checkoutCountry || savedCheckoutCountry || exitCountry || 'DE';
|
||
return {
|
||
countryCode,
|
||
requestedCountry: normalizeText(countryOverride)
|
||
|| normalizeText(state.plusCheckoutCountry)
|
||
|| exitCountry
|
||
|| 'DE',
|
||
source: checkoutCountry ? 'checkout_page' : (savedCheckoutCountry ? 'checkout_state' : (exitCountry ? 'proxy_exit' : 'paypal_fallback')),
|
||
};
|
||
}
|
||
|
||
async function resolveBillingAddressSeed(state = {}, countryOverride = '', options = {}) {
|
||
const paymentMethod = normalizePlusPaymentMethod(options.paymentMethod || state?.plusPaymentMethod);
|
||
const countryResolution = resolveBillingAddressCountry(state, countryOverride, paymentMethod);
|
||
const countryCode = countryResolution.countryCode;
|
||
const requestedCountry = countryResolution.requestedCountry;
|
||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY && countryResolution.source === 'proxy_exit') {
|
||
await addLog(`步骤 7:GoPay 账单地址将按当前代理出口地区 ${countryCode} 填写。`, 'info');
|
||
}
|
||
const localSeed = getLocalAddressSeed(countryCode);
|
||
const lookupSeed = localSeed || buildMeiguodizhiLookupSeed(countryCode);
|
||
if (!lookupSeed) {
|
||
throw new Error(`步骤 7:无法识别账单国家或地区:${requestedCountry || '空'}`);
|
||
}
|
||
try {
|
||
const remoteSeed = await fetchMeiguodizhiAddressSeed(countryCode, lookupSeed);
|
||
if (hasCompleteAddressFallback(remoteSeed)) {
|
||
await addLog(
|
||
`步骤 7:已从 meiguodizhi 接口获取账单地址(${remoteSeed.fallback.city} / ${remoteSeed.fallback.postalCode}),将跳过 Google 地址推荐。`,
|
||
'info'
|
||
);
|
||
return remoteSeed;
|
||
}
|
||
await addLog('步骤 7:meiguodizhi 接口返回的地址字段不完整,回退到本地地址种子。', 'warn');
|
||
} catch (error) {
|
||
await addLog(`步骤 7:meiguodizhi 地址接口不可用,回退到本地地址种子:${error?.message || String(error || '')}`, 'warn');
|
||
}
|
||
|
||
if (hasCompleteAddressFallback(localSeed)) {
|
||
return localSeed;
|
||
}
|
||
throw new Error(`步骤 7:${requestedCountry} 的 meiguodizhi 地址不可用,且没有本地兜底地址。`);
|
||
}
|
||
|
||
async function getAlivePlusCheckoutTabId(tabId) {
|
||
if (!Number.isInteger(tabId) || tabId <= 0) {
|
||
return null;
|
||
}
|
||
if (!chrome?.tabs?.get) {
|
||
return tabId;
|
||
}
|
||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||
return tab && isPlusCheckoutUrl(tab.url) ? tabId : null;
|
||
}
|
||
|
||
async function getCurrentPlusCheckoutTabId() {
|
||
if (!chrome?.tabs?.query) {
|
||
return null;
|
||
}
|
||
|
||
const queryTabs = typeof queryTabsInAutomationWindow === 'function'
|
||
? queryTabsInAutomationWindow
|
||
: (queryInfo) => chrome.tabs.query(queryInfo);
|
||
const activeTabs = await queryTabs({ active: true, currentWindow: true }).catch(() => []);
|
||
const activeCheckoutTab = activeTabs.find((tab) => Number.isInteger(tab?.id) && isPlusCheckoutUrl(tab.url));
|
||
if (activeCheckoutTab) {
|
||
return activeCheckoutTab.id;
|
||
}
|
||
|
||
const checkoutTabs = await queryTabs({ url: 'https://chatgpt.com/checkout/*' }).catch(() => []);
|
||
const checkoutTab = checkoutTabs.find((tab) => Number.isInteger(tab?.id) && isPlusCheckoutUrl(tab.url));
|
||
return checkoutTab?.id || null;
|
||
}
|
||
|
||
async function getCheckoutFrames(tabId) {
|
||
if (!chrome?.webNavigation?.getAllFrames) {
|
||
return [{ frameId: 0, url: '' }];
|
||
}
|
||
const frames = await chrome.webNavigation.getAllFrames({ tabId }).catch(() => null);
|
||
if (!Array.isArray(frames) || !frames.length) {
|
||
return [{ frameId: 0, url: '' }];
|
||
}
|
||
return frames
|
||
.filter((frame) => Number.isInteger(frame?.frameId))
|
||
.sort((left, right) => Number(left.frameId) - Number(right.frameId));
|
||
}
|
||
|
||
async function pingCheckoutFrame(tabId, frameId) {
|
||
try {
|
||
const pong = await chrome.tabs.sendMessage(tabId, {
|
||
type: 'PING',
|
||
source: 'background',
|
||
payload: {},
|
||
}, {
|
||
frameId: Number.isInteger(frameId) ? frameId : 0,
|
||
});
|
||
return Boolean(pong?.ok && (!pong.source || pong.source === PLUS_CHECKOUT_SOURCE));
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function ensurePlusCheckoutFrameReady(tabId, frameId) {
|
||
if (await pingCheckoutFrame(tabId, frameId)) {
|
||
return true;
|
||
}
|
||
if (!chrome?.scripting?.executeScript) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
await chrome.scripting.executeScript({
|
||
target: { tabId, frameIds: [frameId] },
|
||
func: (injectedSource) => {
|
||
window.__MULTIPAGE_SOURCE = injectedSource;
|
||
},
|
||
args: [PLUS_CHECKOUT_SOURCE],
|
||
});
|
||
await chrome.scripting.executeScript({
|
||
target: { tabId, frameIds: [frameId] },
|
||
files: PLUS_CHECKOUT_INJECT_FILES,
|
||
});
|
||
} catch {
|
||
// If the frame was already injected or navigated mid-injection, ping once more below.
|
||
}
|
||
|
||
await sleepWithStop(PLUS_CHECKOUT_FRAME_READY_DELAY_MS);
|
||
return await pingCheckoutFrame(tabId, frameId);
|
||
}
|
||
|
||
async function ensurePlusCheckoutFramesReady(tabId, frames) {
|
||
const checkedFrames = [];
|
||
for (const frame of frames) {
|
||
const ready = await ensurePlusCheckoutFrameReady(tabId, frame.frameId);
|
||
checkedFrames.push({ ...frame, ready });
|
||
}
|
||
return checkedFrames;
|
||
}
|
||
|
||
async function sendFrameMessage(tabId, frameId, message) {
|
||
return chrome.tabs.sendMessage(tabId, message, {
|
||
frameId: Number.isInteger(frameId) ? frameId : 0,
|
||
});
|
||
}
|
||
|
||
async function waitForPaymentRedirectAfterSubmit(tabId, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||
const paymentConfig = getPaymentMethodConfig(paymentMethod);
|
||
const startedAt = Date.now();
|
||
while (Date.now() - startedAt < PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS) {
|
||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||
if (!tab) {
|
||
throw new Error(`步骤 7:checkout 标签页已关闭,无法继续等待 ${paymentConfig.label} 跳转。`);
|
||
}
|
||
const url = String(tab.url || '');
|
||
if (paymentConfig.redirectPattern.test(url) && !isPlusCheckoutUrl(url)) {
|
||
await waitForTabCompleteUntilStopped(tabId);
|
||
await sleepWithStop(1000);
|
||
return true;
|
||
}
|
||
if (url && !isPlusCheckoutUrl(url)) {
|
||
await addLog(`步骤 7:点击订阅后页面跳转到非 ${paymentConfig.label} 识别地址:${url}`, 'warn');
|
||
return false;
|
||
}
|
||
await sleepWithStop(500);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function waitForPayPalRedirectAfterSubmit(tabId) {
|
||
return waitForPaymentRedirectAfterSubmit(tabId, PLUS_PAYMENT_METHOD_PAYPAL);
|
||
}
|
||
|
||
async function inspectCheckoutFrame(tabId, frame) {
|
||
try {
|
||
const result = await sendFrameMessage(tabId, frame.frameId, {
|
||
type: 'PLUS_CHECKOUT_GET_STATE',
|
||
source: 'background',
|
||
payload: {},
|
||
});
|
||
if (result?.error) {
|
||
return { frame, error: result.error };
|
||
}
|
||
return { frame: { ...frame, ready: true }, result: result || {} };
|
||
} catch (error) {
|
||
const readyError = frame.ready === false ? 'content-script-not-ready' : '';
|
||
const message = error?.message || String(error || '');
|
||
return { frame, error: readyError ? `${readyError}: ${message}` : message };
|
||
}
|
||
}
|
||
|
||
function isPaymentFrameUrl(url = '') {
|
||
return /elements-inner-payment|componentName=payment/i.test(String(url || ''));
|
||
}
|
||
|
||
function isAddressFrameUrl(url = '') {
|
||
return /elements-inner-address|componentName=address/i.test(String(url || ''));
|
||
}
|
||
|
||
function isAutocompleteFrameUrl(url = '') {
|
||
return /elements-inner-autocompl/i.test(String(url || ''));
|
||
}
|
||
|
||
function buildFrameSummary(inspections) {
|
||
return inspections
|
||
.map((item) => {
|
||
const flags = [];
|
||
if (item.result?.hasPayPal) flags.push('paypal');
|
||
if (item.result?.hasGoPay) flags.push('gopay');
|
||
if (item.result?.billingFieldsVisible) flags.push('billing');
|
||
if (item.result?.hasSubscribeButton) flags.push('subscribe');
|
||
if (!flags.length && item.error) flags.push(item.error);
|
||
if (!flags.length) flags.push('no-match');
|
||
return `${item.frame.frameId}:${item.frame.url || 'about:blank'}:${flags.join(',')}`;
|
||
})
|
||
.slice(0, 8)
|
||
.join(' | ');
|
||
}
|
||
|
||
async function inspectCheckoutFrames(tabId, frames) {
|
||
const inspections = [];
|
||
for (const frame of frames) {
|
||
const inspection = await inspectCheckoutFrame(tabId, frame);
|
||
inspections.push(inspection);
|
||
}
|
||
return inspections;
|
||
}
|
||
|
||
function pickPaymentFrame(inspections, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||
const normalizedPaymentMethod = normalizePlusPaymentMethod(paymentMethod);
|
||
if (normalizedPaymentMethod === PLUS_PAYMENT_METHOD_GOPAY) {
|
||
return inspections.find((item) => item.result?.hasGoPay || item.result?.gopayCandidates?.length)
|
||
|| inspections.find((item) => isPaymentFrameUrl(item.frame.url))
|
||
|| null;
|
||
}
|
||
return inspections.find((item) => item.result?.hasPayPal || item.result?.paypalCandidates?.length)
|
||
|| inspections.find((item) => isPaymentFrameUrl(item.frame.url))
|
||
|| null;
|
||
}
|
||
|
||
function pickBillingFrame(inspections) {
|
||
return inspections.find((item) => item.result?.billingFieldsVisible)
|
||
|| inspections.find((item) => isAddressFrameUrl(item.frame.url))
|
||
|| null;
|
||
}
|
||
|
||
function pickSubscribeFrame(inspections) {
|
||
return inspections.find((item) => item.result?.hasSubscribeButton)
|
||
|| inspections.find((item) => item.frame.frameId === 0)
|
||
|| null;
|
||
}
|
||
|
||
function findCheckoutAmountInspection(inspections = []) {
|
||
return inspections.find((item) => item.result?.checkoutAmountSummary?.hasTodayDue)
|
||
|| null;
|
||
}
|
||
|
||
async function inspectCheckoutAmountSummary(tabId) {
|
||
const frames = await getReadyCheckoutFrames(tabId);
|
||
const inspections = await inspectCheckoutFrames(tabId, frames);
|
||
const amountInspection = findCheckoutAmountInspection(inspections);
|
||
return amountInspection?.result?.checkoutAmountSummary || null;
|
||
}
|
||
|
||
async function ensureFreeTrialAmount(tabId, state = {}, options = {}) {
|
||
const phaseLabel = String(options.phaseLabel || '').trim() || '提交前';
|
||
const amountSummary = await inspectCheckoutAmountSummary(tabId);
|
||
if (!amountSummary?.hasTodayDue) {
|
||
await addLog(`步骤 7:${phaseLabel}未能识别 checkout 的“今日应付金额”,为避免误判将继续执行。`, 'warn');
|
||
return;
|
||
}
|
||
|
||
if (amountSummary.isZero) {
|
||
await addLog(`步骤 7:${phaseLabel}已确认今日应付金额为 ${amountSummary.rawAmount || '0'},继续执行。`, 'ok');
|
||
return;
|
||
}
|
||
|
||
const amountLabel = amountSummary.rawAmount || (
|
||
Number.isFinite(Number(amountSummary.amount)) ? String(amountSummary.amount) : '未知金额'
|
||
);
|
||
await addLog(`步骤 7:${phaseLabel}检测到今日应付金额不是 0(${amountLabel}),说明当前账号没有免费试用资格,将跳过支付提交。`, 'warn');
|
||
if (typeof markCurrentRegistrationAccountUsed === 'function') {
|
||
await markCurrentRegistrationAccountUsed(state, {
|
||
reason: 'plus-checkout-non-free-trial',
|
||
logPrefix: 'Plus Checkout:当前账号没有免费试用资格',
|
||
});
|
||
}
|
||
throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(${amountLabel}),当前账号没有免费试用资格,已跳过支付提交。`);
|
||
}
|
||
|
||
async function getReadyCheckoutFrames(tabId) {
|
||
return ensurePlusCheckoutFramesReady(tabId, await getCheckoutFrames(tabId));
|
||
}
|
||
|
||
async function resolveOptionalFrameByUrl(tabId, predicate) {
|
||
const frames = await getCheckoutFrames(tabId);
|
||
const frame = frames.find((item) => predicate(item.url));
|
||
if (!frame) {
|
||
return null;
|
||
}
|
||
const ready = await ensurePlusCheckoutFrameReady(tabId, frame.frameId);
|
||
return {
|
||
frame,
|
||
ready,
|
||
};
|
||
}
|
||
|
||
async function resolvePaymentFrame(tabId, frames, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||
const inspections = await inspectCheckoutFrames(tabId, frames);
|
||
const picked = pickPaymentFrame(inspections, paymentMethod);
|
||
if (picked) {
|
||
return {
|
||
frameId: picked.frame.frameId,
|
||
frameUrl: picked.frame.url || '',
|
||
ready: picked.frame.ready !== false,
|
||
inspections,
|
||
};
|
||
}
|
||
|
||
return {
|
||
frameId: null,
|
||
frameUrl: '',
|
||
inspections,
|
||
};
|
||
}
|
||
|
||
async function waitForBillingFrame(tabId) {
|
||
while (true) {
|
||
const frames = await getReadyCheckoutFrames(tabId);
|
||
const inspections = await inspectCheckoutFrames(tabId, frames);
|
||
const picked = pickBillingFrame(inspections);
|
||
if (picked) {
|
||
return {
|
||
frameId: picked.frame.frameId,
|
||
frameUrl: picked.frame.url || '',
|
||
countryText: picked.result?.countryText || '',
|
||
ready: picked.frame.ready !== false,
|
||
inspections,
|
||
};
|
||
}
|
||
await sleepWithStop(250);
|
||
}
|
||
}
|
||
|
||
async function waitForSubscribeFrame(tabId, candidateFrames) {
|
||
const frames = candidateFrames.length ? candidateFrames : [{ frameId: 0, url: '' }];
|
||
while (true) {
|
||
const readyFrames = await ensurePlusCheckoutFramesReady(tabId, frames);
|
||
const inspections = await inspectCheckoutFrames(tabId, readyFrames);
|
||
const picked = pickSubscribeFrame(inspections);
|
||
if (picked) {
|
||
return picked.frame;
|
||
}
|
||
await sleepWithStop(250);
|
||
}
|
||
}
|
||
|
||
async function getCheckoutTabId(state = {}) {
|
||
const registeredTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||
if (registeredTabId && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
|
||
const aliveRegisteredTabId = await getAlivePlusCheckoutTabId(registeredTabId);
|
||
if (aliveRegisteredTabId) {
|
||
return aliveRegisteredTabId;
|
||
}
|
||
}
|
||
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||
if (storedTabId) {
|
||
const aliveStoredTabId = await getAlivePlusCheckoutTabId(storedTabId);
|
||
if (aliveStoredTabId) {
|
||
return aliveStoredTabId;
|
||
}
|
||
}
|
||
const currentCheckoutTabId = await getCurrentPlusCheckoutTabId();
|
||
if (currentCheckoutTabId) {
|
||
await addLog('步骤 7:检测到当前已在 Plus Checkout 页面,直接接管当前标签页。', 'info');
|
||
return currentCheckoutTabId;
|
||
}
|
||
throw new Error('步骤 7:未找到 Plus Checkout 标签页。请先打开 Plus Checkout 页面,或完成步骤 6。');
|
||
}
|
||
|
||
async function executePlusCheckoutBilling(state = {}) {
|
||
if (isGpcHelperCheckout(state)) {
|
||
await executeGpcHelperBilling(state);
|
||
return;
|
||
}
|
||
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
|
||
const paymentConfig = getPaymentMethodConfig(paymentMethod);
|
||
const tabId = await getCheckoutTabId(state);
|
||
await addLog('步骤 7:正在等待 Plus Checkout 页面加载完成...', 'info');
|
||
await waitForTabCompleteUntilStopped(tabId);
|
||
await sleepWithStop(1000);
|
||
|
||
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||
injectSource: PLUS_CHECKOUT_SOURCE,
|
||
logMessage: '步骤 7:Checkout 页面仍在加载,等待账单填写脚本就绪...',
|
||
});
|
||
const readyFrames = await getReadyCheckoutFrames(tabId);
|
||
await ensureFreeTrialAmount(tabId, state, {
|
||
phaseLabel: 'Checkout 页面加载后',
|
||
});
|
||
const paymentFrame = await resolvePaymentFrame(tabId, readyFrames, paymentMethod);
|
||
if (paymentFrame.frameId === null) {
|
||
const frameSummary = buildFrameSummary(paymentFrame.inspections);
|
||
throw new Error(`步骤 7:未在主页面或 iframe 中发现 ${paymentConfig.label} DOM,无法自动切换付款方式。frame 摘要:${frameSummary}`);
|
||
}
|
||
if (!paymentFrame.ready) {
|
||
throw new Error(`步骤 7:已定位到 ${paymentConfig.label} 所在 iframe(frameId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
|
||
}
|
||
|
||
if (paymentFrame.frameId !== 0) {
|
||
await addLog(`步骤 7:${paymentConfig.label} 位于 checkout iframe(frameId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
|
||
}
|
||
|
||
const randomName = generateRandomName();
|
||
const fullName = [randomName.firstName, randomName.lastName].filter(Boolean).join(' ');
|
||
|
||
await addLog(`步骤 7:正在切换 ${paymentConfig.label} 付款方式...`, 'info');
|
||
const paymentResult = await sendFrameMessage(tabId, paymentFrame.frameId, {
|
||
type: paymentConfig.selectMessageType,
|
||
source: 'background',
|
||
payload: { paymentMethod },
|
||
});
|
||
if (paymentResult?.error) {
|
||
throw new Error(paymentResult.error);
|
||
}
|
||
|
||
const billingFrame = await waitForBillingFrame(tabId);
|
||
if (!billingFrame.ready) {
|
||
throw new Error(`步骤 7:已定位到账单地址 iframe(frameId=${billingFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
|
||
}
|
||
if (billingFrame.frameId !== paymentFrame.frameId) {
|
||
await addLog(`步骤 7:账单地址位于 checkout iframe(frameId=${billingFrame.frameId}),将改为在该 frame 内填写。`, 'info');
|
||
}
|
||
|
||
let billingState = state;
|
||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY && typeof probeIpProxyExit === 'function') {
|
||
const staleExitRegion = normalizeText(
|
||
state?.ipProxyAppliedExitRegion
|
||
|| state?.ipProxyExitRegion
|
||
|| ''
|
||
);
|
||
try {
|
||
await addLog('步骤 7:GoPay 账单地址准备按代理出口填写,正在重新检测当前出口地区...', 'info');
|
||
const probeResult = await probeIpProxyExit({
|
||
state,
|
||
timeoutMs: 12000,
|
||
authRebindRetry: true,
|
||
detectWhenDisabled: true,
|
||
});
|
||
const routing = probeResult?.proxyRouting || {};
|
||
const probedExitRegion = normalizeText(routing.exitRegion || '');
|
||
const probedExitIp = normalizeText(routing.exitIp || '');
|
||
const probedExitSource = normalizeText(routing.exitSource || '');
|
||
const probeEndpoint = normalizeText(routing.endpoint || routing.exitEndpoint || '');
|
||
const probeReason = normalizeText(routing.reason || '');
|
||
const probeError = normalizeText(routing.exitError || routing.error || '');
|
||
if (probedExitRegion) {
|
||
billingState = {
|
||
...(state || {}),
|
||
ipProxyAppliedExitRegion: probedExitRegion,
|
||
ipProxyExitRegion: probedExitRegion,
|
||
ipProxyAppliedExitIp: probedExitIp,
|
||
ipProxyAppliedExitSource: probedExitSource,
|
||
};
|
||
const sourceSuffix = probedExitSource ? `,来源 ${probedExitSource}` : '';
|
||
const endpointSuffix = probeEndpoint ? `,检测地址 ${probeEndpoint}` : '';
|
||
await addLog(`步骤 7:当前代理出口复测结果:${probedExitRegion}${probedExitIp ? ` / ${probedExitIp}` : ''}${sourceSuffix}${endpointSuffix}。`, 'info');
|
||
} else {
|
||
billingState = {
|
||
...(state || {}),
|
||
ipProxyAppliedExitRegion: '',
|
||
ipProxyExitRegion: '',
|
||
ipProxyAppliedExitIp: probedExitIp,
|
||
ipProxyAppliedExitSource: probedExitSource,
|
||
};
|
||
await addLog(
|
||
`步骤 7:代理出口复测没有返回国家/地区代码,已清空旧出口地区${staleExitRegion ? ` ${staleExitRegion}` : ''},不会继续沿用旧地区。${probeReason ? `状态:${probeReason}。` : ''}${probeError ? `诊断:${probeError}` : ''}`,
|
||
'warn'
|
||
);
|
||
}
|
||
} catch (error) {
|
||
billingState = {
|
||
...(state || {}),
|
||
ipProxyAppliedExitRegion: '',
|
||
ipProxyExitRegion: '',
|
||
};
|
||
await addLog(`步骤 7:代理出口复测失败,已清空旧出口地区${staleExitRegion ? ` ${staleExitRegion}` : ''},不会继续沿用旧地区:${error?.message || String(error || '未知错误')}`, 'warn');
|
||
}
|
||
}
|
||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY
|
||
&& typeof probeIpProxyExit === 'function'
|
||
&& !resolveMeiguodizhiCountryCode(billingState?.ipProxyAppliedExitRegion || billingState?.ipProxyExitRegion || '')) {
|
||
throw new Error('步骤 7:GoPay 账单地址需要当前代理出口国家/地区,但本次复测没有拿到国家码;已停止填写,避免误用旧的 KR/ID 地区。请先点 IP 代理“检测出口”,确认显示 JP 后再继续。');
|
||
}
|
||
const addressSeed = await resolveBillingAddressSeed(billingState, billingFrame.countryText, { paymentMethod });
|
||
if (!addressSeed) {
|
||
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
|
||
}
|
||
|
||
await addLog(`步骤 7:正在填写账单地址(${addressSeed.countryCode} / ${addressSeed.query})...`, 'info');
|
||
const autocompleteFrame = await resolveOptionalFrameByUrl(tabId, isAutocompleteFrameUrl);
|
||
let result = null;
|
||
if (!addressSeed.skipAutocomplete && autocompleteFrame?.frame && autocompleteFrame.frame.frameId !== billingFrame.frameId) {
|
||
if (!autocompleteFrame.ready) {
|
||
throw new Error('步骤 7:发现 Google 地址推荐 iframe,但无法注入账单脚本。请提供该 iframe 的控制台结构。');
|
||
}
|
||
await addLog(`步骤 7:Google 地址推荐位于独立 iframe(frameId=${autocompleteFrame.frame.frameId}),将拆分输入与选择动作。`, 'info');
|
||
|
||
const queryResult = await sendFrameMessage(tabId, billingFrame.frameId, {
|
||
type: 'PLUS_CHECKOUT_FILL_ADDRESS_QUERY',
|
||
source: 'background',
|
||
payload: {
|
||
fullName,
|
||
addressSeed,
|
||
},
|
||
});
|
||
if (queryResult?.error) {
|
||
throw new Error(queryResult.error);
|
||
}
|
||
|
||
const suggestionResult = await sendFrameMessage(tabId, autocompleteFrame.frame.frameId, {
|
||
type: 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION',
|
||
source: 'background',
|
||
payload: {
|
||
addressSeed,
|
||
},
|
||
});
|
||
const suggestionError = suggestionResult?.error || '';
|
||
if (suggestionError) {
|
||
await addLog(`步骤 7:Google 地址推荐不可用,将改用本地地址字段兜底:${suggestionError}`, 'warn');
|
||
}
|
||
|
||
const structuredResult = await sendFrameMessage(tabId, billingFrame.frameId, {
|
||
type: 'PLUS_CHECKOUT_ENSURE_BILLING_ADDRESS',
|
||
source: 'background',
|
||
payload: {
|
||
addressSeed,
|
||
overwriteStructuredAddress: Boolean(suggestionError),
|
||
},
|
||
});
|
||
if (structuredResult?.error) {
|
||
throw new Error(structuredResult.error);
|
||
}
|
||
|
||
result = {
|
||
...structuredResult,
|
||
selectedAddressText: suggestionError ? '' : (suggestionResult?.selectedAddressText || ''),
|
||
};
|
||
} else {
|
||
result = await sendFrameMessage(tabId, billingFrame.frameId, {
|
||
type: 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS',
|
||
source: 'background',
|
||
payload: {
|
||
fullName,
|
||
addressSeed,
|
||
},
|
||
});
|
||
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
}
|
||
|
||
await setState({
|
||
plusCheckoutTabId: tabId,
|
||
plusBillingCountryText: result?.countryText || '',
|
||
plusBillingAddress: result?.structuredAddress || null,
|
||
});
|
||
await ensureFreeTrialAmount(tabId, state, {
|
||
phaseLabel: '提交订阅前',
|
||
});
|
||
|
||
let redirectedToPayment = false;
|
||
let lastSubmitError = '';
|
||
for (let attempt = 1; attempt <= PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS; attempt += 1) {
|
||
await addLog(
|
||
attempt === 1
|
||
? '步骤 7:账单地址已填写完成,等待 3 秒让 checkout 完成校验...'
|
||
: `步骤 7:准备第 ${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS} 次重新提交账单地址...`,
|
||
attempt === 1 ? 'info' : 'warn'
|
||
);
|
||
await sleepWithStop(3000);
|
||
await addLog('步骤 7:正在定位订阅按钮...', 'info');
|
||
const subscribeFrame = await waitForSubscribeFrame(tabId, [
|
||
{ frameId: 0, url: '' },
|
||
{ frameId: paymentFrame.frameId, url: paymentFrame.frameUrl || '' },
|
||
{ frameId: billingFrame.frameId, url: billingFrame.frameUrl || '' },
|
||
]);
|
||
const subscribeResult = await sendFrameMessage(tabId, subscribeFrame.frameId, {
|
||
type: 'PLUS_CHECKOUT_CLICK_SUBSCRIBE',
|
||
source: 'background',
|
||
payload: {
|
||
beforeClickDelayMs: attempt === 1 ? 700 : 1200,
|
||
paymentMethod,
|
||
},
|
||
});
|
||
if (subscribeResult?.error) {
|
||
lastSubmitError = subscribeResult.error;
|
||
await addLog(`步骤 7:点击订阅失败(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS}):${lastSubmitError}`, 'warn');
|
||
continue;
|
||
}
|
||
|
||
await addLog(`步骤 7:账单地址已提交,正在等待跳转到 ${paymentConfig.label}(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS})...`, 'info');
|
||
redirectedToPayment = await waitForPaymentRedirectAfterSubmit(tabId, paymentMethod);
|
||
if (redirectedToPayment) {
|
||
break;
|
||
}
|
||
lastSubmitError = `提交后 ${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 ${paymentConfig.label}`;
|
||
await addLog(`步骤 7:${lastSubmitError},将重试提交。`, 'warn');
|
||
}
|
||
|
||
if (!redirectedToPayment) {
|
||
throw new Error(`步骤 7:多次提交账单地址后仍未跳转到 ${paymentConfig.label}。${lastSubmitError}`);
|
||
}
|
||
|
||
await completeNodeFromBackground('plus-checkout-billing', {
|
||
plusBillingCountryText: result?.countryText || '',
|
||
});
|
||
}
|
||
|
||
return {
|
||
executePlusCheckoutBilling,
|
||
};
|
||
}
|
||
|
||
return {
|
||
createPlusCheckoutBillingExecutor,
|
||
};
|
||
});
|