Files
FlowPilot/background/steps/fill-plus-checkout.js
T
2026-05-08 20:06:14 -04:00

1758 lines
74 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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_TASK_POLL_INTERVAL_MS = 3000;
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,
completeStepFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
fetch: fetchImpl = null,
generateRandomName,
getAddressSeedForCountry,
getState,
getTabId,
isTabAlive,
markCurrentRegistrationAccountUsed,
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 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 timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null;
try {
const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
const data = await response.json().catch(() => ({}));
return { response, data };
} 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 = String(task.phone_mode || task.phoneMode || '').trim().toLowerCase();
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(`步骤 7GPC 接口返回 ${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 isGpcTaskOtpWait(task = {}) {
return task?.phone_mode === 'manual' && task?.api_waiting_for === 'otp';
}
function isGpcTaskPinWait(task = {}) {
return task?.phone_mode === 'manual'
&& (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready');
}
function isGpcTaskTerminal(status = '') {
return ['completed', 'failed', 'expired', 'discarded'].includes(String(status || '').trim().toLowerCase());
}
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,
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;
if (!taskId) {
throw new Error('步骤 7GPC 模式缺少 task_id,请先执行步骤 6。');
}
if (!apiUrl) {
throw new Error('步骤 7GPC 模式缺少 API 地址。');
}
if (!apiKey) {
throw new Error('步骤 7GPC 模式缺少 API Key。');
}
const rawPin = String(state?.gopayHelperPin || '').trim();
const pinDigits = rawPin.replace(/[^\d]/g, '');
const pin = normalizeSixDigitPin(rawPin);
if (!pin) {
if (taskId && apiUrl && apiKey) {
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
}
throw new Error(pinDigits
? '步骤 7GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
: '步骤 7GPC 模式缺少 PIN 配置。');
}
await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId}...`, 'info');
try {
while (Date.now() <= deadline) {
throwIfStopped();
const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey);
const statusText = task?.status_text || task?.status || '处理中';
const remoteStage = task?.remote_stage || '';
const waitingFor = task?.api_waiting_for || '';
await addLog(`步骤 7GPC 任务状态:${statusText}${remoteStage ? `${remoteStage}` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info');
if (task.status === 'completed') {
terminalReached = true;
await setState({
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
});
await addLog('步骤 7:GPC 任务已完成,准备继续下一步。', 'ok');
await completeStepFromBackground(7, {
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
});
return;
}
if (['failed', 'expired', 'discarded'].includes(task.status)) {
terminalReached = true;
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
}
if (isGpcTaskOtpWait(task)) {
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(`步骤 7OTP 提交被拒绝:${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) && !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('步骤 7GPC 任务轮询超时。');
} 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('步骤 7meiguodizhi 接口返回的地址字段不完整,回退到本地地址种子。', 'warn');
} catch (error) {
await addLog(`步骤 7meiguodizhi 地址接口不可用,回退到本地地址种子:${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 activeTabs = await chrome.tabs.query({ 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 chrome.tabs.query({ 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: '步骤 7Checkout 页面仍在加载,等待账单填写脚本就绪...',
});
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} 所在 iframeframeId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
}
if (paymentFrame.frameId !== 0) {
await addLog(`步骤 7${paymentConfig.label} 位于 checkout iframeframeId=${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:已定位到账单地址 iframeframeId=${billingFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
}
if (billingFrame.frameId !== paymentFrame.frameId) {
await addLog(`步骤 7:账单地址位于 checkout iframeframeId=${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 地址推荐位于独立 iframeframeId=${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 completeStepFromBackground(7, {
plusBillingCountryText: result?.countryText || '',
});
}
return {
executePlusCheckoutBilling,
};
}
return {
createPlusCheckoutBillingExecutor,
};
});