feat(gpc): align helper flow with api key queue tasks
This commit is contained in:
+117
-19
@@ -239,7 +239,7 @@ const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
|
|||||||
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
|
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
|
||||||
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
|
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
|
||||||
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
||||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
|
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz';
|
||||||
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
|
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
|
||||||
const DEFAULT_SUB2API_PROXY_NAME = '';
|
const DEFAULT_SUB2API_PROXY_NAME = '';
|
||||||
const DEFAULT_SUB2API_ACCOUNT_PRIORITY = 1;
|
const DEFAULT_SUB2API_ACCOUNT_PRIORITY = 1;
|
||||||
@@ -607,6 +607,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
gopayOtp: '',
|
gopayOtp: '',
|
||||||
gopayPin: '',
|
gopayPin: '',
|
||||||
gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL,
|
gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL,
|
||||||
|
gopayHelperApiKey: '',
|
||||||
gopayHelperCardKey: '',
|
gopayHelperCardKey: '',
|
||||||
gopayHelperPhoneNumber: '',
|
gopayHelperPhoneNumber: '',
|
||||||
gopayHelperCountryCode: '+86',
|
gopayHelperCountryCode: '+86',
|
||||||
@@ -623,6 +624,18 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
gopayHelperFlowId: '',
|
gopayHelperFlowId: '',
|
||||||
gopayHelperChallengeId: '',
|
gopayHelperChallengeId: '',
|
||||||
gopayHelperStartPayload: null,
|
gopayHelperStartPayload: null,
|
||||||
|
gopayHelperTaskId: '',
|
||||||
|
gopayHelperTaskStatus: '',
|
||||||
|
gopayHelperStatusText: '',
|
||||||
|
gopayHelperRemoteStage: '',
|
||||||
|
gopayHelperApiWaitingFor: '',
|
||||||
|
gopayHelperApiInputDeadlineAt: '',
|
||||||
|
gopayHelperApiInputWaitSeconds: 0,
|
||||||
|
gopayHelperLastInputError: '',
|
||||||
|
gopayHelperOtpInvalidCount: 0,
|
||||||
|
gopayHelperFailureStage: '',
|
||||||
|
gopayHelperFailureDetail: '',
|
||||||
|
gopayHelperTaskPayload: null,
|
||||||
gopayHelperBalance: '',
|
gopayHelperBalance: '',
|
||||||
gopayHelperBalancePayload: null,
|
gopayHelperBalancePayload: null,
|
||||||
gopayHelperBalanceUpdatedAt: 0,
|
gopayHelperBalanceUpdatedAt: 0,
|
||||||
@@ -767,6 +780,18 @@ const DEFAULT_STATE = {
|
|||||||
gopayHelperFlowId: '',
|
gopayHelperFlowId: '',
|
||||||
gopayHelperChallengeId: '',
|
gopayHelperChallengeId: '',
|
||||||
gopayHelperStartPayload: null,
|
gopayHelperStartPayload: null,
|
||||||
|
gopayHelperTaskId: '',
|
||||||
|
gopayHelperTaskStatus: '',
|
||||||
|
gopayHelperStatusText: '',
|
||||||
|
gopayHelperRemoteStage: '',
|
||||||
|
gopayHelperApiWaitingFor: '',
|
||||||
|
gopayHelperApiInputDeadlineAt: '',
|
||||||
|
gopayHelperApiInputWaitSeconds: 0,
|
||||||
|
gopayHelperLastInputError: '',
|
||||||
|
gopayHelperOtpInvalidCount: 0,
|
||||||
|
gopayHelperFailureStage: '',
|
||||||
|
gopayHelperFailureDetail: '',
|
||||||
|
gopayHelperTaskPayload: null,
|
||||||
gopayHelperOrderCreatedAt: 0,
|
gopayHelperOrderCreatedAt: 0,
|
||||||
gopayHelperPinPayload: null,
|
gopayHelperPinPayload: null,
|
||||||
gopayHelperResolvedOtp: '',
|
gopayHelperResolvedOtp: '',
|
||||||
@@ -2280,11 +2305,12 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
case 'gopayHelperApiUrl':
|
case 'gopayHelperApiUrl':
|
||||||
{
|
{
|
||||||
const defaultGpcHelperApiUrl = PERSISTED_SETTING_DEFAULTS.gopayHelperApiUrl
|
const defaultGpcHelperApiUrl = PERSISTED_SETTING_DEFAULTS.gopayHelperApiUrl
|
||||||
|| (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gopay.hwork.pro');
|
|| (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gpc.leftcode.xyz');
|
||||||
return self.GoPayUtils?.normalizeGpcHelperBaseUrl
|
return self.GoPayUtils?.normalizeGpcHelperBaseUrl
|
||||||
? self.GoPayUtils.normalizeGpcHelperBaseUrl(value || defaultGpcHelperApiUrl)
|
? self.GoPayUtils.normalizeGpcHelperBaseUrl(value || defaultGpcHelperApiUrl)
|
||||||
: String(value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, '');
|
: String(value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, '');
|
||||||
}
|
}
|
||||||
|
case 'gopayHelperApiKey':
|
||||||
case 'gopayHelperCardKey':
|
case 'gopayHelperCardKey':
|
||||||
case 'gopayHelperReferenceId':
|
case 'gopayHelperReferenceId':
|
||||||
case 'gopayHelperGoPayGuid':
|
case 'gopayHelperGoPayGuid':
|
||||||
@@ -2292,13 +2318,25 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
case 'gopayHelperNextAction':
|
case 'gopayHelperNextAction':
|
||||||
case 'gopayHelperFlowId':
|
case 'gopayHelperFlowId':
|
||||||
case 'gopayHelperChallengeId':
|
case 'gopayHelperChallengeId':
|
||||||
|
case 'gopayHelperTaskId':
|
||||||
|
case 'gopayHelperTaskStatus':
|
||||||
|
case 'gopayHelperStatusText':
|
||||||
|
case 'gopayHelperRemoteStage':
|
||||||
|
case 'gopayHelperApiWaitingFor':
|
||||||
|
case 'gopayHelperApiInputDeadlineAt':
|
||||||
|
case 'gopayHelperLastInputError':
|
||||||
|
case 'gopayHelperFailureStage':
|
||||||
|
case 'gopayHelperFailureDetail':
|
||||||
case 'gopayHelperBalance':
|
case 'gopayHelperBalance':
|
||||||
case 'gopayHelperBalanceError':
|
case 'gopayHelperBalanceError':
|
||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
case 'gopayHelperBalancePayload':
|
case 'gopayHelperBalancePayload':
|
||||||
case 'gopayHelperStartPayload':
|
case 'gopayHelperStartPayload':
|
||||||
|
case 'gopayHelperTaskPayload':
|
||||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
||||||
case 'gopayHelperBalanceUpdatedAt':
|
case 'gopayHelperBalanceUpdatedAt':
|
||||||
|
case 'gopayHelperApiInputWaitSeconds':
|
||||||
|
case 'gopayHelperOtpInvalidCount':
|
||||||
return Math.max(0, Number(value) || 0);
|
return Math.max(0, Number(value) || 0);
|
||||||
case 'autoRunSkipFailures':
|
case 'autoRunSkipFailures':
|
||||||
case 'oauthFlowTimeoutEnabled':
|
case 'oauthFlowTimeoutEnabled':
|
||||||
@@ -7268,7 +7306,8 @@ function getErrorMessage(error) {
|
|||||||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getErrorMessage) {
|
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getErrorMessage) {
|
||||||
return loggingStatus.getErrorMessage(error);
|
return loggingStatus.getErrorMessage(error);
|
||||||
}
|
}
|
||||||
return String(typeof error === 'string' ? error : error?.message || '');
|
return String(typeof error === 'string' ? error : error?.message || '')
|
||||||
|
.replace(/^GPC_TASK_ENDED::/i, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCloudflareSecurityBlockedError(error) {
|
function isCloudflareSecurityBlockedError(error) {
|
||||||
@@ -7479,6 +7518,11 @@ function isPlusCheckoutNonFreeTrialFailure(error) {
|
|||||||
return /PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(message);
|
return /PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGpcTaskEndedFailure(error) {
|
||||||
|
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||||
|
return /GPC_TASK_ENDED::/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
function isGoPayCheckoutRestartRequiredFailure(error) {
|
function isGoPayCheckoutRestartRequiredFailure(error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
return /GOPAY_RESTART_FROM_STEP6::|GOPAY_RETRY_REQUIRED::/i.test(message);
|
return /GOPAY_RESTART_FROM_STEP6::|GOPAY_RETRY_REQUIRED::/i.test(message);
|
||||||
@@ -7549,6 +7593,18 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
gopayHelperFlowId: '',
|
gopayHelperFlowId: '',
|
||||||
gopayHelperChallengeId: '',
|
gopayHelperChallengeId: '',
|
||||||
gopayHelperStartPayload: null,
|
gopayHelperStartPayload: null,
|
||||||
|
gopayHelperTaskId: '',
|
||||||
|
gopayHelperTaskStatus: '',
|
||||||
|
gopayHelperStatusText: '',
|
||||||
|
gopayHelperRemoteStage: '',
|
||||||
|
gopayHelperApiWaitingFor: '',
|
||||||
|
gopayHelperApiInputDeadlineAt: '',
|
||||||
|
gopayHelperApiInputWaitSeconds: 0,
|
||||||
|
gopayHelperLastInputError: '',
|
||||||
|
gopayHelperOtpInvalidCount: 0,
|
||||||
|
gopayHelperFailureStage: '',
|
||||||
|
gopayHelperFailureDetail: '',
|
||||||
|
gopayHelperTaskPayload: null,
|
||||||
gopayHelperOrderCreatedAt: 0,
|
gopayHelperOrderCreatedAt: 0,
|
||||||
gopayHelperPinPayload: null,
|
gopayHelperPinPayload: null,
|
||||||
gopayHelperResolvedOtp: '',
|
gopayHelperResolvedOtp: '',
|
||||||
@@ -7638,6 +7694,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
plusManualConfirmationTitle: '',
|
plusManualConfirmationTitle: '',
|
||||||
plusManualConfirmationMessage: '',
|
plusManualConfirmationMessage: '',
|
||||||
gopayHelperResolvedOtp: '',
|
gopayHelperResolvedOtp: '',
|
||||||
|
gopayHelperLastInputError: '',
|
||||||
gopayHelperOtpRequestId: '',
|
gopayHelperOtpRequestId: '',
|
||||||
gopayHelperOtpReferenceId: '',
|
gopayHelperOtpReferenceId: '',
|
||||||
} : {}),
|
} : {}),
|
||||||
@@ -9598,22 +9655,42 @@ function resolveGpcHelperBaseUrl(apiUrl = '') {
|
|||||||
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
||||||
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
||||||
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/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\/balance(?:\?.*)?$/i, '');
|
||||||
|
normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, '');
|
||||||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGpcCardBalanceRequestUrl(apiUrl = '', cardKey = '') {
|
function buildGpcApiKeyBalanceRequestUrl(apiUrl = '') {
|
||||||
|
if (self.GoPayUtils?.buildGpcApiKeyBalanceUrl) {
|
||||||
|
return self.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl);
|
||||||
|
}
|
||||||
if (self.GoPayUtils?.buildGpcCardBalanceUrl) {
|
if (self.GoPayUtils?.buildGpcCardBalanceUrl) {
|
||||||
return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl, cardKey);
|
return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl);
|
||||||
}
|
}
|
||||||
const baseUrl = resolveGpcHelperBaseUrl(apiUrl);
|
const baseUrl = resolveGpcHelperBaseUrl(apiUrl);
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return `${baseUrl}/api/card/balance?card_key=${encodeURIComponent(String(cardKey || '').trim())}`;
|
return `${baseUrl}/api/gp/balance`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatGpcCardBalancePayload(payload = {}) {
|
function buildGpcApiKeyHeaders(apiKey = '', extraHeaders = {}) {
|
||||||
|
if (self.GoPayUtils?.buildGpcApiKeyHeaders) {
|
||||||
|
return self.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 formatGpcApiKeyBalancePayload(payload = {}) {
|
||||||
if (self.GoPayUtils?.formatGpcBalancePayload) {
|
if (self.GoPayUtils?.formatGpcBalancePayload) {
|
||||||
return self.GoPayUtils.formatGpcBalancePayload(payload);
|
return self.GoPayUtils.formatGpcBalancePayload(payload);
|
||||||
}
|
}
|
||||||
@@ -9621,30 +9698,40 @@ function formatGpcCardBalancePayload(payload = {}) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const remaining = payload.remaining_uses ?? payload.remainingUses ?? payload.balance ?? payload.remaining;
|
const remaining = payload.remaining_uses ?? payload.remainingUses ?? payload.balance ?? payload.remaining;
|
||||||
|
const total = payload.total_uses ?? payload.totalUses;
|
||||||
|
const used = payload.used_uses ?? payload.usedUses;
|
||||||
const status = String(payload.card_status || payload.cardStatus || payload.status || '').trim();
|
const status = String(payload.card_status || payload.cardStatus || payload.status || '').trim();
|
||||||
return [
|
return [
|
||||||
remaining !== undefined && remaining !== null && String(remaining).trim() !== '' ? `余额 ${remaining}` : '',
|
remaining !== undefined && remaining !== null && String(remaining).trim() !== ''
|
||||||
|
? (total !== undefined && total !== null && String(total).trim() !== '' ? `余额 ${remaining}/${total}` : `余额 ${remaining}`)
|
||||||
|
: '',
|
||||||
|
used !== undefined && used !== null && String(used).trim() !== '' ? `已用 ${used}` : '',
|
||||||
status ? `状态 ${status}` : '',
|
status ? `状态 ${status}` : '',
|
||||||
].filter(Boolean).join(',');
|
].filter(Boolean).join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshGpcCardBalance(state = {}, options = {}) {
|
async function refreshGpcApiKeyBalance(state = {}, options = {}) {
|
||||||
const apiUrl = resolveGpcHelperBaseUrl(state?.gopayHelperApiUrl || DEFAULT_GPC_HELPER_API_URL);
|
const apiUrl = resolveGpcHelperBaseUrl(state?.gopayHelperApiUrl || DEFAULT_GPC_HELPER_API_URL);
|
||||||
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
|
const apiKey = String(
|
||||||
|
state?.gopayHelperApiKey
|
||||||
|
|| state?.gpcApiKey
|
||||||
|
|| state?.apiKey
|
||||||
|
|| ''
|
||||||
|
).trim();
|
||||||
if (!apiUrl) {
|
if (!apiUrl) {
|
||||||
throw new Error('缺少 GPC API 地址。');
|
throw new Error('缺少 GPC API 地址。');
|
||||||
}
|
}
|
||||||
if (!cardKey) {
|
if (!apiKey) {
|
||||||
throw new Error('缺少 GPC 卡密。');
|
throw new Error('缺少 GPC API Key。');
|
||||||
}
|
}
|
||||||
const requestUrl = buildGpcCardBalanceRequestUrl(apiUrl, cardKey);
|
const requestUrl = buildGpcApiKeyBalanceRequestUrl(apiUrl);
|
||||||
if (!requestUrl) {
|
if (!requestUrl) {
|
||||||
throw new Error('缺少 GPC API 地址。');
|
throw new Error('缺少 GPC API 地址。');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(requestUrl, {
|
const response = await fetch(requestUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/json' },
|
headers: buildGpcApiKeyHeaders(apiKey, { Accept: 'application/json' }),
|
||||||
});
|
});
|
||||||
const rawText = await response.text();
|
const rawText = await response.text();
|
||||||
let payload = {};
|
let payload = {};
|
||||||
@@ -9653,20 +9740,28 @@ async function refreshGpcCardBalance(state = {}, options = {}) {
|
|||||||
} catch {
|
} catch {
|
||||||
payload = { raw: rawText };
|
payload = { raw: rawText };
|
||||||
}
|
}
|
||||||
const balanceText = formatGpcCardBalancePayload(payload) || rawText || '未知';
|
const balancePayload = self.GoPayUtils?.unwrapGpcResponse
|
||||||
|
? self.GoPayUtils.unwrapGpcResponse(payload)
|
||||||
|
: (payload?.data && typeof payload === 'object' ? payload.data : payload);
|
||||||
|
const balanceText = formatGpcApiKeyBalancePayload(payload) || rawText || '未知';
|
||||||
const updates = {
|
const updates = {
|
||||||
gopayHelperBalance: balanceText,
|
gopayHelperBalance: balanceText,
|
||||||
gopayHelperBalancePayload: payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : { raw: String(payload || '') },
|
gopayHelperBalancePayload: balancePayload && typeof balancePayload === 'object' && !Array.isArray(balancePayload) ? balancePayload : { raw: String(balancePayload || '') },
|
||||||
gopayHelperBalanceUpdatedAt: Date.now(),
|
gopayHelperBalanceUpdatedAt: Date.now(),
|
||||||
gopayHelperBalanceError: '',
|
gopayHelperBalanceError: '',
|
||||||
};
|
};
|
||||||
const flowId = String(payload?.flow_id || payload?.flowId || '').trim();
|
const flowId = String(balancePayload?.flow_id || balancePayload?.flowId || '').trim();
|
||||||
if (flowId) {
|
if (flowId) {
|
||||||
updates.gopayHelperFlowId = flowId;
|
updates.gopayHelperFlowId = flowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok || payload?.ok === false) {
|
const unifiedOk = self.GoPayUtils?.isGpcUnifiedResponseOk
|
||||||
const detail = payload?.error || payload?.message || payload?.detail || `HTTP ${response.status}`;
|
? self.GoPayUtils.isGpcUnifiedResponseOk(payload)
|
||||||
|
: true;
|
||||||
|
if (!response.ok || payload?.ok === false || !unifiedOk) {
|
||||||
|
const detail = self.GoPayUtils?.extractGpcResponseErrorDetail
|
||||||
|
? self.GoPayUtils.extractGpcResponseErrorDetail(payload, response.status)
|
||||||
|
: (payload?.data?.detail || payload?.error || payload?.message || payload?.detail || `HTTP ${response.status}`);
|
||||||
const errorUpdates = { ...updates, gopayHelperBalanceError: String(detail || '余额查询失败') };
|
const errorUpdates = { ...updates, gopayHelperBalanceError: String(detail || '余额查询失败') };
|
||||||
await setPersistentSettings(errorUpdates);
|
await setPersistentSettings(errorUpdates);
|
||||||
broadcastDataUpdate(errorUpdates);
|
broadcastDataUpdate(errorUpdates);
|
||||||
@@ -9685,6 +9780,8 @@ async function refreshGpcCardBalance(state = {}, options = {}) {
|
|||||||
return { balance: balanceText, payload, updatedAt: updates.gopayHelperBalanceUpdatedAt };
|
return { balance: balanceText, payload, updatedAt: updates.gopayHelperBalanceUpdatedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshGpcCardBalance = refreshGpcApiKeyBalance;
|
||||||
|
|
||||||
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
|
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
|
||||||
addLog,
|
addLog,
|
||||||
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
|
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
|
||||||
@@ -9709,6 +9806,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR
|
|||||||
isAddPhoneAuthFailure,
|
isAddPhoneAuthFailure,
|
||||||
isPhoneSmsPlatformRateLimitFailure,
|
isPhoneSmsPlatformRateLimitFailure,
|
||||||
isPlusCheckoutNonFreeTrialFailure,
|
isPlusCheckoutNonFreeTrialFailure,
|
||||||
|
isGpcTaskEndedFailure,
|
||||||
isRestartCurrentAttemptError,
|
isRestartCurrentAttemptError,
|
||||||
isStep4Route405RecoveryLimitFailure,
|
isStep4Route405RecoveryLimitFailure,
|
||||||
isSignupUserAlreadyExistsFailure,
|
isSignupUserAlreadyExistsFailure,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
getState,
|
getState,
|
||||||
hasSavedProgress,
|
hasSavedProgress,
|
||||||
isAddPhoneAuthFailure,
|
isAddPhoneAuthFailure,
|
||||||
|
isGpcTaskEndedFailure,
|
||||||
isPhoneSmsPlatformRateLimitFailure,
|
isPhoneSmsPlatformRateLimitFailure,
|
||||||
isPlusCheckoutNonFreeTrialFailure,
|
isPlusCheckoutNonFreeTrialFailure,
|
||||||
isRestartCurrentAttemptError,
|
isRestartCurrentAttemptError,
|
||||||
@@ -529,6 +530,9 @@
|
|||||||
&& isAddPhoneAuthFailure(err);
|
&& isAddPhoneAuthFailure(err);
|
||||||
const blockedByPlusNonFreeTrial = typeof isPlusCheckoutNonFreeTrialFailure === 'function'
|
const blockedByPlusNonFreeTrial = typeof isPlusCheckoutNonFreeTrialFailure === 'function'
|
||||||
&& isPlusCheckoutNonFreeTrialFailure(err);
|
&& isPlusCheckoutNonFreeTrialFailure(err);
|
||||||
|
const blockedByGpcTaskEnded = typeof isGpcTaskEndedFailure === 'function'
|
||||||
|
? isGpcTaskEndedFailure(err)
|
||||||
|
: /GPC_TASK_ENDED::/i.test(err?.message || String(err || ''));
|
||||||
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
|
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
|
||||||
&& !keepSameEmailUntilAddPhone
|
&& !keepSameEmailUntilAddPhone
|
||||||
&& isSignupUserAlreadyExistsFailure(err);
|
&& isSignupUserAlreadyExistsFailure(err);
|
||||||
@@ -537,6 +541,7 @@
|
|||||||
const canRetry = !blockedByAddPhone
|
const canRetry = !blockedByAddPhone
|
||||||
&& !blockedByPhoneNoSupply
|
&& !blockedByPhoneNoSupply
|
||||||
&& !blockedByPlusNonFreeTrial
|
&& !blockedByPlusNonFreeTrial
|
||||||
|
&& !blockedByGpcTaskEnded
|
||||||
&& !blockedBySignupUserAlreadyExists
|
&& !blockedBySignupUserAlreadyExists
|
||||||
&& autoRunSkipFailures
|
&& autoRunSkipFailures
|
||||||
&& attemptRun < maxAttemptsForRound;
|
&& attemptRun < maxAttemptsForRound;
|
||||||
@@ -650,6 +655,41 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (blockedByGpcTaskEnded) {
|
||||||
|
roundSummary.status = 'failed';
|
||||||
|
roundSummary.finalFailureReason = reason;
|
||||||
|
await setState({
|
||||||
|
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||||
|
});
|
||||||
|
await appendRoundRecordIfNeeded('failed', reason);
|
||||||
|
cancelPendingCommands('当前轮因 GPC 任务已结束。');
|
||||||
|
await broadcastStopToContentScripts();
|
||||||
|
if (!autoRunSkipFailures) {
|
||||||
|
await addLog(
|
||||||
|
`第 ${targetRun}/${totalRuns} 轮 GPC 任务已结束,自动重试未开启,当前自动运行将停止。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
stoppedEarly = true;
|
||||||
|
await broadcastAutoRunStatus('stopped', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun,
|
||||||
|
sessionId: 0,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(`第 ${targetRun}/${totalRuns} 轮 GPC 任务已结束,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||||
|
await addLog(
|
||||||
|
targetRun < totalRuns
|
||||||
|
? `第 ${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,自动流程将继续下一轮。`
|
||||||
|
: `第 ${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,已无后续轮次,本次自动运行结束。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
forceFreshTabsNextRun = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (blockedBySignupUserAlreadyExists) {
|
if (blockedBySignupUserAlreadyExists) {
|
||||||
roundSummary.status = 'failed';
|
roundSummary.status = 'failed';
|
||||||
roundSummary.finalFailureReason = reason;
|
roundSummary.finalFailureReason = reason;
|
||||||
|
|||||||
@@ -71,7 +71,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getErrorMessage(error) {
|
function getErrorMessage(error) {
|
||||||
return String(typeof error === 'string' ? error : error?.message || '');
|
return String(typeof error === 'string' ? error : error?.message || '')
|
||||||
|
.replace(/^GPC_TASK_ENDED::/i, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVerificationMailPollingError(error) {
|
function isVerificationMailPollingError(error) {
|
||||||
|
|||||||
@@ -1031,7 +1031,7 @@
|
|||||||
|
|
||||||
case 'REFRESH_GPC_CARD_BALANCE': {
|
case 'REFRESH_GPC_CARD_BALANCE': {
|
||||||
if (typeof refreshGpcCardBalance !== 'function') {
|
if (typeof refreshGpcCardBalance !== 'function') {
|
||||||
throw new Error('GPC 卡密余额查询能力尚未接入。');
|
throw new Error('GPC API Key 余额查询能力尚未接入。');
|
||||||
}
|
}
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
const result = await refreshGpcCardBalance({
|
const result = await refreshGpcCardBalance({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
|
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz';
|
||||||
|
|
||||||
function createPlusCheckoutCreateExecutor(deps = {}) {
|
function createPlusCheckoutCreateExecutor(deps = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
completeStepFromBackground,
|
completeStepFromBackground,
|
||||||
ensureContentScriptReadyOnTabUntilStopped,
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
fetch: fetchImpl = null,
|
fetch: fetchImpl = null,
|
||||||
markCurrentRegistrationAccountUsed = null,
|
|
||||||
registerTab,
|
registerTab,
|
||||||
sendTabMessageUntilStopped,
|
sendTabMessageUntilStopped,
|
||||||
setState,
|
setState,
|
||||||
@@ -95,121 +94,17 @@
|
|||||||
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
|
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveGpcHelperCardKey(state = {}) {
|
function resolveGpcHelperApiKey(state = {}) {
|
||||||
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
|
const apiKey = String(
|
||||||
if (!cardKey) {
|
state?.gopayHelperApiKey
|
||||||
throw new Error('创建 GPC 订单失败:缺少卡密。');
|
|| state?.gpcApiKey
|
||||||
}
|
|| state?.apiKey
|
||||||
return cardKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveGpcHelperCustomerEmail(state = {}) {
|
|
||||||
const email = String(
|
|
||||||
state?.email
|
|
||||||
|| state?.currentEmail
|
|
||||||
|| state?.registrationEmail
|
|
||||||
|| state?.accountEmail
|
|
||||||
|| state?.mailboxEmail
|
|
||||||
|| ''
|
|| ''
|
||||||
).trim().toLowerCase();
|
).trim();
|
||||||
if (!email) {
|
if (!apiKey) {
|
||||||
throw new Error('创建 GPC 订单失败:缺少当前轮邮箱。');
|
throw new Error('创建 GPC 订单失败:缺少 API Key。');
|
||||||
}
|
}
|
||||||
return email;
|
return apiKey;
|
||||||
}
|
|
||||||
|
|
||||||
function parseGpcAmount(value) {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return Number.isFinite(value) ? { amount: value, raw: String(value) } : null;
|
|
||||||
}
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const raw = String(value || '').trim();
|
|
||||||
if (!raw || !/\d/.test(raw)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const match = raw.match(/([+-]?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{1,2})|[+-]?\d+(?:[.,]\d{1,2})?)/);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let numericText = String(match[1] || '').trim();
|
|
||||||
const lastComma = numericText.lastIndexOf(',');
|
|
||||||
const lastDot = numericText.lastIndexOf('.');
|
|
||||||
if (lastComma > -1 && lastDot > -1) {
|
|
||||||
const decimalSeparator = lastComma > lastDot ? ',' : '.';
|
|
||||||
const thousandsSeparator = decimalSeparator === ',' ? '.' : ',';
|
|
||||||
numericText = numericText
|
|
||||||
.replace(new RegExp(`\\${thousandsSeparator}`, 'g'), '')
|
|
||||||
.replace(decimalSeparator, '.');
|
|
||||||
} else if (lastComma > -1) {
|
|
||||||
numericText = numericText.replace(',', '.');
|
|
||||||
}
|
|
||||||
const amount = Number(numericText.replace(/[^\d.+-]/g, ''));
|
|
||||||
return Number.isFinite(amount) ? { amount, raw } : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGpcAmountKey(key = '') {
|
|
||||||
const normalized = String(key || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
|
||||||
if (!normalized) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (/(?:^|_)(?:id|guid|uuid|phone|country|postal|zip|code|count|status|time|timestamp|created|updated|expires|challenge|client|reference|currency|state)(?:_|$)/i.test(normalized)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /(?:amount|balance|total|due|payable|gross|subtotal|price|charge)/i.test(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findGpcNonZeroAmount(payload = {}) {
|
|
||||||
const seen = new Set();
|
|
||||||
function visit(value, path = [], depth = 0) {
|
|
||||||
if (value == null || depth > 10) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const key = path[path.length - 1] || '';
|
|
||||||
if (isGpcAmountKey(key)) {
|
|
||||||
const parsed = parseGpcAmount(value);
|
|
||||||
if (parsed && Math.abs(parsed.amount) >= 0.005) {
|
|
||||||
return { ...parsed, path: path.join('.') };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (seen.has(value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
seen.add(value);
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
for (let index = 0; index < value.length; index += 1) {
|
|
||||||
const found = visit(value[index], [...path, String(index)], depth + 1);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (const [childKey, childValue] of Object.entries(value)) {
|
|
||||||
const found = visit(childValue, [...path, childKey], depth + 1);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return visit(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function abortGpcNonFreeTrialIfNeeded(data = {}, state = {}) {
|
|
||||||
const nonZeroAmount = findGpcNonZeroAmount(data);
|
|
||||||
if (!nonZeroAmount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const amountLabel = nonZeroAmount.raw || String(nonZeroAmount.amount);
|
|
||||||
await addLog(`步骤 6:GPC 接口返回余额非 0(${amountLabel}),当前账号没有免费试用资格,将跳过当前账号。`, 'warn');
|
|
||||||
if (typeof markCurrentRegistrationAccountUsed === 'function') {
|
|
||||||
await markCurrentRegistrationAccountUsed(state, {
|
|
||||||
reason: 'plus-checkout-non-free-trial',
|
|
||||||
logPrefix: 'GPC:当前账号没有免费试用资格',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 6:GPC 接口返回余额非 0(${amountLabel}),当前账号没有免费试用资格,已跳过支付提交。`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeGpcHelperBaseUrl(apiUrl = '') {
|
function normalizeGpcHelperBaseUrl(apiUrl = '') {
|
||||||
@@ -220,7 +115,10 @@
|
|||||||
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
||||||
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
||||||
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/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\/balance(?:\?.*)?$/i, '');
|
||||||
|
normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, '');
|
||||||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,6 +135,47 @@
|
|||||||
return `${baseUrl}${normalizedPath}`;
|
return `${baseUrl}${normalizedPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGpcTaskCreateUrl(apiUrl = '') {
|
||||||
|
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||||
|
if (rootScope.GoPayUtils?.buildGpcTaskCreateUrl) {
|
||||||
|
return rootScope.GoPayUtils.buildGpcTaskCreateUrl(apiUrl);
|
||||||
|
}
|
||||||
|
return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getGpcResponseErrorDetail(payload = {}, status = 0) {
|
||||||
|
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||||
|
if (rootScope.GoPayUtils?.extractGpcResponseErrorDetail) {
|
||||||
|
return rootScope.GoPayUtils.extractGpcResponseErrorDetail(payload, status);
|
||||||
|
}
|
||||||
|
return payload?.data?.detail || payload?.detail || payload?.message || payload?.error || `HTTP ${status || 0}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
|
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
|
||||||
const fetcher = typeof fetchImpl === 'function'
|
const fetcher = typeof fetchImpl === 'function'
|
||||||
? fetchImpl
|
? fetchImpl
|
||||||
@@ -283,14 +222,14 @@
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('创建 GPC 订单失败:缺少 accessToken。');
|
throw new Error('创建 GPC 订单失败:缺少 accessToken。');
|
||||||
}
|
}
|
||||||
const apiUrl = buildGpcHelperApiUrl(state?.gopayHelperApiUrl, '/api/checkout/start');
|
const apiUrl = buildGpcTaskCreateUrl(state?.gopayHelperApiUrl);
|
||||||
if (!apiUrl) {
|
if (!apiUrl) {
|
||||||
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
||||||
}
|
}
|
||||||
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
|
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
|
||||||
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
|
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
|
||||||
const pin = String(state?.gopayHelperPin || '').trim();
|
const pin = String(state?.gopayHelperPin || '').trim();
|
||||||
const cardKey = resolveGpcHelperCardKey(state);
|
const apiKey = resolveGpcHelperApiKey(state);
|
||||||
if (!phoneNumber) {
|
if (!phoneNumber) {
|
||||||
throw new Error('创建 GPC 订单失败:缺少手机号。');
|
throw new Error('创建 GPC 订单失败:缺少手机号。');
|
||||||
}
|
}
|
||||||
@@ -300,32 +239,11 @@
|
|||||||
|
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const payload = {
|
const payload = {
|
||||||
token,
|
access_token: token,
|
||||||
entry_point: 'all_plans_pricing_modal',
|
phone_mode: 'manual',
|
||||||
plan_name: 'chatgptplusplan',
|
|
||||||
billing_details: { country: 'ID', currency: 'IDR' },
|
|
||||||
promo_campaign: {
|
|
||||||
promo_campaign_id: 'plus-1-month-free',
|
|
||||||
is_coupon_from_query_param: false,
|
|
||||||
},
|
|
||||||
checkout_ui_mode: 'custom',
|
|
||||||
proxy: { type: 'direct', url: '' },
|
|
||||||
tax_region: {
|
|
||||||
country: 'US',
|
|
||||||
line1: '1208 Oakdale Street',
|
|
||||||
city: 'Jonesboro',
|
|
||||||
postal_code: '72401',
|
|
||||||
state: 'AR',
|
|
||||||
},
|
|
||||||
customer_email: resolveGpcHelperCustomerEmail(state),
|
|
||||||
card_key: cardKey,
|
|
||||||
gopay_link: {
|
|
||||||
type: 'gopay',
|
|
||||||
country_code: countryCode,
|
country_code: countryCode,
|
||||||
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
|
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
|
||||||
phone_mode: 'manual',
|
|
||||||
otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel),
|
otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const orderCreatedAt = Date.now();
|
const orderCreatedAt = Date.now();
|
||||||
@@ -335,38 +253,26 @@
|
|||||||
Accept: '*/*',
|
Accept: '*/*',
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': apiKey,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
const referenceId = String(data?.reference_id || data?.referenceId || '').trim();
|
const taskData = unwrapGpcResponse(data);
|
||||||
const gopayGuid = String(data?.gopay_guid || data?.gopayGuid || '').trim();
|
const taskId = String(taskData?.task_id || taskData?.taskId || '').trim();
|
||||||
const redirectUrl = String(data?.redirect_url || data?.redirectUrl || '').trim();
|
|
||||||
const nextAction = String(data?.next_action || data?.nextAction || '').trim();
|
|
||||||
const flowId = String(data?.flow_id || data?.flowId || '').trim();
|
|
||||||
const challengeId = String(data?.challenge_id || data?.challengeId || '').trim();
|
|
||||||
|
|
||||||
if (response?.ok) {
|
if (!response?.ok || !isGpcUnifiedResponseOk(data) || !taskId) {
|
||||||
await abortGpcNonFreeTrialIfNeeded(data, state);
|
const detail = getGpcResponseErrorDetail(data, response?.status || 0);
|
||||||
}
|
|
||||||
|
|
||||||
if (!response?.ok || !referenceId) {
|
|
||||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
|
||||||
const detail = rootScope.GoPayUtils?.extractGpcResponseErrorDetail
|
|
||||||
? rootScope.GoPayUtils.extractGpcResponseErrorDetail(data, response?.status || 0)
|
|
||||||
: (data?.detail || data?.message || data?.error || `HTTP ${response?.status || 0}`);
|
|
||||||
throw new Error(`创建 GPC 订单失败:${detail}`);
|
throw new Error(`创建 GPC 订单失败:${detail}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
referenceId,
|
taskId,
|
||||||
gopayGuid,
|
taskStatus: String(taskData?.status || '').trim(),
|
||||||
redirectUrl,
|
statusText: String(taskData?.status_text || taskData?.statusText || '').trim(),
|
||||||
nextAction,
|
remoteStage: String(taskData?.remote_stage || taskData?.remoteStage || '').trim(),
|
||||||
flowId,
|
|
||||||
challengeId,
|
|
||||||
orderCreatedAt,
|
orderCreatedAt,
|
||||||
responsePayload: data && typeof data === 'object' && !Array.isArray(data) ? data : null,
|
responsePayload: taskData && typeof taskData === 'object' && !Array.isArray(taskData) ? taskData : null,
|
||||||
country: 'ID',
|
country: 'ID',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||||
@@ -398,16 +304,21 @@
|
|||||||
plusCheckoutCountry: result.country || 'ID',
|
plusCheckoutCountry: result.country || 'ID',
|
||||||
plusCheckoutCurrency: result.currency || 'IDR',
|
plusCheckoutCurrency: result.currency || 'IDR',
|
||||||
plusCheckoutSource: result.checkoutSource,
|
plusCheckoutSource: result.checkoutSource,
|
||||||
gopayHelperReferenceId: result.referenceId,
|
gopayHelperTaskId: result.taskId,
|
||||||
gopayHelperGoPayGuid: result.gopayGuid,
|
gopayHelperTaskStatus: result.taskStatus,
|
||||||
gopayHelperRedirectUrl: result.redirectUrl,
|
gopayHelperStatusText: result.statusText,
|
||||||
gopayHelperNextAction: result.nextAction,
|
gopayHelperRemoteStage: result.remoteStage,
|
||||||
gopayHelperFlowId: result.flowId,
|
gopayHelperTaskPayload: result.responsePayload,
|
||||||
gopayHelperChallengeId: result.challengeId,
|
gopayHelperReferenceId: '',
|
||||||
gopayHelperStartPayload: result.responsePayload,
|
gopayHelperGoPayGuid: '',
|
||||||
|
gopayHelperRedirectUrl: '',
|
||||||
|
gopayHelperNextAction: '',
|
||||||
|
gopayHelperFlowId: '',
|
||||||
|
gopayHelperChallengeId: '',
|
||||||
|
gopayHelperStartPayload: null,
|
||||||
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
|
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
|
||||||
});
|
});
|
||||||
await addLog('步骤 6:GPC 订单已创建,准备继续下一步。', 'info');
|
await addLog(`步骤 6:GPC 任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
|
||||||
await completeStepFromBackground(6, {
|
await completeStepFromBackground(6, {
|
||||||
plusCheckoutCountry: result.country || 'ID',
|
plusCheckoutCountry: result.country || 'ID',
|
||||||
plusCheckoutCurrency: result.currency || 'IDR',
|
plusCheckoutCurrency: result.currency || 'IDR',
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
|
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz';
|
||||||
|
const GPC_TASK_POLL_INTERVAL_MS = 3000;
|
||||||
const PAYMENT_METHOD_CONFIGS = {
|
const PAYMENT_METHOD_CONFIGS = {
|
||||||
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
||||||
id: PLUS_PAYMENT_METHOD_PAYPAL,
|
id: PLUS_PAYMENT_METHOD_PAYPAL,
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
function isGpcHelperCheckout(state = {}) {
|
function isGpcHelperCheckout(state = {}) {
|
||||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER
|
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER
|
||||||
|| (normalizeText(state?.plusCheckoutSource) === PLUS_PAYMENT_METHOD_GPC_HELPER
|
|| (normalizeText(state?.plusCheckoutSource) === PLUS_PAYMENT_METHOD_GPC_HELPER
|
||||||
&& Boolean(state?.gopayHelperReferenceId));
|
&& Boolean(state?.gopayHelperTaskId || state?.gopayHelperReferenceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
function compactCountryText(value = '') {
|
function compactCountryText(value = '') {
|
||||||
@@ -123,7 +124,10 @@
|
|||||||
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
||||||
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
||||||
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/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\/balance(?:\?.*)?$/i, '');
|
||||||
|
normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, '');
|
||||||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +157,6 @@
|
|||||||
const payload = {
|
const payload = {
|
||||||
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
|
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
|
||||||
otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''),
|
otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''),
|
||||||
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
|
|
||||||
};
|
};
|
||||||
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
|
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
|
||||||
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
||||||
@@ -183,7 +186,6 @@
|
|||||||
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
|
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
|
||||||
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
|
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
|
||||||
pin: String(input.pin ?? '').trim().replace(/[^\d]/g, ''),
|
pin: String(input.pin ?? '').trim().replace(/[^\d]/g, ''),
|
||||||
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
|
|
||||||
};
|
};
|
||||||
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
||||||
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
||||||
@@ -207,11 +209,160 @@
|
|||||||
return rootScope.GoPayUtils.extractGpcResponseErrorDetail(payload, status);
|
return rootScope.GoPayUtils.extractGpcResponseErrorDetail(payload, status);
|
||||||
}
|
}
|
||||||
if (payload && typeof payload === 'object') {
|
if (payload && typeof payload === 'object') {
|
||||||
return payload.detail || payload.message || payload.error || payload.error_description || payload.reason || `HTTP ${status || 0}`;
|
return payload?.data?.detail || payload.detail || payload.message || payload.error || payload.error_description || payload.reason || `HTTP ${status || 0}`;
|
||||||
}
|
}
|
||||||
return `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) {
|
async function postGpcJsonWithFallback(apiUrl, endpointPath, primaryPayload, fallbackPayload, timeoutMs = 30000) {
|
||||||
const requestUrl = `${apiUrl}${endpointPath}`;
|
const requestUrl = `${apiUrl}${endpointPath}`;
|
||||||
const send = (payload) => fetchJsonWithTimeout(requestUrl, {
|
const send = (payload) => fetchJsonWithTimeout(requestUrl, {
|
||||||
@@ -312,35 +463,42 @@
|
|||||||
return Number.isFinite(parsed) ? parsed : 0;
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLocalSmsHelperOtpUrl(state = {}, referenceId = '') {
|
function buildLocalSmsHelperOtpUrl(state = {}, taskId = '', options = {}) {
|
||||||
const baseUrl = normalizeLocalSmsHelperBaseUrl(state?.gopayHelperLocalSmsHelperUrl);
|
const baseUrl = normalizeLocalSmsHelperBaseUrl(state?.gopayHelperLocalSmsHelperUrl);
|
||||||
const url = new URL(`${baseUrl}/otp`);
|
const url = new URL(`${baseUrl}/otp`);
|
||||||
const normalizedReferenceId = String(referenceId || '').trim();
|
const normalizedTaskId = String(taskId || '').trim();
|
||||||
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
|
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
|
||||||
|
const afterOverrideMs = normalizeEpochMilliseconds(options?.afterMs || options?.after_ms || 0);
|
||||||
const orderCreatedAt = normalizeEpochMilliseconds(
|
const orderCreatedAt = normalizeEpochMilliseconds(
|
||||||
state?.gopayHelperOrderCreatedAt
|
state?.gopayHelperOrderCreatedAt
|
||||||
|
|| state?.gopayHelperTaskPayload?.created_at
|
||||||
|
|| state?.gopayHelperTaskPayload?.createdAt
|
||||||
|| state?.gopayHelperStartPayload?.order_created_at
|
|| state?.gopayHelperStartPayload?.order_created_at
|
||||||
|| state?.gopayHelperStartPayload?.orderCreatedAt
|
|| state?.gopayHelperStartPayload?.orderCreatedAt
|
||||||
|| state?.gopayHelperStartPayload?.created_at
|
|| state?.gopayHelperStartPayload?.created_at
|
||||||
|| state?.gopayHelperStartPayload?.createdAt
|
|| state?.gopayHelperStartPayload?.createdAt
|
||||||
);
|
);
|
||||||
if (normalizedReferenceId) {
|
if (normalizedTaskId) {
|
||||||
url.searchParams.set('reference_id', normalizedReferenceId);
|
url.searchParams.set('task_id', normalizedTaskId);
|
||||||
|
url.searchParams.set('reference_id', normalizedTaskId);
|
||||||
}
|
}
|
||||||
if (phoneNumber) {
|
if (phoneNumber) {
|
||||||
url.searchParams.set('phone_number', phoneNumber);
|
url.searchParams.set('phone_number', phoneNumber);
|
||||||
}
|
}
|
||||||
if (orderCreatedAt > 0) {
|
const effectiveAfterMs = Math.max(orderCreatedAt, afterOverrideMs);
|
||||||
url.searchParams.set('after_ms', String(orderCreatedAt));
|
if (effectiveAfterMs > 0) {
|
||||||
|
url.searchParams.set('after_ms', String(effectiveAfterMs));
|
||||||
}
|
}
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollLocalSmsHelperOtp(state = {}, referenceId = '') {
|
async function pollLocalSmsHelperOtp(state = {}, taskId = '', options = {}) {
|
||||||
const timeoutSeconds = Math.max(10, Math.min(300, Number(state?.gopayHelperLocalSmsTimeoutSeconds) || 90));
|
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 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 deadline = Date.now() + timeoutSeconds * 1000;
|
||||||
const requestUrl = buildLocalSmsHelperOtpUrl(state, referenceId);
|
const requestUrl = buildLocalSmsHelperOtpUrl(state, taskId, options);
|
||||||
let lastMessage = '';
|
let lastMessage = '';
|
||||||
while (Date.now() <= deadline) {
|
while (Date.now() <= deadline) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
@@ -348,7 +506,7 @@
|
|||||||
const { response, data } = await fetchJsonWithTimeout(requestUrl, {
|
const { response, data } = await fetchJsonWithTimeout(requestUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
}, Math.min(8000, Math.max(1000, pollIntervalSeconds * 1000)));
|
}, requestTimeoutMs);
|
||||||
const otp = normalizeIncomingGpcSmsOtp(data || {});
|
const otp = normalizeIncomingGpcSmsOtp(data || {});
|
||||||
if (response?.ok && otp) {
|
if (response?.ok && otp) {
|
||||||
await setState({
|
await setState({
|
||||||
@@ -364,12 +522,56 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastMessage = error?.message || String(error || '未知错误');
|
lastMessage = error?.message || String(error || '未知错误');
|
||||||
}
|
}
|
||||||
|
if (singleAttempt) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
await sleepWithStop(pollIntervalSeconds * 1000);
|
await sleepWithStop(pollIntervalSeconds * 1000);
|
||||||
}
|
}
|
||||||
throw new Error(lastMessage || '本地 SMS Helper 等待 OTP 超时。');
|
throw new Error(lastMessage || '本地 SMS Helper 等待 OTP 超时。');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestGpcOtpInput({ title = '', message = '', referenceId = '' }) {
|
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 requestId = `otp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
const payload = {
|
const payload = {
|
||||||
plusManualConfirmationPending: true,
|
plusManualConfirmationPending: true,
|
||||||
@@ -378,18 +580,45 @@
|
|||||||
plusManualConfirmationMethod: 'gopay-otp',
|
plusManualConfirmationMethod: 'gopay-otp',
|
||||||
plusManualConfirmationTitle: title || 'GPC OTP 验证',
|
plusManualConfirmationTitle: title || 'GPC OTP 验证',
|
||||||
plusManualConfirmationMessage: message || '请输入 OTP 验证码',
|
plusManualConfirmationMessage: message || '请输入 OTP 验证码',
|
||||||
gopayHelperOtpRequestId: requestId,
|
gopayHelperLastInputError: String(lastInputError || '').trim(),
|
||||||
gopayHelperOtpReferenceId: referenceId,
|
gopayHelperApiInputDeadlineAt: String(inputDeadlineAt || '').trim(),
|
||||||
gopayHelperResolvedOtp: '',
|
gopayHelperResolvedOtp: '',
|
||||||
|
gopayHelperOtpRequestId: requestId,
|
||||||
|
gopayHelperOtpReferenceId: taskId,
|
||||||
};
|
};
|
||||||
await setState(payload);
|
await setState(payload);
|
||||||
if (typeof broadcastDataUpdate === 'function') {
|
if (typeof broadcastDataUpdate === 'function') {
|
||||||
broadcastDataUpdate(payload);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const checkInterval = setInterval(async () => {
|
const checkInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
throwIfStopped();
|
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();
|
const currentState = await getStateInternal();
|
||||||
if (!currentState?.plusManualConfirmationPending || currentState?.plusManualConfirmationRequestId !== requestId) {
|
if (!currentState?.plusManualConfirmationPending || currentState?.plusManualConfirmationRequestId !== requestId) {
|
||||||
clearInterval(checkInterval);
|
clearInterval(checkInterval);
|
||||||
@@ -408,122 +637,308 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {}) {
|
async function executeGpcHelperBilling(state = {}) {
|
||||||
const referenceId = String(state?.gopayHelperReferenceId || '').trim();
|
const taskId = String(state?.gopayHelperTaskId || '').trim();
|
||||||
const apiUrl = normalizeGpcHelperBaseUrl(state?.gopayHelperApiUrl || '');
|
const apiUrl = normalizeGpcHelperBaseUrl(state?.gopayHelperApiUrl || '');
|
||||||
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
|
const apiKey = String(
|
||||||
if (!referenceId) {
|
state?.gopayHelperApiKey
|
||||||
throw new Error('步骤 7:GPC 模式缺少 reference_id,请先执行步骤 6。');
|
|| 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('步骤 7:GPC 模式缺少 task_id,请先执行步骤 6。');
|
||||||
}
|
}
|
||||||
if (!apiUrl) {
|
if (!apiUrl) {
|
||||||
throw new Error('步骤 7:GPC 模式缺少 API 地址。');
|
throw new Error('步骤 7:GPC 模式缺少 API 地址。');
|
||||||
}
|
}
|
||||||
if (!cardKey) {
|
if (!apiKey) {
|
||||||
throw new Error('步骤 7:GPC 模式缺少卡密。');
|
throw new Error('步骤 7:GPC 模式缺少 API Key。');
|
||||||
}
|
|
||||||
await addLog(`步骤 7:GPC 模式开始 OTP 验证(reference_id: ${referenceId})...`, 'info');
|
|
||||||
let otp = '';
|
|
||||||
const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled);
|
|
||||||
if (useLocalSmsHelper) {
|
|
||||||
try {
|
|
||||||
await addLog('步骤 7:正在从本地 SMS Helper 等待 GPC OTP...', 'info');
|
|
||||||
otp = await pollLocalSmsHelperOtp(state, referenceId);
|
|
||||||
await addLog('步骤 7:本地 SMS Helper 已读取到 GPC OTP,准备提交验证。', 'ok');
|
|
||||||
} catch (error) {
|
|
||||||
await addLog(`步骤 7:本地 SMS Helper 未能自动读取 OTP:${error?.message || String(error || '未知错误')},改为手动输入。`, 'warn');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!otp) {
|
|
||||||
await addLog('步骤 7:等待用户输入 OTP...', 'info');
|
|
||||||
otp = await requestGpcOtpInput({
|
|
||||||
title: 'GPC OTP 验证',
|
|
||||||
message: `请输入收到的 OTP 验证码(reference_id: ${referenceId})`,
|
|
||||||
referenceId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const flowId = state?.gopayHelperFlowId
|
const rawPin = String(state?.gopayHelperPin || '').trim();
|
||||||
|| state?.gopayHelperStartPayload?.flow_id
|
const pinDigits = rawPin.replace(/[^\d]/g, '');
|
||||||
|| state?.gopayHelperStartPayload?.flowId
|
const pin = normalizeSixDigitPin(rawPin);
|
||||||
|| state?.gopayHelperBalancePayload?.flow_id
|
|
||||||
|| state?.gopayHelperBalancePayload?.flowId
|
|
||||||
|| '';
|
|
||||||
const baseInput = {
|
|
||||||
reference_id: referenceId,
|
|
||||||
otp,
|
|
||||||
card_key: cardKey,
|
|
||||||
gopay_guid: state?.gopayHelperGoPayGuid || '',
|
|
||||||
redirect_url: state?.gopayHelperRedirectUrl || '',
|
|
||||||
flow_id: flowId,
|
|
||||||
};
|
|
||||||
await addLog('步骤 7:正在提交 OTP...', 'info');
|
|
||||||
const otpResponse = await postGpcJsonWithFallback(
|
|
||||||
apiUrl,
|
|
||||||
'/api/gopay/otp',
|
|
||||||
buildGpcOtpPayload(baseInput),
|
|
||||||
buildGpcOtpRetryPayload(baseInput),
|
|
||||||
30000
|
|
||||||
);
|
|
||||||
if (!otpResponse?.response?.ok) {
|
|
||||||
throw new Error(`步骤 7:OTP 验证失败:${getGpcResponseErrorDetail(otpResponse?.data, otpResponse?.response?.status || 0)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const otpData = otpResponse?.data || {};
|
|
||||||
const challengeId = String(
|
|
||||||
otpData?.challenge_id
|
|
||||||
|| otpData?.challengeId
|
|
||||||
|| state?.gopayHelperChallengeId
|
|
||||||
|| state?.gopayHelperStartPayload?.challenge_id
|
|
||||||
|| state?.gopayHelperStartPayload?.challengeId
|
|
||||||
|| ''
|
|
||||||
).trim();
|
|
||||||
const nextFlowId = String(otpData?.flow_id || otpData?.flowId || baseInput.flow_id || '').trim();
|
|
||||||
const gopayGuid = String(otpData?.gopay_guid || otpData?.gopayGuid || state?.gopayHelperGoPayGuid || '').trim();
|
|
||||||
const redirectUrl = String(otpData?.redirect_url || otpData?.redirectUrl || state?.gopayHelperRedirectUrl || '').trim();
|
|
||||||
if (!challengeId) {
|
|
||||||
throw new Error('步骤 7:GPC OTP 验证后未返回 challenge_id。');
|
|
||||||
}
|
|
||||||
const pin = String(state?.gopayHelperPin || '').trim().replace(/[^\d]/g, '');
|
|
||||||
if (!pin) {
|
if (!pin) {
|
||||||
throw new Error('步骤 7:GPC 模式缺少 PIN 配置。');
|
if (taskId && apiUrl && apiKey) {
|
||||||
|
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
|
||||||
|
}
|
||||||
|
throw new Error(pinDigits
|
||||||
|
? '步骤 7:GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
|
||||||
|
: '步骤 7:GPC 模式缺少 PIN 配置。');
|
||||||
}
|
}
|
||||||
|
|
||||||
await setState({
|
await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId})...`, 'info');
|
||||||
gopayHelperChallengeId: challengeId,
|
try {
|
||||||
gopayHelperFlowId: nextFlowId,
|
while (Date.now() <= deadline) {
|
||||||
gopayHelperGoPayGuid: gopayGuid,
|
throwIfStopped();
|
||||||
gopayHelperRedirectUrl: redirectUrl,
|
const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey);
|
||||||
});
|
const statusText = task?.status_text || task?.status || '处理中';
|
||||||
|
const remoteStage = task?.remote_stage || '';
|
||||||
await addLog('步骤 7:正在提交 PIN...', 'info');
|
const waitingFor = task?.api_waiting_for || '';
|
||||||
const pinInput = {
|
await addLog(`步骤 7:GPC 任务状态:${statusText}${remoteStage ? `(${remoteStage})` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info');
|
||||||
reference_id: referenceId,
|
|
||||||
challenge_id: challengeId,
|
|
||||||
gopay_guid: gopayGuid,
|
|
||||||
redirect_url: redirectUrl,
|
|
||||||
flow_id: nextFlowId,
|
|
||||||
pin,
|
|
||||||
card_key: cardKey,
|
|
||||||
};
|
|
||||||
const pinResponse = await postGpcJsonWithFallback(
|
|
||||||
apiUrl,
|
|
||||||
'/api/gopay/pin',
|
|
||||||
buildGpcPinPayload(pinInput),
|
|
||||||
buildGpcPinRetryPayload(pinInput),
|
|
||||||
30000
|
|
||||||
);
|
|
||||||
if (!pinResponse?.response?.ok) {
|
|
||||||
throw new Error(`步骤 7:PIN 验证失败:${getGpcResponseErrorDetail(pinResponse?.data, pinResponse?.response?.status || 0)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (task.status === 'completed') {
|
||||||
|
terminalReached = true;
|
||||||
await setState({
|
await setState({
|
||||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||||
gopayHelperPinPayload: pinResponse?.data || null,
|
|
||||||
});
|
});
|
||||||
await addLog('步骤 7:GPC 支付完成,准备继续下一步。', 'ok');
|
await addLog('步骤 7:GPC 任务已完成,准备继续下一步。', 'ok');
|
||||||
await completeStepFromBackground(7, {
|
await completeStepFromBackground(7, {
|
||||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
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(`步骤 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) && !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 = '') {
|
function resolveMeiguodizhiCountryCode(value = '') {
|
||||||
|
|||||||
+143
-18
@@ -4,7 +4,7 @@
|
|||||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
|
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz';
|
||||||
|
|
||||||
function normalizePlusPaymentMethod(value = '') {
|
function normalizePlusPaymentMethod(value = '') {
|
||||||
const normalized = String(value || '').trim().toLowerCase();
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
@@ -62,7 +62,10 @@
|
|||||||
normalized = normalized.replace(/\/+$/g, '');
|
normalized = normalized.replace(/\/+$/g, '');
|
||||||
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
||||||
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/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\/balance(?:\?.*)?$/i, '');
|
||||||
|
normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, '');
|
||||||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,12 +78,85 @@
|
|||||||
return `${baseUrl}${normalizedPath}`;
|
return `${baseUrl}${normalizedPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGpcCardBalanceUrl(apiUrl = '', cardKey = '') {
|
function buildGpcApiKeyBalanceUrl(apiUrl = '') {
|
||||||
const endpoint = buildGpcHelperApiUrl(apiUrl, '/api/card/balance');
|
return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance');
|
||||||
if (!endpoint) {
|
}
|
||||||
|
|
||||||
|
function buildGpcCardBalanceUrl(apiUrl = '') {
|
||||||
|
return buildGpcApiKeyBalanceUrl(apiUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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 buildGpcTaskCreateUrl(apiUrl = '') {
|
||||||
|
return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGpcTaskId(value = '') {
|
||||||
|
return String(value || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGpcTaskQueryUrl(apiUrl = '', taskId = '') {
|
||||||
|
const normalizedTaskId = normalizeGpcTaskId(taskId);
|
||||||
|
return buildGpcHelperApiUrl(apiUrl, `/api/gp/tasks/${encodeURIComponent(normalizedTaskId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGpcTaskActionUrl(apiUrl = '', taskId = '', action = '') {
|
||||||
|
const normalizedTaskId = normalizeGpcTaskId(taskId);
|
||||||
|
const normalizedAction = String(action || '').trim().replace(/^\/+|\/+$/g, '');
|
||||||
|
return buildGpcHelperApiUrl(apiUrl, `/api/gp/tasks/${encodeURIComponent(normalizedTaskId)}/${normalizedAction}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapGpcResponse(payload = {}) {
|
||||||
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
const hasUnifiedShape = Object.prototype.hasOwnProperty.call(payload, 'data')
|
||||||
|
&& (
|
||||||
|
Object.prototype.hasOwnProperty.call(payload, 'code')
|
||||||
|
|| Object.prototype.hasOwnProperty.call(payload, 'message')
|
||||||
|
);
|
||||||
|
return hasUnifiedShape ? (payload.data ?? {}) : payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGpcUnifiedResponseOk(payload = {}) {
|
||||||
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(payload, 'code')) {
|
||||||
|
return payload.ok !== false;
|
||||||
|
}
|
||||||
|
const code = Number(payload.code);
|
||||||
|
if (Number.isFinite(code)) {
|
||||||
|
return code >= 200 && code < 300;
|
||||||
|
}
|
||||||
|
return String(payload.code || '').trim() === '200';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGpcErrorField(field) {
|
||||||
|
if (field === undefined || field === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return `${endpoint}?card_key=${encodeURIComponent(String(cardKey || '').trim())}`;
|
if (typeof field === 'string') {
|
||||||
|
return field.trim();
|
||||||
|
}
|
||||||
|
if (typeof field !== 'object') {
|
||||||
|
return String(field).trim();
|
||||||
|
}
|
||||||
|
const key = Array.isArray(field.loc)
|
||||||
|
? field.loc.join('.')
|
||||||
|
: String(field.field || field.path || field.name || field.param || '').trim();
|
||||||
|
const message = String(field.msg || field.message || field.error || field.detail || field.reason || '').trim();
|
||||||
|
return [key, message].filter(Boolean).join(': ') || JSON.stringify(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractGpcResponseErrorDetail(payload = {}, status = 0) {
|
function extractGpcResponseErrorDetail(payload = {}, status = 0) {
|
||||||
@@ -93,6 +169,27 @@
|
|||||||
return 'GOPAY已经绑了订阅,需要手动解绑';
|
return 'GOPAY已经绑了订阅,需要手动解绑';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = payload.data;
|
||||||
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||||||
|
const nestedDetail = data.detail ?? data.error ?? data.reason;
|
||||||
|
if (nestedDetail !== undefined && nestedDetail !== null && String(nestedDetail).trim()) {
|
||||||
|
const nestedText = String(nestedDetail).trim();
|
||||||
|
return /account\s+already\s+linked/i.test(nestedText)
|
||||||
|
? 'GOPAY已经绑了订阅,需要手动解绑'
|
||||||
|
: nestedText;
|
||||||
|
}
|
||||||
|
const fields = data.fields ?? data.errors;
|
||||||
|
if (Array.isArray(fields) && fields.length > 0) {
|
||||||
|
const formatted = fields
|
||||||
|
.map(formatGpcErrorField)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('; ');
|
||||||
|
if (formatted) {
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const direct = payload.detail
|
const direct = payload.detail
|
||||||
?? payload.message
|
?? payload.message
|
||||||
?? payload.error
|
?? payload.error
|
||||||
@@ -136,7 +233,6 @@
|
|||||||
const payload = {
|
const payload = {
|
||||||
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
|
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
|
||||||
otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''),
|
otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''),
|
||||||
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
|
|
||||||
};
|
};
|
||||||
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
||||||
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
|
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
|
||||||
@@ -158,7 +254,6 @@
|
|||||||
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
|
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
|
||||||
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
|
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
|
||||||
pin: normalizeGoPayPin(input.pin ?? ''),
|
pin: normalizeGoPayPin(input.pin ?? ''),
|
||||||
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
|
|
||||||
};
|
};
|
||||||
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
||||||
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
||||||
@@ -172,25 +267,45 @@
|
|||||||
return { ...payload, challengeId: payload.challenge_id };
|
return { ...payload, challengeId: payload.challenge_id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGpcTaskOtpPayload(input = {}) {
|
||||||
|
return {
|
||||||
|
otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGpcTaskPinPayload(input = {}) {
|
||||||
|
return {
|
||||||
|
pin: normalizeGoPayPin(input.pin ?? ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function formatGpcBalancePayload(payload = {}) {
|
function formatGpcBalancePayload(payload = {}) {
|
||||||
if (!payload || typeof payload !== 'object') {
|
const data = unwrapGpcResponse(payload);
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const candidates = [
|
const candidates = [
|
||||||
payload.remaining_uses,
|
data.remaining_uses,
|
||||||
payload.remainingUses,
|
data.remainingUses,
|
||||||
payload.balance,
|
data.balance,
|
||||||
payload.remaining,
|
data.remaining,
|
||||||
payload.uses,
|
data.uses,
|
||||||
payload.available_uses,
|
data.available_uses,
|
||||||
payload.availableUses,
|
data.availableUses,
|
||||||
];
|
];
|
||||||
const firstValue = candidates.find((value) => value !== undefined && value !== null && String(value).trim() !== '');
|
const firstValue = candidates.find((value) => value !== undefined && value !== null && String(value).trim() !== '');
|
||||||
const status = String(payload.card_status || payload.cardStatus || payload.status || '').trim();
|
const totalUses = data.total_uses ?? data.totalUses;
|
||||||
const flowId = String(payload.flow_id || payload.flowId || '').trim();
|
const usedUses = data.used_uses ?? data.usedUses;
|
||||||
|
const status = String(data.status || data.card_status || data.cardStatus || '').trim();
|
||||||
|
const flowId = String(data.flow_id || data.flowId || '').trim();
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (firstValue !== undefined) {
|
if (firstValue !== undefined) {
|
||||||
parts.push(`余额 ${firstValue}`);
|
parts.push(totalUses !== undefined && totalUses !== null && String(totalUses).trim() !== ''
|
||||||
|
? `余额 ${firstValue}/${totalUses}`
|
||||||
|
: `余额 ${firstValue}`);
|
||||||
|
}
|
||||||
|
if (usedUses !== undefined && usedUses !== null && String(usedUses).trim() !== '') {
|
||||||
|
parts.push(`已用 ${usedUses}`);
|
||||||
}
|
}
|
||||||
if (status) {
|
if (status) {
|
||||||
parts.push(`状态 ${status}`);
|
parts.push(`状态 ${status}`);
|
||||||
@@ -208,14 +323,23 @@
|
|||||||
PLUS_PAYMENT_METHOD_GOPAY,
|
PLUS_PAYMENT_METHOD_GOPAY,
|
||||||
PLUS_PAYMENT_METHOD_PAYPAL,
|
PLUS_PAYMENT_METHOD_PAYPAL,
|
||||||
buildGpcCardBalanceUrl,
|
buildGpcCardBalanceUrl,
|
||||||
|
buildGpcApiKeyBalanceUrl,
|
||||||
|
buildGpcApiKeyHeaders,
|
||||||
buildGpcHelperApiUrl,
|
buildGpcHelperApiUrl,
|
||||||
buildGpcOtpPayload,
|
buildGpcOtpPayload,
|
||||||
buildGpcOtpRetryPayload,
|
buildGpcOtpRetryPayload,
|
||||||
buildGpcPinPayload,
|
buildGpcPinPayload,
|
||||||
buildGpcPinRetryPayload,
|
buildGpcPinRetryPayload,
|
||||||
|
buildGpcTaskActionUrl,
|
||||||
|
buildGpcTaskCreateUrl,
|
||||||
|
buildGpcTaskOtpPayload,
|
||||||
|
buildGpcTaskPinPayload,
|
||||||
|
buildGpcTaskQueryUrl,
|
||||||
extractGpcResponseErrorDetail,
|
extractGpcResponseErrorDetail,
|
||||||
formatGpcBalancePayload,
|
formatGpcBalancePayload,
|
||||||
|
isGpcUnifiedResponseOk,
|
||||||
normalizeGpcHelperBaseUrl,
|
normalizeGpcHelperBaseUrl,
|
||||||
|
normalizeGpcTaskId,
|
||||||
normalizeGoPayCountryCode,
|
normalizeGoPayCountryCode,
|
||||||
normalizeGoPayPhone,
|
normalizeGoPayPhone,
|
||||||
normalizeGoPayPhoneForCountry,
|
normalizeGoPayPhoneForCountry,
|
||||||
@@ -223,5 +347,6 @@
|
|||||||
normalizeGoPayPin,
|
normalizeGoPayPin,
|
||||||
normalizeGpcOtpChannel,
|
normalizeGpcOtpChannel,
|
||||||
normalizePlusPaymentMethod,
|
normalizePlusPaymentMethod,
|
||||||
|
unwrapGpcResponse,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -264,7 +264,7 @@
|
|||||||
<option value="gopay">GoPay</option>
|
<option value="gopay">GoPay</option>
|
||||||
<option value="gpc-helper">GPC</option>
|
<option value="gpc-helper">GPC</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="btn-gpc-card-key-purchase" class="btn btn-outline btn-sm data-inline-btn" type="button" style="display:none;">购买卡密</button>
|
<button id="btn-gpc-card-key-purchase" class="btn btn-outline btn-sm data-inline-btn" type="button" style="display:none;">获取 API Key</button>
|
||||||
<span class="setting-caption" id="plus-payment-method-caption">PayPal 订阅链路</span>
|
<span class="setting-caption" id="plus-payment-method-caption">PayPal 订阅链路</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,15 +283,20 @@
|
|||||||
<button id="btn-add-paypal-account" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
|
<button id="btn-add-paypal-account" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row" id="row-gpc-helper-api" style="display:none;">
|
||||||
|
<span class="data-label">GPC API</span>
|
||||||
|
<input type="text" id="input-gpc-helper-api" class="data-input"
|
||||||
|
placeholder="https://gpc.leftcode.xyz" autocomplete="off" />
|
||||||
|
</div>
|
||||||
<div class="data-row" id="row-gpc-helper-card-key" style="display:none;">
|
<div class="data-row" id="row-gpc-helper-card-key" style="display:none;">
|
||||||
<span class="data-label">GPC 卡密</span>
|
<span class="data-label">GPC API Key</span>
|
||||||
<div class="data-inline">
|
<div class="data-inline">
|
||||||
<div class="input-with-icon">
|
<div class="input-with-icon">
|
||||||
<input type="password" id="input-gpc-helper-card-key" class="data-input data-input-with-icon"
|
<input type="password" id="input-gpc-helper-card-key" class="data-input data-input-with-icon"
|
||||||
placeholder="请输入购买的 GPC 卡密" autocomplete="off" />
|
placeholder="请输入 GPC API Key(gpc-...)" autocomplete="off" />
|
||||||
<button id="btn-toggle-gpc-helper-card-key" class="input-icon-btn" type="button"
|
<button id="btn-toggle-gpc-helper-card-key" class="input-icon-btn" type="button"
|
||||||
data-password-toggle="input-gpc-helper-card-key" data-show-label="显示 GPC 卡密"
|
data-password-toggle="input-gpc-helper-card-key" data-show-label="显示 GPC API Key"
|
||||||
data-hide-label="隐藏 GPC 卡密" aria-label="显示 GPC 卡密" title="显示 GPC 卡密"></button>
|
data-hide-label="隐藏 GPC API Key" aria-label="显示 GPC API Key" title="显示 GPC API Key"></button>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-gpc-helper-balance" class="btn btn-ghost btn-xs data-inline-btn" type="button">查余额</button>
|
<button id="btn-gpc-helper-balance" class="btn btn-ghost btn-xs data-inline-btn" type="button">查余额</button>
|
||||||
<span id="display-gpc-helper-balance" class="data-value mono">余额未获取</span>
|
<span id="display-gpc-helper-balance" class="data-value mono">余额未获取</span>
|
||||||
@@ -334,7 +339,7 @@
|
|||||||
<span class="toggle-switch-thumb"></span>
|
<span class="toggle-switch-thumb"></span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<span class="setting-caption">从本机 helper 读取当前通道 OTP,失败后回退手动输入</span>
|
<span class="setting-caption">从本机 helper 读取当前通道 OTP,开启后不弹手动验证码</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-gpc-helper-local-sms-url" style="display:none;">
|
<div class="data-row" id="row-gpc-helper-local-sms-url" style="display:none;">
|
||||||
<span class="data-label">OTP 接口</span>
|
<span class="data-label">OTP 接口</span>
|
||||||
|
|||||||
+10
-8
@@ -496,7 +496,7 @@ const stepsList = document.querySelector('.steps-list');
|
|||||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
|
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz';
|
||||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
||||||
const SIGNUP_METHOD_EMAIL = 'email';
|
const SIGNUP_METHOD_EMAIL = 'email';
|
||||||
const SIGNUP_METHOD_PHONE = 'phone';
|
const SIGNUP_METHOD_PHONE = 'phone';
|
||||||
@@ -2695,7 +2695,7 @@ function applyCloudflareTempEmailSettingsState(state = {}) {
|
|||||||
function collectSettingsPayload() {
|
function collectSettingsPayload() {
|
||||||
const defaultGpcHelperApiUrl = typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined'
|
const defaultGpcHelperApiUrl = typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined'
|
||||||
? DEFAULT_GPC_HELPER_API_URL
|
? DEFAULT_GPC_HELPER_API_URL
|
||||||
: 'https://gopay.hwork.pro';
|
: 'https://gpc.leftcode.xyz';
|
||||||
const { domains, activeDomain } = getCloudflareDomainsFromState();
|
const { domains, activeDomain } = getCloudflareDomainsFromState();
|
||||||
const selectedCloudflareDomain = normalizeCloudflareDomainValue(
|
const selectedCloudflareDomain = normalizeCloudflareDomainValue(
|
||||||
!cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
|
!cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
|
||||||
@@ -3287,9 +3287,10 @@ function collectSettingsPayload() {
|
|||||||
: (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi
|
: (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi
|
||||||
? String(inputGpcHelperApi.value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, '')
|
? String(inputGpcHelperApi.value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, '')
|
||||||
: String(latestState?.gopayHelperApiUrl || defaultGpcHelperApiUrl).trim()),
|
: String(latestState?.gopayHelperApiUrl || defaultGpcHelperApiUrl).trim()),
|
||||||
gopayHelperCardKey: typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey
|
gopayHelperApiKey: typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey
|
||||||
? String(inputGpcHelperCardKey.value || '').trim()
|
? String(inputGpcHelperCardKey.value || '').trim()
|
||||||
: String(latestState?.gopayHelperCardKey || '').trim(),
|
: String(latestState?.gopayHelperApiKey || latestState?.gopayHelperCardKey || '').trim(),
|
||||||
|
gopayHelperCardKey: '',
|
||||||
gopayHelperCountryCode: window.GoPayUtils?.normalizeGoPayCountryCode
|
gopayHelperCountryCode: window.GoPayUtils?.normalizeGoPayCountryCode
|
||||||
? window.GoPayUtils.normalizeGoPayCountryCode(typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode ? selectGpcHelperCountryCode.value : latestState?.gopayHelperCountryCode)
|
? window.GoPayUtils.normalizeGoPayCountryCode(typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode ? selectGpcHelperCountryCode.value : latestState?.gopayHelperCountryCode)
|
||||||
: (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode
|
: (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode
|
||||||
@@ -7178,6 +7179,7 @@ function updatePlusModeUI() {
|
|||||||
row.style.display = enabled && selectedMethod === paypalValue ? '' : 'none';
|
row.style.display = enabled && selectedMethod === paypalValue ? '' : 'none';
|
||||||
});
|
});
|
||||||
[
|
[
|
||||||
|
typeof rowGpcHelperApi !== 'undefined' ? rowGpcHelperApi : null,
|
||||||
typeof rowGpcHelperCardKey !== 'undefined' ? rowGpcHelperCardKey : null,
|
typeof rowGpcHelperCardKey !== 'undefined' ? rowGpcHelperCardKey : null,
|
||||||
typeof rowGpcHelperCountryCode !== 'undefined' ? rowGpcHelperCountryCode : null,
|
typeof rowGpcHelperCountryCode !== 'undefined' ? rowGpcHelperCountryCode : null,
|
||||||
typeof rowGpcHelperPhone !== 'undefined' ? rowGpcHelperPhone : null,
|
typeof rowGpcHelperPhone !== 'undefined' ? rowGpcHelperPhone : null,
|
||||||
@@ -7427,7 +7429,7 @@ async function openPlusManualConfirmationDialog(options = {}) {
|
|||||||
validate: (value) => {
|
validate: (value) => {
|
||||||
const normalized = String(value || '').trim().replace(/[^\d]/g, '');
|
const normalized = String(value || '').trim().replace(/[^\d]/g, '');
|
||||||
if (!normalized) return '请输入 OTP 验证码。';
|
if (!normalized) return '请输入 OTP 验证码。';
|
||||||
if (normalized.length < 4) return 'OTP 验证码长度过短,请检查。';
|
if (!/^\d{6}$/.test(normalized)) return 'OTP 必须是 6 位数字,请检查。';
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -7920,11 +7922,11 @@ function applySettingsState(state) {
|
|||||||
if (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi) {
|
if (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi) {
|
||||||
const defaultGpcHelperApiUrl = typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined'
|
const defaultGpcHelperApiUrl = typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined'
|
||||||
? DEFAULT_GPC_HELPER_API_URL
|
? DEFAULT_GPC_HELPER_API_URL
|
||||||
: 'https://gopay.hwork.pro';
|
: 'https://gpc.leftcode.xyz';
|
||||||
inputGpcHelperApi.value = state?.gopayHelperApiUrl || defaultGpcHelperApiUrl;
|
inputGpcHelperApi.value = state?.gopayHelperApiUrl || defaultGpcHelperApiUrl;
|
||||||
}
|
}
|
||||||
if (typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey) {
|
if (typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey) {
|
||||||
inputGpcHelperCardKey.value = state?.gopayHelperCardKey || '';
|
inputGpcHelperCardKey.value = state?.gopayHelperApiKey || state?.gopayHelperCardKey || '';
|
||||||
}
|
}
|
||||||
if (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode) {
|
if (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode) {
|
||||||
const normalizedCountryCode = window.GoPayUtils?.normalizeGoPayCountryCode
|
const normalizedCountryCode = window.GoPayUtils?.normalizeGoPayCountryCode
|
||||||
@@ -11341,7 +11343,7 @@ btnGpcHelperBalance?.addEventListener('click', async () => {
|
|||||||
source: 'sidepanel',
|
source: 'sidepanel',
|
||||||
payload: {
|
payload: {
|
||||||
gopayHelperApiUrl: inputGpcHelperApi?.value || DEFAULT_GPC_HELPER_API_URL,
|
gopayHelperApiUrl: inputGpcHelperApi?.value || DEFAULT_GPC_HELPER_API_URL,
|
||||||
gopayHelperCardKey: inputGpcHelperCardKey?.value || '',
|
gopayHelperApiKey: inputGpcHelperCardKey?.value || '',
|
||||||
gopayHelperCountryCode: selectGpcHelperCountryCode?.value || '+86',
|
gopayHelperCountryCode: selectGpcHelperCountryCode?.value || '+86',
|
||||||
reason: 'manual',
|
reason: 'manual',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -332,6 +332,167 @@ test('auto-run controller treats phone-number supply exhaustion as round-fatal a
|
|||||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auto-run controller treats ended GPC task as round-fatal and skips same-round retries', async () => {
|
||||||
|
const events = {
|
||||||
|
logs: [],
|
||||||
|
broadcasts: [],
|
||||||
|
accountRecords: [],
|
||||||
|
runCalls: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentState = {
|
||||||
|
stepStatuses: {},
|
||||||
|
vpsUrl: 'https://example.com/vps',
|
||||||
|
vpsPassword: 'secret',
|
||||||
|
customPassword: '',
|
||||||
|
autoRunSkipFailures: true,
|
||||||
|
autoRunFallbackThreadIntervalMinutes: 0,
|
||||||
|
autoRunDelayEnabled: false,
|
||||||
|
autoRunDelayMinutes: 30,
|
||||||
|
autoStepDelaySeconds: null,
|
||||||
|
mailProvider: '163',
|
||||||
|
emailGenerator: 'duck',
|
||||||
|
gmailBaseEmail: '',
|
||||||
|
mail2925BaseEmail: '',
|
||||||
|
emailPrefix: 'demo',
|
||||||
|
inbucketHost: '',
|
||||||
|
inbucketMailbox: '',
|
||||||
|
cloudflareDomain: '',
|
||||||
|
cloudflareDomains: [],
|
||||||
|
tabRegistry: {},
|
||||||
|
sourceLastUrls: {},
|
||||||
|
autoRunRoundSummaries: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
state: {
|
||||||
|
autoRunActive: false,
|
||||||
|
autoRunCurrentRun: 0,
|
||||||
|
autoRunTotalRuns: 1,
|
||||||
|
autoRunAttemptRun: 0,
|
||||||
|
autoRunSessionId: 0,
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return { ...this.state };
|
||||||
|
},
|
||||||
|
set(updates = {}) {
|
||||||
|
this.state = { ...this.state, ...updates };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let sessionSeed = 0;
|
||||||
|
|
||||||
|
const controller = api.createAutoRunController({
|
||||||
|
addLog: async (message, level = 'info') => {
|
||||||
|
events.logs.push({ message, level });
|
||||||
|
},
|
||||||
|
appendAccountRunRecord: async (status, _state, reason) => {
|
||||||
|
events.accountRecords.push({ status, reason });
|
||||||
|
return { status, reason };
|
||||||
|
},
|
||||||
|
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
|
||||||
|
AUTO_RUN_RETRY_DELAY_MS: 3000,
|
||||||
|
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
|
||||||
|
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
|
||||||
|
broadcastAutoRunStatus: async (phase, payload = {}) => {
|
||||||
|
events.broadcasts.push({ phase, ...payload });
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||||
|
autoRunPhase: phase,
|
||||||
|
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
|
||||||
|
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
|
||||||
|
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
|
||||||
|
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
broadcastStopToContentScripts: async () => {},
|
||||||
|
cancelPendingCommands: () => {},
|
||||||
|
clearStopRequest: () => {},
|
||||||
|
createAutoRunSessionId: () => {
|
||||||
|
sessionSeed += 1;
|
||||||
|
return sessionSeed;
|
||||||
|
},
|
||||||
|
getAutoRunStatusPayload: (phase, payload = {}) => ({
|
||||||
|
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||||
|
autoRunPhase: phase,
|
||||||
|
autoRunCurrentRun: payload.currentRun ?? 0,
|
||||||
|
autoRunTotalRuns: payload.totalRuns ?? 1,
|
||||||
|
autoRunAttemptRun: payload.attemptRun ?? 0,
|
||||||
|
autoRunSessionId: payload.sessionId ?? 0,
|
||||||
|
}),
|
||||||
|
getErrorMessage: (error) => String(error?.message || error || '').replace(/^GPC_TASK_ENDED::/i, ''),
|
||||||
|
getFirstUnfinishedStep: () => 1,
|
||||||
|
getPendingAutoRunTimerPlan: () => null,
|
||||||
|
getRunningSteps: () => [],
|
||||||
|
getState: async () => ({
|
||||||
|
...currentState,
|
||||||
|
stepStatuses: { ...(currentState.stepStatuses || {}) },
|
||||||
|
tabRegistry: { ...(currentState.tabRegistry || {}) },
|
||||||
|
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
|
||||||
|
}),
|
||||||
|
getStopRequested: () => false,
|
||||||
|
hasSavedProgress: () => false,
|
||||||
|
isAddPhoneAuthFailure: () => false,
|
||||||
|
isGpcTaskEndedFailure: (error) => /GPC_TASK_ENDED::/i.test(error?.message || String(error || '')),
|
||||||
|
isRestartCurrentAttemptError: () => false,
|
||||||
|
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
|
||||||
|
launchAutoRunTimerPlan: async () => false,
|
||||||
|
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
|
||||||
|
persistAutoRunTimerPlan: async () => ({}),
|
||||||
|
resetState: async () => {
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
stepStatuses: {},
|
||||||
|
tabRegistry: {},
|
||||||
|
sourceLastUrls: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
runAutoSequenceFromStep: async () => {
|
||||||
|
events.runCalls += 1;
|
||||||
|
if (events.runCalls === 1) {
|
||||||
|
throw new Error('GPC_TASK_ENDED::等待 OTP 超过 60 秒,任务已超时');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
setState: async (updates = {}) => {
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
...updates,
|
||||||
|
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
|
||||||
|
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
|
||||||
|
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfAutoRunSessionStopped: (sessionId) => {
|
||||||
|
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
|
||||||
|
throw new Error('流程已被用户停止。');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
waitForRunningStepsToFinish: async () => currentState,
|
||||||
|
chrome: {
|
||||||
|
runtime: {
|
||||||
|
sendMessage() {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.autoRunLoop(2, {
|
||||||
|
autoRunSkipFailures: true,
|
||||||
|
mode: 'restart',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(events.runCalls, 2, 'ended GPC task should fail current round and continue next round');
|
||||||
|
assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false);
|
||||||
|
assert.equal(events.accountRecords.length, 1);
|
||||||
|
assert.equal(events.accountRecords[0].status, 'failed');
|
||||||
|
assert.match(events.accountRecords[0].reason, /等待 OTP/);
|
||||||
|
assert.ok(events.logs.some(({ message }) => /GPC 任务.*继续下一轮|继续下一轮/.test(message)));
|
||||||
|
});
|
||||||
|
|
||||||
test('auto-run controller keeps same-round retrying for step9 local replacement exhaustion errors', async () => {
|
test('auto-run controller keeps same-round retrying for step9 local replacement exhaustion errors', async () => {
|
||||||
const events = {
|
const events = {
|
||||||
logs: [],
|
logs: [],
|
||||||
@@ -1150,4 +1311,3 @@ test('auto-run controller retries 5sim rate limit failures instead of treating c
|
|||||||
assert.equal(runtime.state.autoRunActive, false);
|
assert.equal(runtime.state.autoRunActive, false);
|
||||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -150,13 +150,16 @@ const self = {
|
|||||||
.replace(/\\/+$/g, '')
|
.replace(/\\/+$/g, '')
|
||||||
.replace(/\\/api\\/checkout\\/start$/i, '')
|
.replace(/\\/api\\/checkout\\/start$/i, '')
|
||||||
.replace(/\\/api\\/gopay\\/(?:otp|pin)$/i, '')
|
.replace(/\\/api\\/gopay\\/(?:otp|pin)$/i, '')
|
||||||
.replace(/\\/api\\/card\\/balance(?:\\?.*)?$/i, '');
|
.replace(/\\/api\\/gp\\/tasks(?:\\/[^/?#]+)?(?:\\/(?:otp|pin|stop))?(?:\\?.*)?$/i, '')
|
||||||
|
.replace(/\\/api\\/gp\\/balance(?:\\?.*)?$/i, '')
|
||||||
|
.replace(/\\/api\\/card\\/balance(?:\\?.*)?$/i, '')
|
||||||
|
.replace(/\\/api\\/card\\/redeem-api-key(?:\\?.*)?$/i, '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const PERSISTED_SETTING_DEFAULTS = {
|
const PERSISTED_SETTING_DEFAULTS = {
|
||||||
autoStepDelaySeconds: null,
|
autoStepDelaySeconds: null,
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz',
|
||||||
mailProvider: '163',
|
mailProvider: '163',
|
||||||
};
|
};
|
||||||
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
|
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
|
||||||
@@ -191,11 +194,19 @@ return {
|
|||||||
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal'), 'paypal');
|
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal'), 'paypal');
|
||||||
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'unknown'), 'paypal');
|
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'unknown'), 'paypal');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gopay.hwork.pro/api/checkout/start '),
|
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.leftcode.xyz/api/checkout/start '),
|
||||||
'https://gopay.hwork.pro'
|
'https://gpc.leftcode.xyz'
|
||||||
);
|
);
|
||||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiUrl', ''), 'https://gopay.hwork.pro');
|
assert.equal(
|
||||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperCardKey', ' card_123 '), 'card_123');
|
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.leftcode.xyz/api/gp/tasks/task_1/pin '),
|
||||||
|
'https://gpc.leftcode.xyz'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.leftcode.xyz/api/gp/balance '),
|
||||||
|
'https://gpc.leftcode.xyz'
|
||||||
|
);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiUrl', ''), 'https://gpc.leftcode.xyz');
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiKey', ' gpc-123 '), 'gpc-123');
|
||||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperCountryCode', ' 86 '), '+86');
|
assert.equal(api.normalizePersistentSettingValue('gopayHelperCountryCode', ' 86 '), '+86');
|
||||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneNumber', ' +86 138-0013-8000 '), '+8613800138000');
|
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneNumber', ' +86 138-0013-8000 '), '+8613800138000');
|
||||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperPin', ' 12-34-56 '), '123456');
|
assert.equal(api.normalizePersistentSettingValue('gopayHelperPin', ' 12-34-56 '), '123456');
|
||||||
|
|||||||
@@ -46,4 +46,8 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu
|
|||||||
assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页');
|
assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页');
|
||||||
assert.equal(loggingStatus.getLoginAuthStateLabel('add_email_page'), '添加邮箱页');
|
assert.equal(loggingStatus.getLoginAuthStateLabel('add_email_page'), '添加邮箱页');
|
||||||
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
|
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
|
||||||
|
assert.equal(
|
||||||
|
loggingStatus.getErrorMessage(new Error('GPC_TASK_ENDED::GPC OTP 超时,请重新创建任务')),
|
||||||
|
'GPC OTP 超时,请重新创建任务'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ function createRouter(overrides = {}) {
|
|||||||
clearStopRequest: () => {},
|
clearStopRequest: () => {},
|
||||||
closeLocalhostCallbackTabs: async () => {},
|
closeLocalhostCallbackTabs: async () => {},
|
||||||
closeTabsByUrlPrefix: async () => {},
|
closeTabsByUrlPrefix: async () => {},
|
||||||
|
completeStepFromBackground: async (step, payload) => {
|
||||||
|
events.notifyCompletions.push({ step, payload, via: 'completeStepFromBackground' });
|
||||||
|
},
|
||||||
deleteHotmailAccount: async () => {},
|
deleteHotmailAccount: async () => {},
|
||||||
deleteHotmailAccounts: async () => {},
|
deleteHotmailAccounts: async () => {},
|
||||||
deleteIcloudAlias: async () => {},
|
deleteIcloudAlias: async () => {},
|
||||||
@@ -540,7 +543,7 @@ test('message router refreshes GPC balance through explicit sidepanel message',
|
|||||||
const state = {
|
const state = {
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
gopayHelperApiUrl: 'http://localhost:18473/',
|
gopayHelperApiUrl: 'http://localhost:18473/',
|
||||||
gopayHelperCardKey: 'state_card',
|
gopayHelperApiKey: 'state_api_key',
|
||||||
};
|
};
|
||||||
const { router, events } = createRouter({ state });
|
const { router, events } = createRouter({ state });
|
||||||
|
|
||||||
@@ -548,7 +551,7 @@ test('message router refreshes GPC balance through explicit sidepanel message',
|
|||||||
type: 'REFRESH_GPC_CARD_BALANCE',
|
type: 'REFRESH_GPC_CARD_BALANCE',
|
||||||
source: 'sidepanel',
|
source: 'sidepanel',
|
||||||
payload: {
|
payload: {
|
||||||
gopayHelperCardKey: 'payload_card',
|
gopayHelperApiKey: 'payload_api_key',
|
||||||
reason: 'manual',
|
reason: 'manual',
|
||||||
},
|
},
|
||||||
}, {});
|
}, {});
|
||||||
@@ -556,6 +559,6 @@ test('message router refreshes GPC balance through explicit sidepanel message',
|
|||||||
assert.deepStrictEqual(response, { ok: true, balance: '余额 3' });
|
assert.deepStrictEqual(response, { ok: true, balance: '余额 3' });
|
||||||
assert.equal(events.balanceRefreshes.length, 1);
|
assert.equal(events.balanceRefreshes.length, 1);
|
||||||
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiUrl, 'http://localhost:18473/');
|
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiUrl, 'http://localhost:18473/');
|
||||||
assert.equal(events.balanceRefreshes[0].state.gopayHelperCardKey, 'payload_card');
|
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiKey, 'payload_api_key');
|
||||||
assert.deepStrictEqual(events.balanceRefreshes[0].options, { reason: 'manual' });
|
assert.deepStrictEqual(events.balanceRefreshes[0].options, { reason: 'manual' });
|
||||||
});
|
});
|
||||||
|
|||||||
+57
-55
@@ -24,88 +24,90 @@ test('GoPay utils keeps GPC helper payment method distinct', () => {
|
|||||||
assert.equal(api.normalizePlusPaymentMethod('unknown'), 'paypal');
|
assert.equal(api.normalizePlusPaymentMethod('unknown'), 'paypal');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GoPay utils builds GPC card balance URL from helper endpoints', () => {
|
test('GoPay utils builds GPC queue task and balance URLs from helper endpoints', () => {
|
||||||
const api = loadGoPayUtils();
|
const api = loadGoPayUtils();
|
||||||
assert.equal(api.DEFAULT_GPC_HELPER_API_URL, 'https://gopay.hwork.pro');
|
assert.equal(api.DEFAULT_GPC_HELPER_API_URL, 'https://gpc.leftcode.xyz');
|
||||||
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gopay.hwork.pro');
|
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gpc.leftcode.xyz');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.buildGpcHelperApiUrl('', '/api/checkout/start'),
|
api.buildGpcHelperApiUrl('', '/api/checkout/start'),
|
||||||
'https://gopay.hwork.pro/api/checkout/start'
|
'https://gpc.leftcode.xyz/api/checkout/start'
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.buildGpcCardBalanceUrl('http://localhost:18473/', ' card key/1 '),
|
api.buildGpcApiKeyBalanceUrl('http://localhost:18473/'),
|
||||||
'http://localhost:18473/api/card/balance?card_key=card%20key%2F1'
|
'http://localhost:18473/api/gp/balance'
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.buildGpcCardBalanceUrl('https://gopay.hwork.pro/api/checkout/start', 'GPC-1'),
|
api.buildGpcCardBalanceUrl('https://gpc.leftcode.xyz/api/gp/balance'),
|
||||||
'https://gopay.hwork.pro/api/card/balance?card_key=GPC-1'
|
'https://gpc.leftcode.xyz/api/gp/balance'
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
api.buildGpcApiKeyHeaders(' gpc-123 ', { Accept: 'application/json' }),
|
||||||
|
{ Accept: 'application/json', 'X-API-Key': 'gpc-123' }
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.buildGpcCardBalanceUrl('https://gopay.hwork.pro/api/card/balance?card_key=old', 'new'),
|
api.buildGpcTaskCreateUrl('https://gpc.leftcode.xyz/api/checkout/start'),
|
||||||
'https://gopay.hwork.pro/api/card/balance?card_key=new'
|
'https://gpc.leftcode.xyz/api/gp/tasks'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
api.buildGpcTaskQueryUrl('https://gpc.leftcode.xyz/api/gp/tasks/task_old?card_key=old', 'task/1'),
|
||||||
|
'https://gpc.leftcode.xyz/api/gp/tasks/task%2F1'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
api.buildGpcTaskActionUrl('https://gpc.leftcode.xyz/api/gp/tasks/task_old/stop', 'task_1', 'pin'),
|
||||||
|
'https://gpc.leftcode.xyz/api/gp/tasks/task_1/pin'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GoPay utils builds GPC OTP/PIN payloads with card_key and flow_id', () => {
|
test('GoPay utils builds GPC queue OTP/PIN payloads without card_key', () => {
|
||||||
const api = loadGoPayUtils();
|
const api = loadGoPayUtils();
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
api.buildGpcOtpPayload({
|
api.buildGpcTaskOtpPayload({ otp: ' 12-34 56 ', card_key: ' card_1 ', reference_id: 'ref_1' }),
|
||||||
reference_id: ' ref_1 ',
|
{ otp: '123456' }
|
||||||
otp: ' 12-34 56 ',
|
|
||||||
card_key: ' card_1 ',
|
|
||||||
gopay_guid: ' guid_1 ',
|
|
||||||
flow_id: ' flow_1 ',
|
|
||||||
redirect_url: 'https://pm-redirects.stripe.com/test',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
reference_id: 'ref_1',
|
|
||||||
otp: '123456',
|
|
||||||
card_key: 'card_1',
|
|
||||||
flow_id: 'flow_1',
|
|
||||||
gopay_guid: 'guid_1',
|
|
||||||
redirect_url: 'https://pm-redirects.stripe.com/test',
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
api.buildGpcOtpRetryPayload({ referenceId: 'ref_1', otp: '123456', cardKey: 'card_1', flowId: 'flow_1' }),
|
api.buildGpcTaskPinPayload({ pin: '65-43-21', cardKey: 'card_1', challengeId: 'challenge_1' }),
|
||||||
{
|
{ pin: '654321' }
|
||||||
reference_id: 'ref_1',
|
|
||||||
otp: '123456',
|
|
||||||
card_key: 'card_1',
|
|
||||||
flow_id: 'flow_1',
|
|
||||||
code: '123456',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert.deepEqual(
|
|
||||||
api.buildGpcPinPayload({
|
|
||||||
referenceId: 'ref_1',
|
|
||||||
challengeId: 'challenge_1',
|
|
||||||
gopayGuid: 'guid_1',
|
|
||||||
pin: '65-43-21',
|
|
||||||
cardKey: 'card_1',
|
|
||||||
flowId: 'flow_1',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
reference_id: 'ref_1',
|
|
||||||
challenge_id: 'challenge_1',
|
|
||||||
gopay_guid: 'guid_1',
|
|
||||||
pin: '654321',
|
|
||||||
card_key: 'card_1',
|
|
||||||
flow_id: 'flow_1',
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GoPay utils formats balance and maps linked-account errors', () => {
|
test('GoPay utils formats balance and maps linked-account errors', () => {
|
||||||
const api = loadGoPayUtils();
|
const api = loadGoPayUtils();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.formatGpcBalancePayload({ remaining_uses: 12, card_status: 'active', flow_id: 'flow_1' }),
|
api.formatGpcBalancePayload({ remaining_uses: 12, status: 'active', used_uses: 2, flow_id: 'flow_1' }),
|
||||||
'余额 12,状态 active,flow_id flow_1'
|
'余额 12,已用 2,状态 active,flow_id flow_1'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
api.formatGpcBalancePayload({
|
||||||
|
code: 200,
|
||||||
|
message: 'ok',
|
||||||
|
data: { remaining_uses: 0, total_uses: 3, used_uses: 3, status: 'active' },
|
||||||
|
}),
|
||||||
|
'余额 0/3,已用 3,状态 active'
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
api.unwrapGpcResponse({ code: 200, message: 'ok', data: { task_id: 'task_1' } }),
|
||||||
|
{ task_id: 'task_1' }
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.extractGpcResponseErrorDetail({ errors: [{ loc: ['body', 'otp'], msg: 'Field required' }] }, 422),
|
api.extractGpcResponseErrorDetail({ errors: [{ loc: ['body', 'otp'], msg: 'Field required' }] }, 422),
|
||||||
'body.otp: Field required'
|
'body.otp: Field required'
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
api.extractGpcResponseErrorDetail({
|
||||||
|
code: 400,
|
||||||
|
message: 'invalid_param',
|
||||||
|
data: { detail: '手机号不能为空', fields: [{ field: 'phone_number', message: '必填' }] },
|
||||||
|
}, 400),
|
||||||
|
'手机号不能为空'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
api.extractGpcResponseErrorDetail({
|
||||||
|
code: 400,
|
||||||
|
message: 'invalid_param',
|
||||||
|
data: { fields: [{ field: 'phone_number', message: '必填' }] },
|
||||||
|
}, 400),
|
||||||
|
'phone_number: 必填'
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.extractGpcResponseErrorDetail({ error_messages: ['account already linked'] }, 406),
|
api.extractGpcResponseErrorDetail({ error_messages: ['account already linked'] }, 406),
|
||||||
'GOPAY已经绑了订阅,需要手动解绑'
|
'GOPAY已经绑了订阅,需要手动解绑'
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ function createExecutorHarness({
|
|||||||
getState = null,
|
getState = null,
|
||||||
markCurrentRegistrationAccountUsed = async () => {},
|
markCurrentRegistrationAccountUsed = async () => {},
|
||||||
probeIpProxyExit = null,
|
probeIpProxyExit = null,
|
||||||
|
onSetState = null,
|
||||||
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
|
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
|
||||||
}) {
|
}) {
|
||||||
const api = loadPlusCheckoutBillingModule();
|
const api = loadPlusCheckoutBillingModule();
|
||||||
@@ -93,6 +94,7 @@ function createExecutorHarness({
|
|||||||
injectedAllFrames: false,
|
injectedAllFrames: false,
|
||||||
logs: [],
|
logs: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
|
sleeps: [],
|
||||||
states: [],
|
states: [],
|
||||||
waitedUrls: [],
|
waitedUrls: [],
|
||||||
};
|
};
|
||||||
@@ -158,8 +160,13 @@ function createExecutorHarness({
|
|||||||
getTabId: async () => null,
|
getTabId: async () => null,
|
||||||
isTabAlive: async () => false,
|
isTabAlive: async () => false,
|
||||||
markCurrentRegistrationAccountUsed,
|
markCurrentRegistrationAccountUsed,
|
||||||
setState: async (updates) => events.states.push(updates),
|
setState: async (updates) => {
|
||||||
sleepWithStop: async () => {},
|
events.states.push(updates);
|
||||||
|
if (typeof onSetState === 'function') {
|
||||||
|
await onSetState(updates, events);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sleepWithStop: async (ms) => events.sleeps.push(ms),
|
||||||
waitForTabCompleteUntilStopped: async () => checkoutTab,
|
waitForTabCompleteUntilStopped: async () => checkoutTab,
|
||||||
waitForTabUrlMatchUntilStopped: async (tabId, matcher) => {
|
waitForTabUrlMatchUntilStopped: async (tabId, matcher) => {
|
||||||
events.waitedUrls.push({ tabId });
|
events.waitedUrls.push({ tabId });
|
||||||
@@ -798,30 +805,68 @@ test('Plus checkout billing reports when the payment iframe exists but cannot re
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GPC billing normalizes API URL and submits OTP then PIN with card_key and flow_id', async () => {
|
|
||||||
|
function createGpcTaskResponse(data) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'ok',
|
||||||
|
data: {
|
||||||
|
task_id: 'task_123',
|
||||||
|
phone_mode: 'manual',
|
||||||
|
status_text: data.status === 'completed' ? '充值完成' : (data.status === 'otp_ready' ? '等待 PIN' : '处理中'),
|
||||||
|
api_input_deadline_at: data.api_input_deadline_at ?? new Date(Date.now() + 60000).toISOString(),
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('GPC billing polls queue task, submits WhatsApp OTP then PIN, and waits until completed', async () => {
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
let currentState = {
|
let currentState = {
|
||||||
plusManualConfirmationPending: true,
|
plusManualConfirmationPending: true,
|
||||||
plusManualConfirmationRequestId: '',
|
plusManualConfirmationRequestId: '',
|
||||||
};
|
};
|
||||||
|
let pollCount = 0;
|
||||||
const { events, executor } = createExecutorHarness({
|
const { events, executor } = createExecutorHarness({
|
||||||
frames: [],
|
frames: [],
|
||||||
stateByFrame: {},
|
stateByFrame: {},
|
||||||
getState: async () => currentState,
|
getState: async () => currentState,
|
||||||
fetchImpl: async (url, options = {}) => {
|
fetchImpl: async (url, options = {}) => {
|
||||||
fetchCalls.push({ url, options });
|
fetchCalls.push({ url, options });
|
||||||
if (url.endsWith('/api/gopay/otp')) {
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_123') {
|
||||||
|
pollCount += 1;
|
||||||
|
if (pollCount === 1) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => ({ reference_id: 'ref_123', challenge_id: 'challenge_456' }),
|
json: async () => createGpcTaskResponse({ status: 'active', remote_stage: 'whatsapp_otp_wait', api_waiting_for: 'otp' }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (url.endsWith('/api/gopay/pin')) {
|
if (pollCount === 2) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => ({ stage: 'gopay_complete' }),
|
json: async () => createGpcTaskResponse({ status: 'otp_ready', status_text: '等待 PIN', remote_stage: 'otp_ready', api_waiting_for: 'pin' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ status: 'completed', status_text: '充值完成', remote_stage: 'completed' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_123/otp')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ status: 'otp_ready', status_text: '等待 PIN', remote_stage: 'otp_ready', api_waiting_for: 'pin' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_123/pin')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ status: 'active', status_text: '处理中', remote_stage: 'payment_processing' }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error(`unexpected url: ${url}`);
|
throw new Error(`unexpected url: ${url}`);
|
||||||
@@ -831,12 +876,10 @@ test('GPC billing normalizes API URL and submits OTP then PIN with card_key and
|
|||||||
const run = executor.executePlusCheckoutBilling({
|
const run = executor.executePlusCheckoutBilling({
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
plusCheckoutSource: 'gpc-helper',
|
plusCheckoutSource: 'gpc-helper',
|
||||||
gopayHelperReferenceId: 'ref_123',
|
gopayHelperTaskId: 'task_123',
|
||||||
gopayHelperGoPayGuid: 'guid_789',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/api/gp/tasks/task_old/otp',
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/api/checkout/start',
|
|
||||||
gopayHelperPin: '654321',
|
gopayHelperPin: '654321',
|
||||||
gopayHelperCardKey: 'card_billing_123',
|
gopayHelperApiKey: 'gpc_billing_123',
|
||||||
gopayHelperFlowId: 'flow_billing_123',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
@@ -850,29 +893,24 @@ test('GPC billing normalizes API URL and submits OTP then PIN with card_key and
|
|||||||
|
|
||||||
await run;
|
await run;
|
||||||
|
|
||||||
assert.equal(fetchCalls[0].url, 'https://gopay.hwork.pro/api/gopay/otp');
|
assert.equal(fetchCalls[0].url, 'https://gpc.leftcode.xyz/api/gp/tasks/task_123');
|
||||||
assert.deepEqual(JSON.parse(fetchCalls[0].options.body), {
|
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_billing_123');
|
||||||
reference_id: 'ref_123',
|
const otpCall = fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_123/otp'));
|
||||||
otp: '123456',
|
const pinCall = fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_123/pin'));
|
||||||
card_key: 'card_billing_123',
|
assert.deepEqual(JSON.parse(otpCall.options.body), { otp: '123456' });
|
||||||
flow_id: 'flow_billing_123',
|
assert.equal(otpCall.options.headers['X-API-Key'], 'gpc_billing_123');
|
||||||
gopay_guid: 'guid_789',
|
assert.deepEqual(JSON.parse(pinCall.options.body), { pin: '654321' });
|
||||||
});
|
assert.equal(pinCall.options.headers['X-API-Key'], 'gpc_billing_123');
|
||||||
assert.equal(fetchCalls[1].url, 'https://gopay.hwork.pro/api/gopay/pin');
|
assert.ok(fetchCalls.findIndex((call) => call.url.endsWith('/api/gp/tasks/task_123/pin')) < fetchCalls.length - 1);
|
||||||
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
|
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_123' && state.gopayHelperTaskStatus === 'completed'), true);
|
||||||
reference_id: 'ref_123',
|
|
||||||
challenge_id: 'challenge_456',
|
|
||||||
gopay_guid: 'guid_789',
|
|
||||||
pin: '654321',
|
|
||||||
card_key: 'card_billing_123',
|
|
||||||
flow_id: 'flow_billing_123',
|
|
||||||
});
|
|
||||||
assert.equal(events.completed[0].step, 7);
|
assert.equal(events.completed[0].step, 7);
|
||||||
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
|
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
|
||||||
|
assert.ok(events.sleeps.includes(3000));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GPC billing reads OTP from local SMS helper when enabled', async () => {
|
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
|
let pollCount = 0;
|
||||||
const { events, executor } = createExecutorHarness({
|
const { events, executor } = createExecutorHarness({
|
||||||
frames: [],
|
frames: [],
|
||||||
stateByFrame: {},
|
stateByFrame: {},
|
||||||
@@ -885,18 +923,40 @@ test('GPC billing reads OTP from local SMS helper when enabled', async () => {
|
|||||||
json: async () => ({ ok: true, otp: '654321', message_id: 'sms-1' }),
|
json: async () => ({ ok: true, otp: '654321', message_id: 'sms-1' }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (url.endsWith('/api/gopay/otp')) {
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_sms') {
|
||||||
|
pollCount += 1;
|
||||||
|
if (pollCount === 1) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => ({ reference_id: 'ref_sms', challenge_id: 'challenge_sms' }),
|
json: async () => createGpcTaskResponse({ task_id: 'task_sms', status: 'active', remote_stage: 'sms_otp_wait', api_waiting_for: 'otp' }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (url.endsWith('/api/gopay/pin')) {
|
if (pollCount === 2) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => ({ stage: 'gopay_complete' }),
|
json: async () => createGpcTaskResponse({ task_id: 'task_sms', status: 'otp_ready', remote_stage: 'otp_ready', api_waiting_for: 'pin' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ task_id: 'task_sms', status: 'completed', remote_stage: 'completed' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_sms/otp')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ task_id: 'task_sms', status: 'otp_ready', remote_stage: 'otp_ready', api_waiting_for: 'pin' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_sms/pin')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ task_id: 'task_sms', status: 'active', remote_stage: 'payment_processing' }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error(`unexpected url: ${url}`);
|
throw new Error(`unexpected url: ${url}`);
|
||||||
@@ -906,10 +966,10 @@ test('GPC billing reads OTP from local SMS helper when enabled', async () => {
|
|||||||
await executor.executePlusCheckoutBilling({
|
await executor.executePlusCheckoutBilling({
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
plusCheckoutSource: 'gpc-helper',
|
plusCheckoutSource: 'gpc-helper',
|
||||||
gopayHelperReferenceId: 'ref_sms',
|
gopayHelperTaskId: 'task_sms',
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
gopayHelperPin: '654321',
|
gopayHelperPin: '654321',
|
||||||
gopayHelperCardKey: 'card_sms',
|
gopayHelperApiKey: 'gpc_sms',
|
||||||
gopayHelperOtpChannel: 'sms',
|
gopayHelperOtpChannel: 'sms',
|
||||||
gopayHelperLocalSmsHelperEnabled: true,
|
gopayHelperLocalSmsHelperEnabled: true,
|
||||||
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
|
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
|
||||||
@@ -919,21 +979,21 @@ test('GPC billing reads OTP from local SMS helper when enabled', async () => {
|
|||||||
|
|
||||||
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
|
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
|
||||||
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '654321'), true);
|
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '654321'), true);
|
||||||
const helperUrl = new URL(fetchCalls[0].url);
|
const helperUrl = new URL(fetchCalls[1].url);
|
||||||
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp');
|
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp');
|
||||||
assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_sms');
|
assert.equal(helperUrl.searchParams.get('task_id'), 'task_sms');
|
||||||
|
assert.equal(helperUrl.searchParams.get('reference_id'), 'task_sms');
|
||||||
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000');
|
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000');
|
||||||
assert.equal(helperUrl.searchParams.get('after_ms'), '1710000000000');
|
assert.equal(helperUrl.searchParams.get('after_ms'), '1710000000000');
|
||||||
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
|
assert.deepEqual(JSON.parse(fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_sms/otp')).options.body), {
|
||||||
reference_id: 'ref_sms',
|
|
||||||
otp: '654321',
|
otp: '654321',
|
||||||
card_key: 'card_sms',
|
|
||||||
});
|
});
|
||||||
assert.equal(events.completed[0].step, 7);
|
assert.equal(events.completed[0].step, 7);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => {
|
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => {
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
|
let pollCount = 0;
|
||||||
const { events, executor } = createExecutorHarness({
|
const { events, executor } = createExecutorHarness({
|
||||||
frames: [],
|
frames: [],
|
||||||
stateByFrame: {},
|
stateByFrame: {},
|
||||||
@@ -946,18 +1006,40 @@ test('GPC billing can read WhatsApp OTP from local helper when enabled', async (
|
|||||||
json: async () => ({ ok: true, otp: '765432', message_id: 'wa-1' }),
|
json: async () => ({ ok: true, otp: '765432', message_id: 'wa-1' }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (url.endsWith('/api/gopay/otp')) {
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_wa') {
|
||||||
|
pollCount += 1;
|
||||||
|
if (pollCount === 1) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => ({ reference_id: 'ref_wa', challenge_id: 'challenge_wa' }),
|
json: async () => createGpcTaskResponse({ task_id: 'task_wa', status: 'active', remote_stage: 'whatsapp_otp_wait', api_waiting_for: 'otp' }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (url.endsWith('/api/gopay/pin')) {
|
if (pollCount === 2) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => ({ stage: 'gopay_complete' }),
|
json: async () => createGpcTaskResponse({ task_id: 'task_wa', status: 'otp_ready', remote_stage: 'otp_ready', api_waiting_for: 'pin' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ task_id: 'task_wa', status: 'completed', remote_stage: 'completed' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_wa/otp')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ task_id: 'task_wa', status: 'otp_ready', remote_stage: 'otp_ready', api_waiting_for: 'pin' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_wa/pin')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ task_id: 'task_wa', status: 'active', remote_stage: 'payment_processing' }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error(`unexpected url: ${url}`);
|
throw new Error(`unexpected url: ${url}`);
|
||||||
@@ -967,10 +1049,10 @@ test('GPC billing can read WhatsApp OTP from local helper when enabled', async (
|
|||||||
await executor.executePlusCheckoutBilling({
|
await executor.executePlusCheckoutBilling({
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
plusCheckoutSource: 'gpc-helper',
|
plusCheckoutSource: 'gpc-helper',
|
||||||
gopayHelperReferenceId: 'ref_wa',
|
gopayHelperTaskId: 'task_wa',
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
gopayHelperPin: '654321',
|
gopayHelperPin: '654321',
|
||||||
gopayHelperCardKey: 'card_wa',
|
gopayHelperApiKey: 'gpc_wa',
|
||||||
gopayHelperOtpChannel: 'whatsapp',
|
gopayHelperOtpChannel: 'whatsapp',
|
||||||
gopayHelperLocalSmsHelperEnabled: true,
|
gopayHelperLocalSmsHelperEnabled: true,
|
||||||
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
|
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
|
||||||
@@ -979,19 +1061,221 @@ test('GPC billing can read WhatsApp OTP from local helper when enabled', async (
|
|||||||
|
|
||||||
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
|
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
|
||||||
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '765432'), true);
|
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '765432'), true);
|
||||||
const helperUrl = new URL(fetchCalls[0].url);
|
assert.deepEqual(JSON.parse(fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_wa/otp')).options.body), {
|
||||||
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp');
|
|
||||||
assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_wa');
|
|
||||||
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000');
|
|
||||||
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
|
|
||||||
reference_id: 'ref_wa',
|
|
||||||
otp: '765432',
|
otp: '765432',
|
||||||
card_key: 'card_wa',
|
|
||||||
});
|
});
|
||||||
assert.equal(events.completed[0].step, 7);
|
assert.equal(events.completed[0].step, 7);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GPC billing retries OTP with compatibility field after HTTP 400', async () => {
|
|
||||||
|
test('GPC billing helper mode does not open OTP dialog when helper has no code and task times out', async () => {
|
||||||
|
const fetchCalls = [];
|
||||||
|
const { events, executor } = createExecutorHarness({
|
||||||
|
frames: [],
|
||||||
|
stateByFrame: {},
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
fetchCalls.push({ url, options });
|
||||||
|
if (url.startsWith('http://127.0.0.1:18767/otp')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ ok: true, status: 'waiting', otp: '', message: '未查询到验证码' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_timeout') {
|
||||||
|
const queryCount = fetchCalls.filter((call) => call.url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_timeout').length;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse(queryCount === 1
|
||||||
|
? {
|
||||||
|
task_id: 'task_timeout',
|
||||||
|
status: 'active',
|
||||||
|
remote_stage: 'whatsapp_otp_wait',
|
||||||
|
api_waiting_for: 'otp',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
task_id: 'task_timeout',
|
||||||
|
status: 'failed',
|
||||||
|
status_text: '充值失败',
|
||||||
|
remote_stage: 'api_otp_timeout',
|
||||||
|
error_message: '等待 OTP 超过 60 秒,任务已超时',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected url: ${url}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => executor.executePlusCheckoutBilling({
|
||||||
|
plusPaymentMethod: 'gpc-helper',
|
||||||
|
plusCheckoutSource: 'gpc-helper',
|
||||||
|
gopayHelperTaskId: 'task_timeout',
|
||||||
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
|
gopayHelperPin: '654321',
|
||||||
|
gopayHelperApiKey: 'gpc_timeout',
|
||||||
|
gopayHelperOtpChannel: 'whatsapp',
|
||||||
|
gopayHelperLocalSmsHelperEnabled: true,
|
||||||
|
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
|
||||||
|
gopayHelperPhoneNumber: '+8613800138000',
|
||||||
|
}),
|
||||||
|
/GPC_TASK_ENDED::等待 OTP 超过 60 秒,任务已超时/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
|
||||||
|
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_timeout/otp')), false);
|
||||||
|
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_timeout/stop')), false);
|
||||||
|
assert.ok(events.sleeps.includes(3000));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GPC billing helper mode requests newer OTP after invalid OTP error', async () => {
|
||||||
|
const fetchCalls = [];
|
||||||
|
let taskPollCount = 0;
|
||||||
|
let helperCallCount = 0;
|
||||||
|
let otpPostCount = 0;
|
||||||
|
const { events, executor } = createExecutorHarness({
|
||||||
|
frames: [],
|
||||||
|
stateByFrame: {},
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
fetchCalls.push({ url, options });
|
||||||
|
if (url.startsWith('http://127.0.0.1:18767/otp')) {
|
||||||
|
helperCallCount += 1;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ ok: true, otp: helperCallCount === 1 ? '111111' : '222222', message_id: `sms-${helperCallCount}` }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_retry') {
|
||||||
|
taskPollCount += 1;
|
||||||
|
if (taskPollCount === 1) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_retry', status: 'active', remote_stage: 'sms_otp_wait', api_waiting_for: 'otp' }) };
|
||||||
|
}
|
||||||
|
if (taskPollCount === 2) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_retry', status: 'active', remote_stage: 'sms_otp_wait', api_waiting_for: 'otp', last_input_error: 'OTP 校验失败,请重新输入正确的 OTP', otp_invalid_count: 1 }) };
|
||||||
|
}
|
||||||
|
if (taskPollCount === 3) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_retry', status: 'otp_ready', remote_stage: 'otp_ready', api_waiting_for: 'pin' }) };
|
||||||
|
}
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_retry', status: 'completed', remote_stage: 'completed' }) };
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_retry/otp')) {
|
||||||
|
otpPostCount += 1;
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_retry', status: 'active', remote_stage: 'otp_submitted_local' }) };
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_retry/pin')) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_retry', status: 'active', remote_stage: 'payment_processing' }) };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected url: ${url}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executePlusCheckoutBilling({
|
||||||
|
plusPaymentMethod: 'gpc-helper',
|
||||||
|
plusCheckoutSource: 'gpc-helper',
|
||||||
|
gopayHelperTaskId: 'task_retry',
|
||||||
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
|
gopayHelperPin: '654321',
|
||||||
|
gopayHelperApiKey: 'gpc_retry',
|
||||||
|
gopayHelperOtpChannel: 'sms',
|
||||||
|
gopayHelperLocalSmsHelperEnabled: true,
|
||||||
|
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
|
||||||
|
gopayHelperPhoneNumber: '+8613800138000',
|
||||||
|
gopayHelperOrderCreatedAt: 1710000000000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const otpBodies = fetchCalls
|
||||||
|
.filter((call) => call.url.endsWith('/api/gp/tasks/task_retry/otp'))
|
||||||
|
.map((call) => JSON.parse(call.options.body));
|
||||||
|
assert.deepEqual(otpBodies, [{ otp: '111111' }, { otp: '222222' }]);
|
||||||
|
assert.equal(otpPostCount, 2);
|
||||||
|
const helperUrls = fetchCalls.filter((call) => call.url.startsWith('http://127.0.0.1:18767/otp')).map((call) => new URL(call.url));
|
||||||
|
assert.equal(helperUrls.length, 2);
|
||||||
|
assert.equal(helperUrls[0].searchParams.get('after_ms'), '1710000000000');
|
||||||
|
assert.ok(Number(helperUrls[1].searchParams.get('after_ms')) > 1710000000000);
|
||||||
|
assert.equal(events.logs.some((entry) => /OTP 校验失败/.test(entry.message)), true);
|
||||||
|
assert.equal(events.completed[0].step, 7);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GPC billing manual OTP wrong input opens next dialog only after previous one closes', async () => {
|
||||||
|
const fetchCalls = [];
|
||||||
|
let currentState = {
|
||||||
|
plusManualConfirmationPending: true,
|
||||||
|
plusManualConfirmationRequestId: '',
|
||||||
|
};
|
||||||
|
let pendingDialogCount = 0;
|
||||||
|
let pollCount = 0;
|
||||||
|
const { events, executor } = createExecutorHarness({
|
||||||
|
frames: [],
|
||||||
|
stateByFrame: {},
|
||||||
|
getState: async () => currentState,
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
fetchCalls.push({ url, options });
|
||||||
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_manual_retry') {
|
||||||
|
pollCount += 1;
|
||||||
|
if (pollCount === 1) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'active', remote_stage: 'whatsapp_otp_wait', api_waiting_for: 'otp' }) };
|
||||||
|
}
|
||||||
|
if (pollCount === 2) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'active', remote_stage: 'whatsapp_otp_wait', api_waiting_for: 'otp', last_input_error: 'OTP 校验失败,请重新输入正确的 OTP', otp_invalid_count: 1 }) };
|
||||||
|
}
|
||||||
|
if (pollCount === 3) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'otp_ready', remote_stage: 'otp_ready', api_waiting_for: 'pin' }) };
|
||||||
|
}
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'completed', remote_stage: 'completed' }) };
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_manual_retry/otp')) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'active', remote_stage: 'otp_submitted_local' }) };
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_manual_retry/pin')) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'active', remote_stage: 'payment_processing' }) };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected url: ${url}`);
|
||||||
|
},
|
||||||
|
onSetState: async (updates) => {
|
||||||
|
if (updates?.plusManualConfirmationMethod !== 'gopay-otp') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingDialogCount += 1;
|
||||||
|
const resolvedOtp = pendingDialogCount === 1 ? '111111' : '222222';
|
||||||
|
setTimeout(() => {
|
||||||
|
currentState = {
|
||||||
|
plusManualConfirmationPending: false,
|
||||||
|
plusManualConfirmationRequestId: updates.plusManualConfirmationRequestId,
|
||||||
|
gopayHelperResolvedOtp: resolvedOtp,
|
||||||
|
};
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = executor.executePlusCheckoutBilling({
|
||||||
|
plusPaymentMethod: 'gpc-helper',
|
||||||
|
plusCheckoutSource: 'gpc-helper',
|
||||||
|
gopayHelperTaskId: 'task_manual_retry',
|
||||||
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
|
gopayHelperPin: '654321',
|
||||||
|
gopayHelperApiKey: 'gpc_manual_retry',
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
|
const firstPending = events.states.find((state) => state.plusManualConfirmationMethod === 'gopay-otp');
|
||||||
|
assert.ok(firstPending);
|
||||||
|
assert.equal(events.states.filter((state) => state.plusManualConfirmationMethod === 'gopay-otp').length, 1);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 650));
|
||||||
|
const pendingDialogs = events.states.filter((state) => state.plusManualConfirmationMethod === 'gopay-otp');
|
||||||
|
assert.equal(pendingDialogs.length, 2);
|
||||||
|
assert.notEqual(pendingDialogs[1].plusManualConfirmationRequestId, firstPending.plusManualConfirmationRequestId);
|
||||||
|
assert.match(pendingDialogs[1].plusManualConfirmationMessage, /OTP 校验失败/);
|
||||||
|
await run;
|
||||||
|
const otpBodies = fetchCalls
|
||||||
|
.filter((call) => call.url.endsWith('/api/gp/tasks/task_manual_retry/otp'))
|
||||||
|
.map((call) => JSON.parse(call.options.body));
|
||||||
|
assert.deepEqual(otpBodies, [{ otp: '111111' }, { otp: '222222' }]);
|
||||||
|
assert.equal(events.completed[0].step, 7);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GPC billing manual OTP cancel stops task and ends current round', async () => {
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
let currentState = {
|
let currentState = {
|
||||||
plusManualConfirmationPending: true,
|
plusManualConfirmationPending: true,
|
||||||
@@ -1003,26 +1287,11 @@ test('GPC billing retries OTP with compatibility field after HTTP 400', async ()
|
|||||||
getState: async () => currentState,
|
getState: async () => currentState,
|
||||||
fetchImpl: async (url, options = {}) => {
|
fetchImpl: async (url, options = {}) => {
|
||||||
fetchCalls.push({ url, options });
|
fetchCalls.push({ url, options });
|
||||||
if (url.endsWith('/api/gopay/otp') && fetchCalls.filter((call) => call.url.endsWith('/api/gopay/otp')).length === 1) {
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_cancel') {
|
||||||
return {
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_cancel', status: 'active', remote_stage: 'whatsapp_otp_wait', api_waiting_for: 'otp' }) };
|
||||||
ok: false,
|
|
||||||
status: 400,
|
|
||||||
json: async () => ({ error: 'otp field invalid' }),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (url.endsWith('/api/gopay/otp')) {
|
if (url.endsWith('/api/gp/tasks/task_cancel/stop')) {
|
||||||
return {
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_cancel', status: 'discarded', status_text: '已停止' }) };
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
json: async () => ({ challenge_id: 'challenge_retry' }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (url.endsWith('/api/gopay/pin')) {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
json: async () => ({ stage: 'gopay_complete' }),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
throw new Error(`unexpected url: ${url}`);
|
throw new Error(`unexpected url: ${url}`);
|
||||||
},
|
},
|
||||||
@@ -1031,35 +1300,145 @@ test('GPC billing retries OTP with compatibility field after HTTP 400', async ()
|
|||||||
const run = executor.executePlusCheckoutBilling({
|
const run = executor.executePlusCheckoutBilling({
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
plusCheckoutSource: 'gpc-helper',
|
plusCheckoutSource: 'gpc-helper',
|
||||||
gopayHelperReferenceId: 'ref_retry',
|
gopayHelperTaskId: 'task_cancel',
|
||||||
gopayHelperGoPayGuid: 'guid_retry',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
gopayHelperRedirectUrl: 'https://pm-redirects.stripe.com/retry',
|
|
||||||
gopayHelperApiUrl: 'http://localhost:18473/',
|
|
||||||
gopayHelperPin: '654321',
|
gopayHelperPin: '654321',
|
||||||
gopayHelperCardKey: 'card_retry',
|
gopayHelperApiKey: 'gpc_cancel',
|
||||||
gopayHelperFlowId: 'flow_retry',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
const pending = events.states.find((state) => state.plusManualConfirmationMethod === 'gopay-otp');
|
const pending = events.states.find((state) => state.plusManualConfirmationMethod === 'gopay-otp');
|
||||||
|
assert.ok(pending);
|
||||||
currentState = {
|
currentState = {
|
||||||
plusManualConfirmationPending: false,
|
plusManualConfirmationPending: false,
|
||||||
plusManualConfirmationRequestId: pending.plusManualConfirmationRequestId,
|
plusManualConfirmationRequestId: pending.plusManualConfirmationRequestId,
|
||||||
gopayHelperResolvedOtp: '123456',
|
gopayHelperResolvedOtp: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
await run;
|
await assert.rejects(run, /GPC_TASK_ENDED::OTP 输入已取消,已结束当前 GPC 任务。/);
|
||||||
|
const stopCall = fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_cancel/stop'));
|
||||||
assert.equal(fetchCalls.filter((call) => call.url.endsWith('/api/gopay/otp')).length, 2);
|
assert.ok(stopCall);
|
||||||
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
|
assert.equal(stopCall.options.headers['X-API-Key'], 'gpc_cancel');
|
||||||
reference_id: 'ref_retry',
|
assert.equal(events.completed.length, 0);
|
||||||
otp: '123456',
|
});
|
||||||
card_key: 'card_retry',
|
|
||||||
flow_id: 'flow_retry',
|
test('GPC billing PIN failure ends task without retrying PIN', async () => {
|
||||||
gopay_guid: 'guid_retry',
|
const fetchCalls = [];
|
||||||
redirect_url: 'https://pm-redirects.stripe.com/retry',
|
let pollCount = 0;
|
||||||
code: '123456',
|
const { executor } = createExecutorHarness({
|
||||||
});
|
frames: [],
|
||||||
assert.equal(events.logs.some((entry) => /兼容字段重试/.test(entry.message)), true);
|
stateByFrame: {},
|
||||||
assert.equal(events.completed[0].step, 7);
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
fetchCalls.push({ url, options });
|
||||||
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_pin_failed') {
|
||||||
|
pollCount += 1;
|
||||||
|
if (pollCount === 1) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_pin_failed', status: 'otp_ready', status_text: '等待 PIN', remote_stage: 'otp_ready', api_waiting_for: 'pin' }) };
|
||||||
|
}
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_pin_failed', status: 'failed', status_text: '充值失败', remote_stage: 'gopay_validate_pin', failure_stage: 'gopay_validate_pin', failure_detail: 'PIN 校验失败', error_message: 'GoPay PIN validation failed' }) };
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_pin_failed/pin')) {
|
||||||
|
return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_pin_failed', status: 'active', remote_stage: 'pin_submitted_local' }) };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected url: ${url}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => executor.executePlusCheckoutBilling({
|
||||||
|
plusPaymentMethod: 'gpc-helper',
|
||||||
|
plusCheckoutSource: 'gpc-helper',
|
||||||
|
gopayHelperTaskId: 'task_pin_failed',
|
||||||
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
|
gopayHelperPin: '654321',
|
||||||
|
gopayHelperApiKey: 'gpc_pin_failed',
|
||||||
|
}),
|
||||||
|
/GPC_TASK_ENDED::GoPay PIN validation failed(gopay_validate_pin)/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(fetchCalls.filter((call) => call.url.endsWith('/api/gp/tasks/task_pin_failed/pin')).length, 1);
|
||||||
|
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_pin_failed/stop')), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const terminalStatus of ['failed', 'expired', 'discarded']) {
|
||||||
|
test(`GPC billing throws readable error for terminal ${terminalStatus} task`, async () => {
|
||||||
|
const fetchCalls = [];
|
||||||
|
const { executor } = createExecutorHarness({
|
||||||
|
frames: [],
|
||||||
|
stateByFrame: {},
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
fetchCalls.push({ url, options });
|
||||||
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_bad') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({
|
||||||
|
task_id: 'task_bad',
|
||||||
|
status: terminalStatus,
|
||||||
|
status_text: terminalStatus,
|
||||||
|
error_message: '用户可读失败原因',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected url: ${url}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => executor.executePlusCheckoutBilling({
|
||||||
|
plusPaymentMethod: 'gpc-helper',
|
||||||
|
plusCheckoutSource: 'gpc-helper',
|
||||||
|
gopayHelperTaskId: 'task_bad',
|
||||||
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
|
gopayHelperPin: '654321',
|
||||||
|
gopayHelperApiKey: 'gpc_bad',
|
||||||
|
}),
|
||||||
|
/GPC_TASK_ENDED::用户可读失败原因/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_bad/stop')), false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('GPC billing stops task best-effort when flow is interrupted before terminal state', async () => {
|
||||||
|
const fetchCalls = [];
|
||||||
|
const { executor } = createExecutorHarness({
|
||||||
|
frames: [],
|
||||||
|
stateByFrame: {},
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
fetchCalls.push({ url, options });
|
||||||
|
if (url === 'https://gpc.leftcode.xyz/api/gp/tasks/task_stop') {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
json: async () => ({ code: 500, message: 'server_error', data: { detail: '临时失败' } }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.endsWith('/api/gp/tasks/task_stop/stop')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => createGpcTaskResponse({ task_id: 'task_stop', status: 'discarded', status_text: '已停止' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected url: ${url}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => executor.executePlusCheckoutBilling({
|
||||||
|
plusPaymentMethod: 'gpc-helper',
|
||||||
|
plusCheckoutSource: 'gpc-helper',
|
||||||
|
gopayHelperTaskId: 'task_stop',
|
||||||
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
|
gopayHelperPin: '654321',
|
||||||
|
gopayHelperApiKey: 'gpc_stop',
|
||||||
|
}),
|
||||||
|
/临时失败/
|
||||||
|
);
|
||||||
|
|
||||||
|
const stopCall = fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_stop/stop'));
|
||||||
|
assert.ok(stopCall);
|
||||||
|
assert.deepEqual(JSON.parse(stopCall.options.body), {});
|
||||||
|
assert.equal(stopCall.options.headers['X-API-Key'], 'gpc_stop');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ test('GoPay plus checkout create forwards gopay payment method to the checkout c
|
|||||||
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
|
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GPC checkout injects Plus script before reading ChatGPT session token and sends card_key', async () => {
|
test('GPC checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
|
||||||
const events = [];
|
const events = [];
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
const executor = api.createPlusCheckoutCreateExecutor({
|
const executor = api.createPlusCheckoutCreateExecutor({
|
||||||
@@ -129,10 +129,16 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
|
|||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
reference_id: 'ref_123',
|
code: 200,
|
||||||
gopay_guid: 'guid_456',
|
message: 'ok',
|
||||||
next_action: 'enter_otp',
|
data: {
|
||||||
flow_id: 'flow_789',
|
task_id: 'task_123',
|
||||||
|
status: 'active',
|
||||||
|
status_text: '处理中',
|
||||||
|
phone_mode: 'manual',
|
||||||
|
remote_stage: 'checkout_start',
|
||||||
|
otp_channel: 'whatsapp',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -149,12 +155,12 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
|
|||||||
await executor.executePlusCheckoutCreate({
|
await executor.executePlusCheckoutCreate({
|
||||||
email: 'Current.Round+GPC@Example.COM',
|
email: 'Current.Round+GPC@Example.COM',
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
gopayHelperPhoneNumber: '+8613800138000',
|
gopayHelperPhoneNumber: '+8613800138000',
|
||||||
gopayPhone: '',
|
gopayPhone: '',
|
||||||
gopayHelperCountryCode: '+86',
|
gopayHelperCountryCode: '+86',
|
||||||
gopayHelperPin: '123456',
|
gopayHelperPin: '123456',
|
||||||
gopayHelperCardKey: 'card_test_123',
|
gopayHelperApiKey: 'gpc_test_123',
|
||||||
});
|
});
|
||||||
|
|
||||||
const readyIndex = events.findIndex((event) => event.type === 'ready');
|
const readyIndex = events.findIndex((event) => event.type === 'ready');
|
||||||
@@ -167,20 +173,27 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
|
|||||||
includeAccessToken: true,
|
includeAccessToken: true,
|
||||||
});
|
});
|
||||||
assert.equal(fetchCalls.length, 1);
|
assert.equal(fetchCalls.length, 1);
|
||||||
assert.equal(fetchCalls[0].url, 'https://gopay.hwork.pro/api/checkout/start');
|
assert.equal(fetchCalls[0].url, 'https://gpc.leftcode.xyz/api/gp/tasks');
|
||||||
const helperPayload = JSON.parse(fetchCalls[0].options.body);
|
const helperPayload = JSON.parse(fetchCalls[0].options.body);
|
||||||
assert.equal(helperPayload.customer_email, 'current.round+gpc@example.com');
|
assert.deepEqual(helperPayload, {
|
||||||
assert.equal(helperPayload.card_key, 'card_test_123');
|
access_token: 'session-access-token',
|
||||||
assert.deepEqual(helperPayload.gopay_link, {
|
phone_mode: 'manual',
|
||||||
type: 'gopay',
|
|
||||||
country_code: '86',
|
country_code: '86',
|
||||||
phone_number: '13800138000',
|
phone_number: '13800138000',
|
||||||
phone_mode: 'manual',
|
|
||||||
otp_channel: 'whatsapp',
|
otp_channel: 'whatsapp',
|
||||||
});
|
});
|
||||||
|
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_test_123');
|
||||||
|
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
|
||||||
|
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'customer_email'), false);
|
||||||
|
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'checkout_ui_mode'), false);
|
||||||
|
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'gopay_link'), false);
|
||||||
|
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'plan_name'), false);
|
||||||
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.plusCheckoutSource, 'gpc-helper');
|
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.plusCheckoutSource, 'gpc-helper');
|
||||||
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperReferenceId, 'ref_123');
|
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperTaskId, 'task_123');
|
||||||
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperFlowId, 'flow_789');
|
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperTaskStatus, 'active');
|
||||||
|
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperStatusText, '处理中');
|
||||||
|
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperRemoteStage, 'checkout_start');
|
||||||
|
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperReferenceId, '');
|
||||||
assert.ok(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperOrderCreatedAt > 0);
|
assert.ok(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperOrderCreatedAt > 0);
|
||||||
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
|
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
|
||||||
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
|
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
|
||||||
@@ -203,7 +216,11 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => ({ reference_id: 'ref_sms', next_action: 'enter_otp' }),
|
json: async () => ({
|
||||||
|
code: 200,
|
||||||
|
message: 'ok',
|
||||||
|
data: { task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' },
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
registerTab: async () => {},
|
registerTab: async () => {},
|
||||||
@@ -216,25 +233,25 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
|
|||||||
await executor.executePlusCheckoutCreate({
|
await executor.executePlusCheckoutCreate({
|
||||||
email: 'sms@example.com',
|
email: 'sms@example.com',
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
gopayHelperPhoneNumber: '+8613800138000',
|
gopayHelperPhoneNumber: '+8613800138000',
|
||||||
gopayHelperCountryCode: '+86',
|
gopayHelperCountryCode: '+86',
|
||||||
gopayHelperPin: '123456',
|
gopayHelperPin: '123456',
|
||||||
gopayHelperCardKey: 'card_sms',
|
gopayHelperApiKey: 'gpc_sms',
|
||||||
gopayHelperOtpChannel: 'sms',
|
gopayHelperOtpChannel: 'sms',
|
||||||
});
|
});
|
||||||
|
|
||||||
const helperPayload = JSON.parse(fetchCalls[0].options.body);
|
const helperPayload = JSON.parse(fetchCalls[0].options.body);
|
||||||
assert.equal(helperPayload.gopay_link.phone_mode, 'manual');
|
assert.equal(helperPayload.phone_mode, 'manual');
|
||||||
assert.equal(helperPayload.gopay_link.otp_channel, 'sms');
|
assert.equal(helperPayload.otp_channel, 'sms');
|
||||||
|
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_sms');
|
||||||
|
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GPC checkout treats non-zero API amount as non-free-trial and does not create order', async () => {
|
test('GPC checkout surfaces unified queue API errors', async () => {
|
||||||
const markCalls = [];
|
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
const events = [];
|
|
||||||
const executor = api.createPlusCheckoutCreateExecutor({
|
const executor = api.createPlusCheckoutCreateExecutor({
|
||||||
addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }),
|
addLog: async () => {},
|
||||||
chrome: {
|
chrome: {
|
||||||
tabs: {
|
tabs: {
|
||||||
create: async () => {
|
create: async () => {
|
||||||
@@ -243,27 +260,20 @@ test('GPC checkout treats non-zero API amount as non-free-trial and does not cre
|
|||||||
remove: async () => {},
|
remove: async () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
completeStepFromBackground: async () => {
|
completeStepFromBackground: async () => {},
|
||||||
throw new Error('should not complete step 6 for non-free-trial checkout');
|
|
||||||
},
|
|
||||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||||
fetch: async (url, options = {}) => {
|
fetch: async (url, options = {}) => {
|
||||||
fetchCalls.push({ url, options });
|
fetchCalls.push({ url, options });
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: false,
|
||||||
status: 200,
|
status: 400,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
reference_id: 'ref_paid',
|
code: 400,
|
||||||
gopay_guid: 'guid_paid',
|
message: 'invalid_param',
|
||||||
next_action: 'enter_otp',
|
data: { detail: 'access_token 无效' },
|
||||||
checkout: { amount_due: 'Rp 29.000' },
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
markCurrentRegistrationAccountUsed: async (state, options) => {
|
|
||||||
markCalls.push({ state, options });
|
|
||||||
return { updated: true };
|
|
||||||
},
|
|
||||||
registerTab: async () => {},
|
registerTab: async () => {},
|
||||||
sendTabMessageUntilStopped: async () => {},
|
sendTabMessageUntilStopped: async () => {},
|
||||||
setState: async () => {},
|
setState: async () => {},
|
||||||
@@ -275,22 +285,19 @@ test('GPC checkout treats non-zero API amount as non-free-trial and does not cre
|
|||||||
() => executor.executePlusCheckoutCreate({
|
() => executor.executePlusCheckoutCreate({
|
||||||
email: 'paid@example.com',
|
email: 'paid@example.com',
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
chatgptAccessToken: 'state-access-token',
|
chatgptAccessToken: 'state-access-token',
|
||||||
gopayHelperPhoneNumber: '+8613800138000',
|
gopayHelperPhoneNumber: '+8613800138000',
|
||||||
gopayHelperCountryCode: '+86',
|
gopayHelperCountryCode: '+86',
|
||||||
gopayHelperPin: '123456',
|
gopayHelperPin: '123456',
|
||||||
gopayHelperCardKey: 'card_paid_456',
|
gopayHelperApiKey: 'gpc_paid_456',
|
||||||
}),
|
}),
|
||||||
/PLUS_CHECKOUT_NON_FREE_TRIAL::.*余额非 0/
|
/创建 GPC 订单失败:access_token 无效/
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(fetchCalls.length, 1);
|
assert.equal(fetchCalls.length, 1);
|
||||||
assert.equal(JSON.parse(fetchCalls[0].options.body).card_key, 'card_paid_456');
|
assert.equal(Object.prototype.hasOwnProperty.call(JSON.parse(fetchCalls[0].options.body), 'card_key'), false);
|
||||||
assert.equal(markCalls.length, 1);
|
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_paid_456');
|
||||||
assert.equal(markCalls[0].state.email, 'paid@example.com');
|
|
||||||
assert.equal(markCalls[0].options.reason, 'plus-checkout-non-free-trial');
|
|
||||||
assert.equal(events.some((event) => event.type === 'log' && /订单已创建/.test(event.message)), false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GPC checkout does not fall back to browser GoPay phone fields', async () => {
|
test('GPC checkout does not fall back to browser GoPay phone fields', async () => {
|
||||||
@@ -319,7 +326,7 @@ test('GPC checkout does not fall back to browser GoPay phone fields', async () =
|
|||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => executor.executePlusCheckoutCreate({
|
() => executor.executePlusCheckoutCreate({
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
chatgptAccessToken: 'state-access-token',
|
chatgptAccessToken: 'state-access-token',
|
||||||
email: 'helper-phone-test@example.com',
|
email: 'helper-phone-test@example.com',
|
||||||
gopayPhone: '+8613800138000',
|
gopayPhone: '+8613800138000',
|
||||||
@@ -327,13 +334,13 @@ test('GPC checkout does not fall back to browser GoPay phone fields', async () =
|
|||||||
gopayPin: '123456',
|
gopayPin: '123456',
|
||||||
gopayHelperPhoneNumber: '',
|
gopayHelperPhoneNumber: '',
|
||||||
gopayHelperPin: '123456',
|
gopayHelperPin: '123456',
|
||||||
gopayHelperCardKey: 'card_phone_test',
|
gopayHelperApiKey: 'gpc_phone_test',
|
||||||
}),
|
}),
|
||||||
/缺少手机号/
|
/缺少手机号/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GPC checkout rejects missing card key before calling helper API', async () => {
|
test('GPC checkout rejects missing API Key before calling helper API', async () => {
|
||||||
const executor = api.createPlusCheckoutCreateExecutor({
|
const executor = api.createPlusCheckoutCreateExecutor({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
chrome: {
|
chrome: {
|
||||||
@@ -347,7 +354,7 @@ test('GPC checkout rejects missing card key before calling helper API', async ()
|
|||||||
completeStepFromBackground: async () => {},
|
completeStepFromBackground: async () => {},
|
||||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||||
fetch: async () => {
|
fetch: async () => {
|
||||||
throw new Error('should not call helper API without card key');
|
throw new Error('should not call helper API without API Key');
|
||||||
},
|
},
|
||||||
registerTab: async () => {},
|
registerTab: async () => {},
|
||||||
sendTabMessageUntilStopped: async () => {},
|
sendTabMessageUntilStopped: async () => {},
|
||||||
@@ -359,14 +366,14 @@ test('GPC checkout rejects missing card key before calling helper API', async ()
|
|||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => executor.executePlusCheckoutCreate({
|
() => executor.executePlusCheckoutCreate({
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
|
gopayHelperApiUrl: 'https://gpc.leftcode.xyz/',
|
||||||
chatgptAccessToken: 'state-access-token',
|
chatgptAccessToken: 'state-access-token',
|
||||||
email: 'missing-card@example.com',
|
email: 'missing-card@example.com',
|
||||||
gopayHelperPhoneNumber: '+8613800138000',
|
gopayHelperPhoneNumber: '+8613800138000',
|
||||||
gopayHelperCountryCode: '+86',
|
gopayHelperCountryCode: '+86',
|
||||||
gopayHelperPin: '123456',
|
gopayHelperPin: '123456',
|
||||||
gopayHelperCardKey: '',
|
gopayHelperApiKey: '',
|
||||||
}),
|
}),
|
||||||
/缺少卡密/
|
/缺少 API Key/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ return {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidepanel Plus UI shows GPC fields and purchase button only for GPC without API input', () => {
|
test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('normalizePlusPaymentMethod'),
|
extractFunction('normalizePlusPaymentMethod'),
|
||||||
extractFunction('getSelectedPlusPaymentMethod'),
|
extractFunction('getSelectedPlusPaymentMethod'),
|
||||||
@@ -253,7 +253,7 @@ return {
|
|||||||
|
|
||||||
assert.equal(api.rowPayPalAccount.style.display, 'none');
|
assert.equal(api.rowPayPalAccount.style.display, 'none');
|
||||||
assert.equal(api.btnGpcCardKeyPurchase.style.display, '');
|
assert.equal(api.btnGpcCardKeyPurchase.style.display, '');
|
||||||
assert.equal(api.rows.rowGpcHelperApi.style.display, 'none');
|
assert.equal(api.rows.rowGpcHelperApi.style.display, '');
|
||||||
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
|
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
|
||||||
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
|
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
|
||||||
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
|
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
|
||||||
|
|||||||
@@ -135,9 +135,10 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
|
|||||||
assert.match(html, /id="input-gopay-pin"/);
|
assert.match(html, /id="input-gopay-pin"/);
|
||||||
assert.match(html, /<option value="gpc-helper">GPC<\/option>/);
|
assert.match(html, /<option value="gpc-helper">GPC<\/option>/);
|
||||||
assert.match(html, /id="btn-gpc-card-key-purchase"/);
|
assert.match(html, /id="btn-gpc-card-key-purchase"/);
|
||||||
assert.match(html, />购买卡密</);
|
assert.match(html, />获取 API Key</);
|
||||||
assert.doesNotMatch(html, /GPC API/);
|
assert.match(html, /GPC API/);
|
||||||
assert.doesNotMatch(html, /id="input-gpc-helper-api"/);
|
assert.match(html, /id="input-gpc-helper-api"/);
|
||||||
|
assert.match(html, /GPC API Key/);
|
||||||
assert.match(html, /id="input-gpc-helper-card-key"/);
|
assert.match(html, /id="input-gpc-helper-card-key"/);
|
||||||
assert.match(html, /id="btn-gpc-helper-balance"/);
|
assert.match(html, /id="btn-gpc-helper-balance"/);
|
||||||
assert.match(html, /id="input-gpc-helper-phone"/);
|
assert.match(html, /id="input-gpc-helper-phone"/);
|
||||||
|
|||||||
Reference in New Issue
Block a user