Merge branch 'QLHazyCoder:master' into master

This commit is contained in:
jiulongting
2026-05-07 18:45:39 +08:00
committed by GitHub
30 changed files with 2550 additions and 681 deletions
+1 -1
View File
@@ -605,7 +605,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
这一步只负责等待注册完成后的页面状态稳定: 这一步只负责等待注册完成后的页面状态稳定:
- 固定等待 20 秒 - 固定等待 20 秒
-清理 `chatgpt.com / openai.com` 相关 cookies - 默认不清理 `chatgpt.com / openai.com` 相关 cookies;侧栏开启第六步 `清 Cookies` 后才会在等待结束后执行清理
- 等待完成后直接进入后续 OAuth 登录链路 - 等待完成后直接进入后续 OAuth 登录链路
+129 -23
View File
@@ -253,7 +253,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.qlhazycoder.top';
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;
@@ -621,6 +621,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',
@@ -637,6 +638,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,
@@ -647,6 +660,7 @@ const PERSISTED_SETTING_DEFAULTS = {
autoRunDelayEnabled: false, autoRunDelayEnabled: false,
autoRunDelayMinutes: 30, autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
step6CookieCleanupEnabled: false,
phoneVerificationEnabled: false, phoneVerificationEnabled: false,
freePhoneReuseEnabled: true, freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true, freePhoneReuseAutoEnabled: true,
@@ -788,6 +802,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: '',
@@ -2338,12 +2364,17 @@ function normalizePersistentSettingValue(key, value) {
); );
case 'gopayHelperApiUrl': case 'gopayHelperApiUrl':
{ {
const legacyGpcHelperApiUrl = 'https://gpc.leftcode.xyz';
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.qlhazycoder.top');
return self.GoPayUtils?.normalizeGpcHelperBaseUrl const normalizedGpcHelperApiUrl = 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, '');
return normalizedGpcHelperApiUrl === legacyGpcHelperApiUrl
? defaultGpcHelperApiUrl
: normalizedGpcHelperApiUrl;
} }
case 'gopayHelperApiKey':
case 'gopayHelperCardKey': case 'gopayHelperCardKey':
case 'gopayHelperReferenceId': case 'gopayHelperReferenceId':
case 'gopayHelperGoPayGuid': case 'gopayHelperGoPayGuid':
@@ -2351,18 +2382,31 @@ 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':
case 'gopayHelperLocalSmsHelperEnabled': case 'gopayHelperLocalSmsHelperEnabled':
case 'autoRunDelayEnabled': case 'autoRunDelayEnabled':
case 'step6CookieCleanupEnabled':
case 'phoneVerificationEnabled': case 'phoneVerificationEnabled':
case 'freePhoneReuseEnabled': case 'freePhoneReuseEnabled':
case 'freePhoneReuseAutoEnabled': case 'freePhoneReuseAutoEnabled':
@@ -7594,7 +7638,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) {
@@ -7805,6 +7850,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);
@@ -7875,6 +7925,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: '',
@@ -7964,6 +8026,7 @@ function getDownstreamStateResets(step, state = {}) {
plusManualConfirmationTitle: '', plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '', plusManualConfirmationMessage: '',
gopayHelperResolvedOtp: '', gopayHelperResolvedOtp: '',
gopayHelperLastInputError: '',
gopayHelperOtpRequestId: '', gopayHelperOtpRequestId: '',
gopayHelperOtpReferenceId: '', gopayHelperOtpReferenceId: '',
} : {}), } : {}),
@@ -9930,22 +9993,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);
} }
@@ -9953,30 +10036,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 = {};
@@ -9985,20 +10078,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);
@@ -10017,6 +10118,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),
@@ -10041,6 +10144,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR
isAddPhoneAuthFailure, isAddPhoneAuthFailure,
isPhoneSmsPlatformRateLimitFailure, isPhoneSmsPlatformRateLimitFailure,
isPlusCheckoutNonFreeTrialFailure, isPlusCheckoutNonFreeTrialFailure,
isGpcTaskEndedFailure,
isRestartCurrentAttemptError, isRestartCurrentAttemptError,
isStep4Route405RecoveryLimitFailure, isStep4Route405RecoveryLimitFailure,
isSignupUserAlreadyExistsFailure, isSignupUserAlreadyExistsFailure,
@@ -10917,7 +11021,9 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
}); });
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({ const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
addLog, addLog,
chrome,
completeStepFromBackground, completeStepFromBackground,
getErrorMessage,
registrationSuccessWaitMs: STEP6_REGISTRATION_SUCCESS_WAIT_MS, registrationSuccessWaitMs: STEP6_REGISTRATION_SUCCESS_WAIT_MS,
sleepWithStop, sleepWithStop,
}); });
@@ -11081,7 +11187,7 @@ const stepExecutorsByKey = {
'fill-password': (state) => step3Executor.executeStep3(state), 'fill-password': (state) => step3Executor.executeStep3(state),
'fetch-signup-code': (state) => step4Executor.executeStep4(state), 'fetch-signup-code': (state) => step4Executor.executeStep4(state),
'fill-profile': (state) => step5Executor.executeStep5(state), 'fill-profile': (state) => step5Executor.executeStep5(state),
'wait-registration-success': () => step6Executor.executeStep6(), 'wait-registration-success': (state) => step6Executor.executeStep6(state),
'plus-checkout-create': (state) => plusCheckoutCreateExecutor.executePlusCheckoutCreate(state), 'plus-checkout-create': (state) => plusCheckoutCreateExecutor.executePlusCheckoutCreate(state),
'plus-checkout-billing': (state) => plusCheckoutBillingExecutor.executePlusCheckoutBilling(state), 'plus-checkout-billing': (state) => plusCheckoutBillingExecutor.executePlusCheckoutBilling(state),
'gopay-subscription-confirm': (state) => goPayManualConfirmExecutor.executeGoPayManualConfirm(state), 'gopay-subscription-confirm': (state) => goPayManualConfirmExecutor.executeGoPayManualConfirm(state),
@@ -11965,8 +12071,8 @@ async function rerunStep7ForStep8Recovery(options = {}) {
} }
} }
async function executeStep6() { async function executeStep6(state = null) {
return step6Executor.executeStep6(); return step6Executor.executeStep6(state || await getState());
} }
// ============================================================ // ============================================================
+40
View File
@@ -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;
+2 -1
View File
@@ -72,7 +72,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) {
+1 -1
View File
@@ -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({
+84 -173
View File
@@ -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.qlhazycoder.top';
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(`步骤 6GPC 接口返回余额非 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::步骤 6GPC 接口返回余额非 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', country_code: countryCode,
billing_details: { country: 'ID', currency: 'IDR' }, phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
promo_campaign: { otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel),
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,
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
phone_mode: 'manual',
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('步骤 6GPC 订单已创建,准备继续下一步。', 'info'); await addLog(`步骤 6GPC 任务已创建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',
+577 -124
View File
@@ -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.qlhazycoder.top';
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,80 @@
return Number.isFinite(parsed) ? parsed : 0; return Number.isFinite(parsed) ? parsed : 0;
} }
function buildLocalSmsHelperOtpUrl(state = {}, referenceId = '') { function normalizeLocalSmsHelperCountryCode(value = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGoPayCountryCode) {
return rootScope.GoPayUtils.normalizeGoPayCountryCode(value || '+86');
}
const digits = String(value || '+86').replace(/\D/g, '');
return digits ? `+${digits}` : '+86';
}
function normalizeLocalSmsHelperPhoneE164(phone = '', countryCode = '+86') {
const rawPhone = String(phone || '').trim();
if (!rawPhone) {
return '';
}
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const normalizedCountryCode = normalizeLocalSmsHelperCountryCode(countryCode);
const normalizedPhone = rootScope.GoPayUtils?.normalizeGoPayPhone
? rootScope.GoPayUtils.normalizeGoPayPhone(rawPhone)
: rawPhone.replace(/[^\d+]/g, '');
const phoneDigits = normalizedPhone.replace(/\D/g, '');
if (!phoneDigits) {
return '';
}
if (normalizedPhone.startsWith('+')) {
return `+${phoneDigits}`;
}
const countryDigits = normalizedCountryCode.replace(/\D/g, '') || '86';
let nationalNumber = phoneDigits;
if (countryDigits && nationalNumber.startsWith(countryDigits) && nationalNumber.length > countryDigits.length) {
nationalNumber = nationalNumber.slice(countryDigits.length);
}
return `+${countryDigits}${nationalNumber}`;
}
function buildLocalSmsHelperOtpUrl(state = {}, taskId = '', options = {}) {
const baseUrl = normalizeLocalSmsHelperBaseUrl(state?.gopayHelperLocalSmsHelperUrl); const baseUrl = normalizeLocalSmsHelperBaseUrl(state?.gopayHelperLocalSmsHelperUrl);
const url = new URL(`${baseUrl}/otp`); const url = new URL(`${baseUrl}/latest-otp`);
const normalizedReferenceId = String(referenceId || '').trim(); const normalizedTaskId = String(taskId || '').trim();
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim(); const phoneNumber = normalizeLocalSmsHelperPhoneE164(
state?.gopayHelperPhoneNumber,
state?.gopayHelperCountryCode || '+86'
);
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', phoneNumber);
} }
if (orderCreatedAt > 0) { url.searchParams.set('consume', '1');
url.searchParams.set('after_ms', String(orderCreatedAt)); const effectiveAfterMs = Math.max(orderCreatedAt, afterOverrideMs);
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 +544,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 +560,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 +618,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 +675,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('步骤 7GPC 模式缺少 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('步骤 7GPC 模式缺少 task_id,请先执行步骤 6。');
} }
if (!apiUrl) { if (!apiUrl) {
throw new Error('步骤 7GPC 模式缺少 API 地址。'); throw new Error('步骤 7GPC 模式缺少 API 地址。');
} }
if (!cardKey) { if (!apiKey) {
throw new Error('步骤 7GPC 模式缺少卡密。'); throw new Error('步骤 7GPC 模式缺少 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(`步骤 7OTP 验证失败:${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('步骤 7GPC OTP 验证后未返回 challenge_id。');
}
const pin = String(state?.gopayHelperPin || '').trim().replace(/[^\d]/g, '');
if (!pin) { if (!pin) {
throw new Error('步骤 7GPC 模式缺少 PIN 配置。'); if (taskId && apiUrl && apiKey) {
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
}
throw new Error(pinDigits
? '步骤 7GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
: '步骤 7GPC 模式缺少 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 || '';
const waitingFor = task?.api_waiting_for || '';
await addLog(`步骤 7GPC 任务状态:${statusText}${remoteStage ? `${remoteStage}` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info');
await addLog('步骤 7:正在提交 PIN...', 'info'); if (task.status === 'completed') {
const pinInput = { terminalReached = true;
reference_id: referenceId, await setState({
challenge_id: challengeId, plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
gopay_guid: gopayGuid, });
redirect_url: redirectUrl, await addLog('步骤 7:GPC 任务已完成,准备继续下一步。', 'ok');
flow_id: nextFlowId, await completeStepFromBackground(7, {
pin, plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
card_key: cardKey, });
}; return;
const pinResponse = await postGpcJsonWithFallback( }
apiUrl,
'/api/gopay/pin', if (['failed', 'expired', 'discarded'].includes(task.status)) {
buildGpcPinPayload(pinInput), terminalReached = true;
buildGpcPinRetryPayload(pinInput), throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
30000 }
);
if (!pinResponse?.response?.ok) { if (isGpcTaskOtpWait(task)) {
throw new Error(`步骤 7PIN 验证失败:${getGpcResponseErrorDetail(pinResponse?.data, pinResponse?.response?.status || 0)}`); if (isGpcTaskInputDeadlineExpired(task)) {
throw buildGpcInputDeadlineError(task, 'OTP');
}
if (task.last_input_error) {
await addLog(
`步骤 7${task.last_input_error}${task.otp_invalid_count ? `OTP 错误 ${task.otp_invalid_count} 次)` : ''}`,
'warn'
);
}
let otp = '';
try {
otp = await resolveGpcTaskOtp(state, taskId, {
retryCount: otpSubmitCount,
afterMs: otpLastSubmittedAt,
lastInputError: task.last_input_error,
inputDeadlineAt: task.api_input_deadline_at,
});
} catch (error) {
if (/OTP\s*输入(?:已取消|超时)/i.test(error?.message || String(error || ''))) {
throw new Error(`GPC_TASK_ENDED::${error?.message || 'OTP 输入已取消'},已结束当前 GPC 任务。`);
}
throw error;
}
if (!otp) {
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
continue;
}
const normalizedOtp = normalizeSixDigitOtp(otp);
if (!normalizedOtp) {
await addLog('步骤 7:OTP 必须是 6 位数字,等待重新输入。', 'warn');
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
continue;
}
if (task.last_input_error && lastSubmittedOtp && normalizedOtp === lastSubmittedOtp) {
await addLog('步骤 7:本地 OTP Helper 返回的仍是上次已失败 OTP,等待新的验证码。', 'warn');
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
continue;
}
await addLog('步骤 7:正在提交 OTP...', 'info');
try {
await postGpcTaskAction(
apiUrl,
taskId,
'otp',
buildGpcTaskOtpPayload({ otp: normalizedOtp }),
apiKey,
30000
);
} catch (error) {
if (isGpcOtpFormatConflict(error)) {
await addLog(`步骤 7OTP 提交被拒绝:${error?.message || String(error || 'OTP 格式错误')},等待重新输入。`, 'warn');
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
continue;
}
throw error;
}
otpSubmitCount += 1;
otpLastSubmittedAt = Date.now();
lastSubmittedOtp = normalizedOtp;
await addLog('步骤 7:OTP 已提交,继续等待 GPC 任务状态更新。', 'ok');
} else if (isGpcTaskPinWait(task) && !pinSubmitted) {
if (isGpcTaskInputDeadlineExpired(task)) {
throw buildGpcInputDeadlineError(task, 'PIN');
}
await addLog('步骤 7:正在提交 PIN...', 'info');
let pinTask = null;
try {
pinTask = await postGpcTaskAction(
apiUrl,
taskId,
'pin',
buildGpcTaskPinPayload({ pin }),
apiKey,
30000
);
} catch (error) {
throw new Error(`GPC_TASK_ENDED::${error?.message || String(error || 'PIN 提交失败,请重新创建任务。')}`);
}
pinSubmitted = true;
await setState({
gopayHelperPinPayload: pinTask,
});
await addLog('步骤 7:PIN 已提交,继续轮询直到任务完成。', 'ok');
}
await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS);
}
throw new Error('步骤 7GPC 任务轮询超时。');
} catch (error) {
if (!terminalReached) {
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, error?.message || '流程中断');
}
if (isGpcTaskEndedError(error)) {
await clearGpcTaskRuntimeState();
}
throw error;
} }
await setState({
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
gopayHelperPinPayload: pinResponse?.data || null,
});
await addLog('步骤 7:GPC 支付完成,准备继续下一步。', 'ok');
await completeStepFromBackground(7, {
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
});
} }
function resolveMeiguodizhiCountryCode(value = '') { function resolveMeiguodizhiCountryCode(value = '') {
+132 -1
View File
@@ -2,21 +2,152 @@
root.MultiPageBackgroundStep6 = factory(); root.MultiPageBackgroundStep6 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep6Module() { })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep6Module() {
const DEFAULT_REGISTRATION_SUCCESS_WAIT_MS = 20000; const DEFAULT_REGISTRATION_SUCCESS_WAIT_MS = 20000;
const STEP6_COOKIE_CLEAR_DOMAINS = [
'chatgpt.com',
'chat.openai.com',
'openai.com',
'auth.openai.com',
'auth0.openai.com',
'accounts.openai.com',
];
const STEP6_COOKIE_CLEAR_ORIGINS = [
'https://chatgpt.com',
'https://chat.openai.com',
'https://auth.openai.com',
'https://auth0.openai.com',
'https://accounts.openai.com',
'https://openai.com',
];
function normalizeStep6CookieDomain(domain) {
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
}
function shouldClearStep6Cookie(cookie) {
const domain = normalizeStep6CookieDomain(cookie?.domain);
if (!domain) return false;
return STEP6_COOKIE_CLEAR_DOMAINS.some((target) => (
domain === target || domain.endsWith(`.${target}`)
));
}
function buildStep6CookieRemovalUrl(cookie) {
const host = normalizeStep6CookieDomain(cookie?.domain);
const rawPath = String(cookie?.path || '/');
const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
return `https://${host}${path}`;
}
async function collectStep6Cookies(chromeApi) {
if (!chromeApi.cookies?.getAll) {
return [];
}
const stores = chromeApi.cookies.getAllCookieStores
? await chromeApi.cookies.getAllCookieStores()
: [{ id: undefined }];
const cookies = [];
const seen = new Set();
for (const store of stores) {
const storeId = store?.id;
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
for (const cookie of batch || []) {
if (!shouldClearStep6Cookie(cookie)) continue;
const key = [
cookie.storeId || storeId || '',
cookie.domain || '',
cookie.path || '',
cookie.name || '',
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
].join('|');
if (seen.has(key)) continue;
seen.add(key);
cookies.push(cookie);
}
}
return cookies;
}
async function removeStep6Cookie(chromeApi, cookie, getErrorMessage) {
const details = {
url: buildStep6CookieRemovalUrl(cookie),
name: cookie.name,
};
if (cookie.storeId) {
details.storeId = cookie.storeId;
}
if (cookie.partitionKey) {
details.partitionKey = cookie.partitionKey;
}
try {
const result = await chromeApi.cookies.remove(details);
return Boolean(result);
} catch (error) {
console.warn('[MultiPage:step6] remove cookie failed', {
domain: cookie?.domain,
name: cookie?.name,
message: getErrorMessage(error),
});
return false;
}
}
function createStep6Executor(deps = {}) { function createStep6Executor(deps = {}) {
const { const {
addLog = async () => {}, addLog = async () => {},
chrome: chromeApi = globalThis.chrome,
completeStepFromBackground, completeStepFromBackground,
getErrorMessage = (error) => error?.message || String(error || '未知错误'),
registrationSuccessWaitMs = DEFAULT_REGISTRATION_SUCCESS_WAIT_MS, registrationSuccessWaitMs = DEFAULT_REGISTRATION_SUCCESS_WAIT_MS,
sleepWithStop = async (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))), sleepWithStop = async (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))),
} = deps; } = deps;
async function executeStep6() { async function clearCookiesIfEnabled(state = {}) {
if (!state?.step6CookieCleanupEnabled) {
return;
}
if (!chromeApi?.cookies?.getAll || !chromeApi.cookies?.remove) {
await addLog('步骤 6:当前浏览器不支持 cookies API,跳过第六步 Cookies 清理。', 'warn');
return;
}
try {
await addLog('步骤 6:已开启 Cookies 清理,正在清理 ChatGPT / OpenAI cookies...', 'info');
const cookies = await collectStep6Cookies(chromeApi);
let removedCount = 0;
for (const cookie of cookies) {
if (await removeStep6Cookie(chromeApi, cookie, getErrorMessage)) {
removedCount += 1;
}
}
if (chromeApi.browsingData?.removeCookies) {
try {
await chromeApi.browsingData.removeCookies({
since: 0,
origins: STEP6_COOKIE_CLEAR_ORIGINS,
});
} catch (error) {
await addLog(`步骤 6browsingData 补扫 cookies 失败:${getErrorMessage(error)}`, 'warn');
}
}
await addLog(`步骤 6:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok');
} catch (error) {
await addLog(`步骤 6:Cookies 清理失败,已跳过并继续后续流程:${getErrorMessage(error)}`, 'warn');
}
}
async function executeStep6(state = {}) {
const waitMs = Math.max(0, Math.floor(Number(registrationSuccessWaitMs) || 0)); const waitMs = Math.max(0, Math.floor(Number(registrationSuccessWaitMs) || 0));
if (waitMs > 0) { if (waitMs > 0) {
await addLog(`步骤 6:等待 ${Math.round(waitMs / 1000)} 秒,确认注册成功并让页面稳定...`, 'info'); await addLog(`步骤 6:等待 ${Math.round(waitMs / 1000)} 秒,确认注册成功并让页面稳定...`, 'info');
await sleepWithStop(waitMs); await sleepWithStop(waitMs);
} }
await clearCookiesIfEnabled(state);
await addLog('步骤 6:注册成功等待完成,准备继续获取 OAuth 链接并登录。', 'ok'); await addLog('步骤 6:注册成功等待完成,准备继续获取 OAuth 链接并登录。', 'ok');
await completeStepFromBackground(6); await completeStepFromBackground(6);
} }
+3 -1
View File
@@ -28,6 +28,7 @@
| 06 | PayPal 注册与绑卡使用教程 | `PayPal`、注册、绑卡、钱包、身份认证、右上角通知 | `docs/使用教程/分部分/06-PayPal-注册与绑卡使用教程.md` | | 06 | PayPal 注册与绑卡使用教程 | `PayPal`、注册、绑卡、钱包、身份认证、右上角通知 | `docs/使用教程/分部分/06-PayPal-注册与绑卡使用教程.md` |
| 07 | ChatGPT Plus 订阅说明(待人工审核) | `ChatGPT Plus`、订阅说明、支付流程说明、人工审核 | `docs/使用教程/分部分/07-0元试用-ChatGPT-Plus-教程.md` | | 07 | ChatGPT Plus 订阅说明(待人工审核) | `ChatGPT Plus`、订阅说明、支付流程说明、人工审核 | `docs/使用教程/分部分/07-0元试用-ChatGPT-Plus-教程.md` |
| 08 | Clash Verge 非港轮询配置 | `Clash Verge``非港轮询`、扩展脚本、规则模式、系统代理 | `docs/使用教程/分部分/08-Clash-Verge-非港轮询配置.md` | | 08 | Clash Verge 非港轮询配置 | `Clash Verge``非港轮询`、扩展脚本、规则模式、系统代理 | `docs/使用教程/分部分/08-Clash-Verge-非港轮询配置.md` |
| 09 | GPC 卡密与 API 使用说明 | `GPC`、充值卡密、`API` 卡密、`API Key`、自动化扩展、短信 `Helper` | `docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md` |
--- ---
@@ -38,7 +39,7 @@
- `HeroSMS` 手机接码扩展使用教程 - `HeroSMS` 手机接码扩展使用教程
- 节点检测与纯净度检查网站使用教程 - 节点检测与纯净度检查网站使用教程
如果后续收到这些主题的新增内容,优先新开部分,而不是硬塞到现有 8 个文件里。 如果后续收到这些主题的新增内容,优先新开部分,而不是硬塞到现有 9 个文件里。
--- ---
@@ -68,3 +69,4 @@
3. 根据你当前使用的邮箱方案,选择 `03``04``05` 3. 根据你当前使用的邮箱方案,选择 `03``04``05`
4. 如果需要支付和订阅,再看 `06``07` 4. 如果需要支付和订阅,再看 `06``07`
5. 如果需要代理配置,再看 `08` 5. 如果需要代理配置,再看 `08`
6. 如果需要 `GPC` 卡密、`API Key` 或自动化接入,再看 `09`
@@ -78,6 +78,7 @@ AI 维护时默认按下面的范围归类:
| 06 | PayPal 注册与绑卡使用教程 | `PayPal`、注册、绑卡、钱包、身份认证、右上角通知 | `docs/使用教程/分部分/06-PayPal-注册与绑卡使用教程.md` | | 06 | PayPal 注册与绑卡使用教程 | `PayPal`、注册、绑卡、钱包、身份认证、右上角通知 | `docs/使用教程/分部分/06-PayPal-注册与绑卡使用教程.md` |
| 07 | ChatGPT Plus 订阅说明(待人工审核) | `ChatGPT Plus`、订阅说明、支付流程说明、人工审核 | `docs/使用教程/分部分/07-0元试用-ChatGPT-Plus-教程.md` | | 07 | ChatGPT Plus 订阅说明(待人工审核) | `ChatGPT Plus`、订阅说明、支付流程说明、人工审核 | `docs/使用教程/分部分/07-0元试用-ChatGPT-Plus-教程.md` |
| 08 | Clash Verge 非港轮询配置 | `Clash Verge``非港轮询`、扩展脚本、规则模式、系统代理 | `docs/使用教程/分部分/08-Clash-Verge-非港轮询配置.md` | | 08 | Clash Verge 非港轮询配置 | `Clash Verge``非港轮询`、扩展脚本、规则模式、系统代理 | `docs/使用教程/分部分/08-Clash-Verge-非港轮询配置.md` |
| 09 | GPC 卡密与 API 使用说明 | `GPC`、充值卡密、`API` 卡密、`API Key`、自动化扩展、短信 `Helper` | `docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md` |
--- ---
@@ -85,7 +86,7 @@ AI 维护时默认按下面的范围归类:
满足任意一条,就应该新建部分,而不是硬塞到旧文件里: 满足任意一条,就应该新建部分,而不是硬塞到旧文件里:
1. 新内容的主题和现有 8 个部分都不匹配 1. 新内容的主题和现有 9 个部分都不匹配
2. 新内容会让已有文件明显跑题 2. 新内容会让已有文件明显跑题
3. 新内容已经形成独立功能块,后续大概率会继续单独维护 3. 新内容已经形成独立功能块,后续大概率会继续单独维护
4. 新内容虽然在总述里提到过,但当前还没有专门的部分文件 4. 新内容虽然在总述里提到过,但当前还没有专门的部分文件
@@ -0,0 +1,129 @@
# 第九部分:GPC 卡密与 API 使用说明
## 部分信息
- `section_slug`: `gpc-card-code-and-api`
- `适用主题`: `GPC`、充值卡密、`API` 卡密、`API Key`、自动化扩展、短信 `Helper`
- `维护方式`: `直接更新本文件`
## 适用场景
- 需要区分 `GPC` 充值卡密和 `API` 卡密
- 号商需要给指定账号直接开通 `ChatGPT Plus`
- 自动化扩展插件需要接入 `GPC` 模式
- 中转站、账号系统或反代系统需要批量自动注册、补号和订阅
- 需要配合短信 `Helper` 自动补号、自动提交验证码
## 准备内容
- 已获取对应类型的 `GPC` 卡密
- 可以打开 `GPC` 前台
- 如果使用 `API` 模式,需要准备自动化扩展插件
- 如果需要全自动补号,需要准备并配置短信 `Helper`
- 如果还没有 `API` 次数套餐,可以先打开购买页面:[https://pay.ldxp.cn/item/9u90k7](https://pay.ldxp.cn/item/9u90k7)
## 核心区别
| 类型 | 适合对象 | 主要用途 | 使用方式 |
|---|---|---|---|
| 充值卡密 | 号商、人工直冲用户 | 给指定账号直接开通 `ChatGPT Plus` | 在 `GPC` 前台选择充值卡密登录后创建充值任务 |
| `API` 卡密 | 自动化插件、中转站、反代系统 | 批量自动补号、自动注册、自动订阅 | 先兑换成 `API Key`,再填入自动化扩展或系统 |
简单理解:
- 充值卡密 = 给某个账号直接充值 `Plus`
- `API` 卡密 = 兑换接口额度,用于自动化扩展或系统集成
## 核心优势
- 私有协议链路,流程更稳定
- 授权 `CODEX` 后,99% 场景不风控、不弹 `add-phone`
- 无需额外接码平台,配合短信 `Helper` 即可自动处理验证码
- `API` 模式适合自动化补号、批量注册、批量订阅和反代系统接入
- 充值卡密适合单账号直冲,`API` 卡密适合系统化批量接入
## 操作步骤
### 方案一:使用充值卡密直冲
充值卡密主要用于给指定账号直接开通 `ChatGPT Plus`,适合人工或半自动的账号直冲场景。客户只需要提供要充值账号的授权信息,不需要自己准备 `GoPay`
#### 第一步:获取充值卡密
先确认手里拿到的是充值卡密,而不是 `API` 卡密。
#### 第二步:打开 `GPC` 前台
进入 `GPC` 前台页面后,选择充值卡密相关入口。
#### 第三步:选择充值卡密登录
在登录方式中选择 `充值卡密登录`,然后输入充值卡密。
#### 第四步:填写要充值账号的授权信息
按照页面提示输入需要开通 `Plus` 的账号授权信息。
#### 第五步:创建充值任务
确认账号信息无误后创建充值任务,并等待系统执行。
#### 第六步:确认任务结果
任务成功后,会扣减 1 次卡密次数。此时对应账号的 `ChatGPT Plus` 开通流程即完成。
### 方案二:使用 `API` 卡密接入自动化扩展
`API` 卡密本身不是直接拿来请求任务接口的。使用前需要先兑换出 `API Key`,后续由自动化插件或系统使用 `API Key` 调用。
#### 第一步:购买 `API` 次数套餐
打开下面页面购买对应次数套餐:
[https://pay.ldxp.cn/item/9u90k7](https://pay.ldxp.cn/item/9u90k7)
#### 第二步:兑换 `API Key`
购买后打开下面页面兑换:
[https://gpc.qlhazycoder.top/兑换](https://gpc.qlhazycoder.top/%E5%85%91%E6%8D%A2)
输入 `API` 卡密后,会获得一个 `API Key`
#### 第三步:填写到自动化扩展
打开自动化扩展插件后,按下面顺序配置:
1. 启用 `GPC 模式`
2. 填写兑换得到的 `API Key`
3. 填写 `GPC` 相关参数
4. 如需全自动补号,开启并配置短信 `Helper`
5. 保存配置
配置保存后,自动化扩展即可开始执行注册账号和开通流程。
## 常见问题
### 充值卡密可以填到自动化扩展里吗?
不建议。充值卡密主要用于 `GPC` 前台的账号直冲流程;自动化扩展应使用 `API` 卡密兑换出来的 `API Key`
### `API` 卡密可以直接请求任务接口吗?
不可以。`API` 卡密需要先到兑换页面换成 `API Key`,后续请求接口或填写扩展配置时使用的是 `API Key`
### 忘记 `API Key` 怎么办?
可以回到兑换页面,用原来的 `API` 卡密再次查看对应的 `API Key`
### 什么时候会扣减次数?
充值卡密通常在充值任务成功后扣减 1 次。`API` 模式的次数扣减以接口任务执行结果和系统记录为准。
## 注意事项
- 充值卡密和 `API` 卡密用途不同,请不要混用
- `API` 卡密需要先兑换成 `API Key`,再填入自动化扩展
- `API Key` 请妥善保存,不要公开泄露
- 忘记 `API Key` 时,可以回到兑换页面用原 `API` 卡密再次查看
- 全自动补号场景需要同时确认短信 `Helper` 已正确启用
+147 -18
View File
@@ -4,7 +4,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.qlhazycoder.top';
const LEGACY_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 +63,13 @@
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, '');
if (normalized === LEGACY_GPC_HELPER_API_URL) {
return DEFAULT_GPC_HELPER_API_URL;
}
return normalized || DEFAULT_GPC_HELPER_API_URL; return normalized || DEFAULT_GPC_HELPER_API_URL;
} }
@@ -75,12 +82,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 +173,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 +237,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 +258,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 +271,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 +327,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 +351,6 @@
normalizeGoPayPin, normalizeGoPayPin,
normalizeGpcOtpChannel, normalizeGpcOtpChannel,
normalizePlusPaymentMethod, normalizePlusPaymentMethod,
unwrapGpcResponse,
}; };
}); });
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "codex-oauth-automation-extension", "name": "codex-oauth-automation-extension",
"version": "6.7", "version": "7.0",
"version_name": "Ultra6.7", "version_name": "Ultra7.0",
"description": "用于自动执行多步骤 OAuth 注册流程", "description": "用于自动执行多步骤 OAuth 注册流程",
"permissions": [ "permissions": [
"sidePanel", "sidePanel",
+136 -24
View File
@@ -74,6 +74,11 @@ def extract_gopay_otp(text: str, require_keywords: bool = True) -> Optional[str]
return None return None
def normalize_phone_key(value: object) -> str:
digits = re.sub(r"\D+", "", str(value or ""))
return f"+{digits}" if digits else ""
def mac_message_time_to_datetime(value: int | float | None) -> dt.datetime: def mac_message_time_to_datetime(value: int | float | None) -> dt.datetime:
if not value: if not value:
return dt.datetime.now(dt.timezone.utc) return dt.datetime.now(dt.timezone.utc)
@@ -113,24 +118,52 @@ def read_recent_messages(db_path: Path, after_rowid: int = 0, limit: int = 80) -
try: try:
conn = sqlite3.connect(str(copied)) conn = sqlite3.connect(str(copied))
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
rows = conn.execute( params = (max(0, int(after_rowid or 0)), max(1, int(limit or 80)))
""" try:
SELECT rows = conn.execute(
message.ROWID AS rowid, """
message.guid AS guid, SELECT
message.text AS text, message.ROWID AS rowid,
message.date AS date, message.guid AS guid,
message.service AS service, message.text AS text,
handle.id AS handle message.date AS date,
FROM message message.service AS service,
LEFT JOIN handle ON message.handle_id = handle.ROWID message.destination_caller_id AS destination_caller_id,
WHERE message.ROWID > ? message.account AS account,
AND message.text IS NOT NULL handle.id AS handle,
ORDER BY message.ROWID DESC chat.last_addressed_handle AS last_addressed_handle,
LIMIT ? chat.account_login AS account_login
""", FROM message
(max(0, int(after_rowid or 0)), max(1, int(limit or 80))), LEFT JOIN handle ON message.handle_id = handle.ROWID
).fetchall() LEFT JOIN chat_message_join ON chat_message_join.message_id = message.ROWID
LEFT JOIN chat ON chat.ROWID = chat_message_join.chat_id
WHERE message.ROWID > ?
AND message.text IS NOT NULL
ORDER BY message.ROWID DESC
LIMIT ?
""",
params,
).fetchall()
except sqlite3.OperationalError:
rows = conn.execute(
"""
SELECT
message.ROWID AS rowid,
message.guid AS guid,
message.text AS text,
message.date AS date,
message.service AS service,
message.account AS account,
handle.id AS handle
FROM message
LEFT JOIN handle ON message.handle_id = handle.ROWID
WHERE message.ROWID > ?
AND message.text IS NOT NULL
ORDER BY message.ROWID DESC
LIMIT ?
""",
params,
).fetchall()
return [dict(row) for row in rows] return [dict(row) for row in rows]
finally: finally:
try: try:
@@ -140,8 +173,17 @@ def read_recent_messages(db_path: Path, after_rowid: int = 0, limit: int = 80) -
shutil.rmtree(copied.parent, ignore_errors=True) shutil.rmtree(copied.parent, ignore_errors=True)
def get_record_phone(row: dict) -> str:
for key in ("destination_caller_id", "last_addressed_handle", "account_login", "account"):
phone = normalize_phone_key(row.get(key))
if phone:
return phone
return ""
def make_otp_record(row: dict, otp: str) -> dict: def make_otp_record(row: dict, otp: str) -> dict:
received_at = mac_message_time_to_datetime(row.get("date")).isoformat() received_at = mac_message_time_to_datetime(row.get("date")).isoformat()
phone_e164 = get_record_phone(row)
return { return {
"otp": otp, "otp": otp,
"code": otp, "code": otp,
@@ -149,6 +191,8 @@ def make_otp_record(row: dict, otp: str) -> dict:
"rowid": int(row.get("rowid") or 0), "rowid": int(row.get("rowid") or 0),
"sender": str(row.get("handle") or ""), "sender": str(row.get("handle") or ""),
"service": str(row.get("service") or ""), "service": str(row.get("service") or ""),
"account_phone": phone_e164,
"phone_e164": phone_e164,
"received_at": received_at, "received_at": received_at,
"message_text": str(row.get("text") or ""), "message_text": str(row.get("text") or ""),
} }
@@ -189,19 +233,86 @@ def parse_timestamp_ms(value: object) -> int:
return int(numeric) return int(numeric)
def select_otp_record(state: dict, after_ms: int = 0) -> Optional[dict]: def record_matches_phone(record: dict, phone: str = "") -> bool:
wanted = normalize_phone_key(phone)
if not wanted:
return True
return normalize_phone_key(record.get("phone_e164") or record.get("account_phone")) == wanted
def select_otp_record(state: dict, after_ms: int = 0, phone: str = "") -> Optional[dict]:
records = state.get("otps") records = state.get("otps")
if not isinstance(records, list): if not isinstance(records, list):
records = [] records = []
if after_ms > 0: if after_ms > 0:
for record in records: for record in records:
if isinstance(record, dict) and parse_timestamp_ms(record.get("received_at")) >= after_ms: if (
isinstance(record, dict)
and record_matches_phone(record, phone)
and parse_timestamp_ms(record.get("received_at")) >= after_ms
):
return record return record
return None return None
record = state.get("last_otp") or None record = state.get("last_otp") or None
if isinstance(record, dict): if isinstance(record, dict) and record_matches_phone(record, phone):
return record return record
return records[0] if records and isinstance(records[0], dict) else None for record in records:
if isinstance(record, dict) and record_matches_phone(record, phone):
return record
return None
def consume_otp_record(phone: str = "", record: Optional[dict] = None) -> None:
wanted = normalize_phone_key(phone)
consumed_message_id = str((record or {}).get("message_id") or "").strip()
consumed_rowid = int((record or {}).get("rowid") or 0)
def is_consumed_record(item: object) -> bool:
if not isinstance(item, dict):
return False
if consumed_message_id and str(item.get("message_id") or "").strip() == consumed_message_id:
return True
if consumed_rowid and int(item.get("rowid") or 0) == consumed_rowid:
return True
return False
def is_same_record(left: object, right: object) -> bool:
if not isinstance(left, dict) or not isinstance(right, dict):
return False
left_message_id = str(left.get("message_id") or "").strip()
right_message_id = str(right.get("message_id") or "").strip()
if left_message_id and right_message_id and left_message_id == right_message_id:
return True
left_rowid = int(left.get("rowid") or 0)
right_rowid = int(right.get("rowid") or 0)
if left_rowid > 0 and right_rowid > 0 and left_rowid == right_rowid:
return True
return left == right
with STATE_LOCK:
records = STATE.get("otps")
if not isinstance(records, list):
records = []
if consumed_message_id or consumed_rowid:
next_records = [item for item in records if isinstance(item, dict) and not is_consumed_record(item)]
elif wanted:
removed_once = False
next_records = []
for item in records:
if not isinstance(item, dict):
continue
if not removed_once and record_matches_phone(item, wanted):
removed_once = True
continue
next_records.append(item)
else:
next_records = []
STATE["otps"] = next_records
last_otp = STATE.get("last_otp")
if isinstance(last_otp, dict) and not any(is_same_record(last_otp, item) for item in STATE["otps"]):
STATE["last_otp"] = STATE["otps"][0] if STATE["otps"] else None
def scan_once(db_path: Path, require_keywords: bool = True) -> None: def scan_once(db_path: Path, require_keywords: bool = True) -> None:
@@ -263,14 +374,15 @@ class HelperHandler(BaseHTTPRequestHandler):
query = parse_qs(parsed.query) query = parse_qs(parsed.query)
consume = str(query.get("consume", ["0"])[0]).strip().lower() in {"1", "true", "yes"} consume = str(query.get("consume", ["0"])[0]).strip().lower() in {"1", "true", "yes"}
after_ms = parse_timestamp_ms(query.get("after_ms", query.get("after", ["0"]))[0]) after_ms = parse_timestamp_ms(query.get("after_ms", query.get("after", ["0"]))[0])
phone = str((query.get("phone") or query.get("phone_e164") or query.get("phone_number") or [""])[0]).strip()
state = get_state() state = get_state()
record = select_otp_record(state, after_ms=after_ms) record = select_otp_record(state, after_ms=after_ms, phone=phone)
if not record: if not record:
write_json(self, 200, {"ok": True, "otp": "", "code": "", "status": "waiting", "message": "未查询到验证码"}) write_json(self, 200, {"ok": True, "otp": "", "code": "", "status": "waiting", "message": "未查询到验证码"})
return return
payload = {"ok": True, "status": "found", **record} payload = {"ok": True, "status": "found", **record}
if consume: if consume:
update_state(last_otp=None) consume_otp_record(phone=phone, record=record)
write_json(self, 200, payload) write_json(self, 200, payload)
return return
write_json(self, 404, {"ok": False, "error": "not_found"}) write_json(self, 404, {"ok": False, "error": "not_found"})
+51
View File
@@ -1971,6 +1971,16 @@ header {
.data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); } .data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
[data-theme="dark"] .data-select { color-scheme: dark; } [data-theme="dark"] .data-select { color-scheme: dark; }
.data-input[readonly] {
color: var(--text-secondary);
background: var(--bg-surface);
cursor: default;
}
.gpc-helper-api-input {
font-family: 'JetBrains Mono', monospace;
}
.editable-list-picker { .editable-list-picker {
position: relative; position: relative;
flex: 1; flex: 1;
@@ -2114,6 +2124,47 @@ header {
justify-content: flex-end; justify-content: flex-end;
} }
.auto-delay-setting-pair {
flex-wrap: nowrap;
}
#row-auto-delay-settings .setting-group-primary {
flex: 0 0 auto;
min-width: 116px;
}
#row-auto-delay-settings .setting-group-secondary {
margin-left: 0;
min-width: 0;
}
.step6-cookie-cleanup-setting {
gap: 8px;
}
.auto-run-delay-setting {
margin-left: auto !important;
}
.oauth-flow-timeout-setting {
gap: 8px;
min-width: 0;
}
.oauth-flow-timeout-caption {
min-width: 0 !important;
overflow: hidden;
text-overflow: ellipsis;
}
#row-auto-delay-settings .setting-caption {
min-width: auto;
}
.setting-caption-left {
text-align: left;
}
.plus-payment-method-select { .plus-payment-method-select {
flex: 0 0 156px; flex: 0 0 156px;
min-width: 156px; min-width: 156px;
+50 -30
View File
@@ -283,15 +283,23 @@
<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>
<div class="data-inline">
<input type="text" id="input-gpc-helper-api" class="data-input gpc-helper-api-input"
value="https://gpc.qlhazycoder.top/" placeholder="https://gpc.qlhazycoder.top/" autocomplete="off" readonly />
<button id="btn-gpc-helper-convert-api-key" class="btn btn-outline btn-sm data-inline-btn" type="button">转换 API Key</button>
</div>
</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 Keygpc-..." 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 +342,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>
@@ -542,9 +550,19 @@
</div> </div>
</div> </div>
<div class="data-row module-divider-start" id="row-auto-delay-settings"> <div class="data-row module-divider-start" id="row-auto-delay-settings">
<span class="data-label">延迟</span> <span class="data-label">第六步</span>
<div class="data-inline setting-pair"> <div class="data-inline setting-pair auto-delay-setting-pair">
<div class="setting-group setting-group-primary"> <div class="setting-group setting-group-primary step6-cookie-cleanup-setting">
<span class="setting-caption setting-caption-left">清 Cookies</span>
<label class="toggle-switch" for="input-step6-cookie-cleanup-enabled">
<input type="checkbox" id="input-step6-cookie-cleanup-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
</div>
<div class="setting-group setting-group-secondary auto-run-delay-setting">
<span class="setting-caption">延迟</span>
<label class="toggle-switch" for="input-auto-delay-enabled"> <label class="toggle-switch" for="input-auto-delay-enabled">
<input type="checkbox" id="input-auto-delay-enabled" /> <input type="checkbox" id="input-auto-delay-enabled" />
<span class="toggle-switch-track" aria-hidden="true"> <span class="toggle-switch-track" aria-hidden="true">
@@ -557,14 +575,6 @@
<span class="data-unit">分钟</span> <span class="data-unit">分钟</span>
</div> </div>
</div> </div>
<div class="setting-group setting-group-secondary">
<span class="setting-caption">步间间隔</span>
<div class="setting-controls">
<input type="number" id="input-auto-step-delay-seconds" class="data-input auto-delay-input" value=""
min="0" max="600" step="1" title="步间延迟秒数,0 或留空则不延迟" />
<span class="data-unit"></span>
</div>
</div>
</div> </div>
</div> </div>
<div class="data-row"> <div class="data-row">
@@ -578,6 +588,30 @@
</span> </span>
</label> </label>
</div> </div>
<div class="setting-group setting-group-secondary auto-step-delay-setting">
<span class="setting-caption">步间间隔</span>
<div class="setting-controls">
<input type="number" id="input-auto-step-delay-seconds" class="data-input auto-delay-input" value=""
min="0" max="600" step="1" title="步间延迟秒数,0 或留空则不延迟" />
<span class="data-unit"></span>
</div>
</div>
</div>
</div>
<div class="data-row" id="row-oauth-flow-timeout">
<span class="data-label">授权总超时</span>
<div class="data-inline setting-pair">
<div class="setting-group setting-group-primary oauth-flow-timeout-setting">
<label class="toggle-switch" for="input-oauth-flow-timeout-enabled"
title="关闭后不再用 5 分钟总预算重启授权链,但页面、点击、回调等本地等待超时仍会生效">
<input type="checkbox" id="input-oauth-flow-timeout-enabled" checked />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>启用</span>
</label>
<span class="setting-caption oauth-flow-timeout-caption">关闭后只取消 Step 7 后链总预算</span>
</div>
<div class="setting-group setting-group-secondary"> <div class="setting-group setting-group-secondary">
<span class="setting-caption">线程间隔</span> <span class="setting-caption">线程间隔</span>
<div class="setting-controls"> <div class="setting-controls">
@@ -588,20 +622,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="data-row" id="row-oauth-flow-timeout">
<span class="data-label">授权总超时</span>
<div class="data-inline">
<label class="toggle-switch" for="input-oauth-flow-timeout-enabled"
title="关闭后不再用 5 分钟总预算重启授权链,但页面、点击、回调等本地等待超时仍会生效">
<input type="checkbox" id="input-oauth-flow-timeout-enabled" checked />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>启用</span>
</label>
<span class="setting-caption">关闭后只取消 Step 7 后链总预算</span>
</div>
</div>
<div class="data-row module-divider-start" id="row-oauth-display"> <div class="data-row module-divider-start" id="row-oauth-display">
<span class="data-label">OAuth</span> <span class="data-label">OAuth</span>
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span> <span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
+39 -14
View File
@@ -187,6 +187,7 @@ const payPalAccountMenu = document.getElementById('paypal-account-menu');
const btnAddPayPalAccount = document.getElementById('btn-add-paypal-account'); const btnAddPayPalAccount = document.getElementById('btn-add-paypal-account');
const rowGpcHelperApi = document.getElementById('row-gpc-helper-api'); const rowGpcHelperApi = document.getElementById('row-gpc-helper-api');
const inputGpcHelperApi = document.getElementById('input-gpc-helper-api'); const inputGpcHelperApi = document.getElementById('input-gpc-helper-api');
const btnGpcHelperConvertApiKey = document.getElementById('btn-gpc-helper-convert-api-key');
const rowGpcHelperCardKey = document.getElementById('row-gpc-helper-card-key'); const rowGpcHelperCardKey = document.getElementById('row-gpc-helper-card-key');
const inputGpcHelperCardKey = document.getElementById('input-gpc-helper-card-key'); const inputGpcHelperCardKey = document.getElementById('input-gpc-helper-card-key');
const btnToggleGpcHelperCardKey = document.getElementById('btn-toggle-gpc-helper-card-key'); const btnToggleGpcHelperCardKey = document.getElementById('btn-toggle-gpc-helper-card-key');
@@ -375,6 +376,7 @@ const btnCfDomainMode = document.getElementById('btn-cf-domain-mode');
const inputRunCount = document.getElementById('input-run-count'); const inputRunCount = document.getElementById('input-run-count');
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures'); const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
const inputAutoSkipFailuresThreadIntervalMinutes = document.getElementById('input-auto-skip-failures-thread-interval-minutes'); const inputAutoSkipFailuresThreadIntervalMinutes = document.getElementById('input-auto-skip-failures-thread-interval-minutes');
const inputStep6CookieCleanupEnabled = document.getElementById('input-step6-cookie-cleanup-enabled');
const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled'); const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes'); const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds'); const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds');
@@ -507,7 +509,8 @@ 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.qlhazycoder.top';
const GPC_HELPER_PORTAL_URL = 'https://gpc.qlhazycoder.top/';
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';
@@ -2736,7 +2739,7 @@ function applyCloudMailSettingsState(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.qlhazycoder.top';
const { domains, activeDomain } = getCloudflareDomainsFromState(); const { domains, activeDomain } = getCloudflareDomainsFromState();
const selectedCloudflareDomain = normalizeCloudflareDomainValue( const selectedCloudflareDomain = normalizeCloudflareDomainValue(
!cloudflareDomainEditMode ? selectCfDomain.value : activeDomain !cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
@@ -3324,13 +3327,12 @@ function collectSettingsPayload() {
? String(inputGoPayPin.value || '') ? String(inputGoPayPin.value || '')
: String(latestState?.gopayPin || '')), : String(latestState?.gopayPin || '')),
gopayHelperApiUrl: window.GoPayUtils?.normalizeGpcHelperBaseUrl gopayHelperApiUrl: window.GoPayUtils?.normalizeGpcHelperBaseUrl
? window.GoPayUtils.normalizeGpcHelperBaseUrl(typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi ? inputGpcHelperApi.value : (latestState?.gopayHelperApiUrl || defaultGpcHelperApiUrl)) ? window.GoPayUtils.normalizeGpcHelperBaseUrl(defaultGpcHelperApiUrl)
: (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi : String(defaultGpcHelperApiUrl).trim().replace(/\/+$/g, ''),
? String(inputGpcHelperApi.value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, '') gopayHelperApiKey: typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey
: String(latestState?.gopayHelperApiUrl || defaultGpcHelperApiUrl).trim()),
gopayHelperCardKey: 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
@@ -3403,6 +3405,9 @@ function collectSettingsPayload() {
cloudMailDomain: normalizeCloudflareTempEmailDomainValue((typeof inputCloudMailDomain !== 'undefined' && inputCloudMailDomain) ? inputCloudMailDomain.value : ''), cloudMailDomain: normalizeCloudflareTempEmailDomainValue((typeof inputCloudMailDomain !== 'undefined' && inputCloudMailDomain) ? inputCloudMailDomain.value : ''),
autoRunSkipFailures: inputAutoSkipFailures.checked, autoRunSkipFailures: inputAutoSkipFailures.checked,
autoRunFallbackThreadIntervalMinutes: normalizeAutoRunThreadIntervalMinutes(inputAutoSkipFailuresThreadIntervalMinutes.value), autoRunFallbackThreadIntervalMinutes: normalizeAutoRunThreadIntervalMinutes(inputAutoSkipFailuresThreadIntervalMinutes.value),
step6CookieCleanupEnabled: typeof inputStep6CookieCleanupEnabled !== 'undefined' && inputStep6CookieCleanupEnabled
? Boolean(inputStep6CookieCleanupEnabled.checked)
: false,
autoRunDelayEnabled: inputAutoDelayEnabled.checked, autoRunDelayEnabled: inputAutoDelayEnabled.checked,
autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value), autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
autoStepDelaySeconds: normalizeAutoStepDelaySeconds(inputAutoStepDelaySeconds.value), autoStepDelaySeconds: normalizeAutoStepDelaySeconds(inputAutoStepDelaySeconds.value),
@@ -7224,6 +7229,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,
@@ -7473,7 +7479,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 '';
}, },
}, },
@@ -7966,11 +7972,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.qlhazycoder.top';
inputGpcHelperApi.value = state?.gopayHelperApiUrl || defaultGpcHelperApiUrl; inputGpcHelperApi.value = `${defaultGpcHelperApiUrl.replace(/\/+$/g, '')}/`;
} }
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
@@ -8218,6 +8224,9 @@ function applySettingsState(state) {
setCloudflareDomainEditMode(false, { clearInput: true }); setCloudflareDomainEditMode(false, { clearInput: true });
inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures); inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
inputAutoSkipFailuresThreadIntervalMinutes.value = String(normalizeAutoRunThreadIntervalMinutes(state?.autoRunFallbackThreadIntervalMinutes)); inputAutoSkipFailuresThreadIntervalMinutes.value = String(normalizeAutoRunThreadIntervalMinutes(state?.autoRunFallbackThreadIntervalMinutes));
if (typeof inputStep6CookieCleanupEnabled !== 'undefined' && inputStep6CookieCleanupEnabled) {
inputStep6CookieCleanupEnabled.checked = Boolean(state?.step6CookieCleanupEnabled);
}
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled); inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes)); inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(state?.autoStepDelaySeconds); inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(state?.autoStepDelaySeconds);
@@ -11407,14 +11416,18 @@ btnGpcCardKeyPurchase?.addEventListener('click', () => {
openExternalUrl('https://pay.ldxp.cn/shop/gpc'); openExternalUrl('https://pay.ldxp.cn/shop/gpc');
}); });
btnGpcHelperConvertApiKey?.addEventListener('click', () => {
openExternalUrl(GPC_HELPER_PORTAL_URL);
});
btnGpcHelperBalance?.addEventListener('click', async () => { btnGpcHelperBalance?.addEventListener('click', async () => {
try { try {
const response = await chrome.runtime.sendMessage({ const response = await chrome.runtime.sendMessage({
type: 'REFRESH_GPC_CARD_BALANCE', type: 'REFRESH_GPC_CARD_BALANCE',
source: 'sidepanel', source: 'sidepanel',
payload: { payload: {
gopayHelperApiUrl: inputGpcHelperApi?.value || DEFAULT_GPC_HELPER_API_URL, gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL,
gopayHelperCardKey: inputGpcHelperCardKey?.value || '', gopayHelperApiKey: inputGpcHelperCardKey?.value || '',
gopayHelperCountryCode: selectGpcHelperCountryCode?.value || '+86', gopayHelperCountryCode: selectGpcHelperCountryCode?.value || '+86',
reason: 'manual', reason: 'manual',
}, },
@@ -12348,6 +12361,11 @@ inputAutoDelayEnabled.addEventListener('change', () => {
saveSettings({ silent: true }).catch(() => { }); saveSettings({ silent: true }).catch(() => { });
}); });
inputStep6CookieCleanupEnabled?.addEventListener('change', () => {
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
inputAutoDelayMinutes.addEventListener('input', () => { inputAutoDelayMinutes.addEventListener('input', () => {
markSettingsDirty(true); markSettingsDirty(true);
scheduleSettingsAutoSave(); scheduleSettingsAutoSave();
@@ -13483,6 +13501,13 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
inputAutoDelayEnabled.checked = Boolean(message.payload.autoRunDelayEnabled); inputAutoDelayEnabled.checked = Boolean(message.payload.autoRunDelayEnabled);
updateAutoDelayInputState(); updateAutoDelayInputState();
} }
if (
message.payload.step6CookieCleanupEnabled !== undefined
&& typeof inputStep6CookieCleanupEnabled !== 'undefined'
&& inputStep6CookieCleanupEnabled
) {
inputStep6CookieCleanupEnabled.checked = Boolean(message.payload.step6CookieCleanupEnabled);
}
if (message.payload.autoRunDelayMinutes !== undefined) { if (message.payload.autoRunDelayMinutes !== undefined) {
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(message.payload.autoRunDelayMinutes)); inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(message.payload.autoRunDelayMinutes));
} }
+161 -1
View File
@@ -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.qlhazycoder.top',
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.qlhazycoder.top/api/checkout/start '),
'https://gopay.hwork.pro' 'https://gpc.qlhazycoder.top'
); );
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.qlhazycoder.top/api/gp/tasks/task_1/pin '),
'https://gpc.qlhazycoder.top'
);
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/gp/balance '),
'https://gpc.qlhazycoder.top'
);
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiUrl', ''), 'https://gpc.qlhazycoder.top');
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' });
}); });
@@ -32,6 +32,63 @@ test('step 6 waits for registration success and completes from background', asyn
assert.ok(events.logs.some(({ message }) => /等待 20 /.test(message))); assert.ok(events.logs.some(({ message }) => /等待 20 /.test(message)));
}); });
test('step 6 only clears cookies when cleanup switch is enabled', async () => {
const source = fs.readFileSync('background/steps/wait-registration-success.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep6;`)(globalScope);
const events = {
removedCookies: [],
browsingDataCalls: [],
completedSteps: [],
};
const chromeApi = {
cookies: {
getAllCookieStores: async () => [{ id: 'store-a' }],
getAll: async () => [
{ domain: '.chatgpt.com', path: '/auth', name: 'session', storeId: 'store-a' },
{ domain: '.example.com', path: '/', name: 'keep', storeId: 'store-a' },
],
remove: async (details) => {
events.removedCookies.push(details);
return details;
},
},
browsingData: {
removeCookies: async (details) => {
events.browsingDataCalls.push(details);
},
},
};
const executor = api.createStep6Executor({
addLog: async () => {},
chrome: chromeApi,
completeStepFromBackground: async (step) => {
events.completedSteps.push(step);
},
sleepWithStop: async () => {},
});
await executor.executeStep6({ step6CookieCleanupEnabled: false });
assert.deepStrictEqual(events.removedCookies, []);
assert.deepStrictEqual(events.browsingDataCalls, []);
await executor.executeStep6({ step6CookieCleanupEnabled: true });
assert.deepStrictEqual(events.completedSteps, [6, 6]);
assert.deepStrictEqual(events.removedCookies, [
{
url: 'https://chatgpt.com/auth',
name: 'session',
storeId: 'store-a',
},
]);
assert.equal(events.browsingDataCalls.length, 1);
assert.ok(events.browsingDataCalls[0].origins.includes('https://chatgpt.com'));
});
test('step 7 retries up to configured limit and then fails', async () => { test('step 7 retries up to configured limit and then fails', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {}; const globalScope = {};
+57 -55
View File
@@ -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.qlhazycoder.top');
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gopay.hwork.pro'); assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gpc.qlhazycoder.top');
assert.equal( assert.equal(
api.buildGpcHelperApiUrl('', '/api/checkout/start'), api.buildGpcHelperApiUrl('', '/api/checkout/start'),
'https://gopay.hwork.pro/api/checkout/start' 'https://gpc.qlhazycoder.top/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.qlhazycoder.top/api/gp/balance'),
'https://gopay.hwork.pro/api/card/balance?card_key=GPC-1' 'https://gpc.qlhazycoder.top/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.qlhazycoder.top/api/checkout/start'),
'https://gopay.hwork.pro/api/card/balance?card_key=new' 'https://gpc.qlhazycoder.top/api/gp/tasks'
);
assert.equal(
api.buildGpcTaskQueryUrl('https://gpc.qlhazycoder.top/api/gp/tasks/task_old?card_key=old', 'task/1'),
'https://gpc.qlhazycoder.top/api/gp/tasks/task%2F1'
);
assert.equal(
api.buildGpcTaskActionUrl('https://gpc.qlhazycoder.top/api/gp/tasks/task_old/stop', 'task_1', 'pin'),
'https://gpc.qlhazycoder.top/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,状态 activeflow_id flow_1' '余额 12已用 2状态 activeflow_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已经绑了订阅,需要手动解绑'
+120
View File
@@ -79,3 +79,123 @@ print(json.dumps(payload))
none_after_fresh: true, none_after_fresh: true,
}); });
}); });
test('GPC SMS helper selects and consumes cached OTP records by phone', () => {
const code = `
import importlib.util
import json
script_path = ${JSON.stringify(scriptPath)}
spec = importlib.util.spec_from_file_location("gpc_sms_helper_macos", script_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
record_a = {
"otp": "111111",
"code": "111111",
"message_id": "a",
"rowid": 1,
"phone_e164": "+8615808505050",
"account_phone": "+8615808505050",
"received_at": "2026-05-05T00:00:00+00:00",
}
record_b = {
"otp": "222222",
"code": "222222",
"message_id": "b",
"rowid": 2,
"phone_e164": "+8618984829950",
"account_phone": "+8618984829950",
"received_at": "2026-05-05T00:00:10+00:00",
}
module.STATE.update({"last_otp": record_b, "otps": [record_b, record_a]})
selected_a = module.select_otp_record(module.get_state(), phone="+8615808505050")
module.consume_otp_record(phone="+8615808505050", record=selected_a)
state_after = module.get_state()
payload = {
"selected_a": selected_a["otp"],
"selected_b_after": module.select_otp_record(state_after, phone="+8618984829950")["otp"],
"selected_a_after": module.select_otp_record(state_after, phone="+8615808505050") is None,
"global_after": module.select_otp_record(state_after)["otp"],
}
print(json.dumps(payload))
`;
const run = runPython(['-c', code], {
timeout: 3000,
env: { GPC_SMS_HELPER_ALLOW_NON_MAC: '1' },
});
if (!run) {
return;
}
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout.trim()), {
selected_a: '111111',
selected_b_after: '222222',
selected_a_after: true,
global_after: '222222',
});
});
test('GPC SMS helper consume keeps newer same-phone OTP when consuming an older record', () => {
const code = `
import importlib.util
import json
script_path = ${JSON.stringify(scriptPath)}
spec = importlib.util.spec_from_file_location("gpc_sms_helper_macos", script_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
record_old = {
"otp": "111111",
"code": "111111",
"message_id": "old",
"rowid": 1,
"phone_e164": "+8613800138000",
"account_phone": "+8613800138000",
"received_at": "2026-05-05T00:00:00+00:00",
}
record_new = {
"otp": "222222",
"code": "222222",
"message_id": "new",
"rowid": 2,
"phone_e164": "+8613800138000",
"account_phone": "+8613800138000",
"received_at": "2026-05-05T00:00:10+00:00",
}
record_other = {
"otp": "333333",
"code": "333333",
"message_id": "other",
"rowid": 3,
"phone_e164": "+8618984829950",
"account_phone": "+8618984829950",
"received_at": "2026-05-05T00:00:20+00:00",
}
module.STATE.update({"last_otp": record_new, "otps": [record_new, record_old, record_other]})
module.consume_otp_record(phone="+8613800138000", record=record_old)
state_after = module.get_state()
payload = {
"same_phone_after": module.select_otp_record(state_after, phone="+8613800138000")["otp"],
"other_phone_after": module.select_otp_record(state_after, phone="+8618984829950")["otp"],
"records_after": [item["message_id"] for item in state_after.get("otps", [])],
"global_after": module.select_otp_record(state_after)["otp"],
}
print(json.dumps(payload))
`;
const run = runPython(['-c', code], {
timeout: 3000,
env: { GPC_SMS_HELPER_ALLOW_NON_MAC: '1' },
});
if (!run) {
return;
}
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout.trim()), {
same_phone_after: '222222',
other_phone_after: '333333',
records_after: ['new', 'other'],
global_after: '222222',
});
});
+518 -129
View File
@@ -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.qlhazycoder.top/api/gp/tasks/task_123') {
pollCount += 1;
if (pollCount === 1) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ status: 'active', remote_stage: 'whatsapp_otp_wait', api_waiting_for: 'otp' }),
};
}
if (pollCount === 2) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ status: 'otp_ready', status_text: '等待 PIN', remote_stage: 'otp_ready', api_waiting_for: 'pin' }),
};
}
return { return {
ok: true, ok: true,
status: 200, status: 200,
json: async () => ({ reference_id: 'ref_123', challenge_id: 'challenge_456' }), json: async () => createGpcTaskResponse({ status: 'completed', status_text: '充值完成', remote_stage: 'completed' }),
}; };
} }
if (url.endsWith('/api/gopay/pin')) { if (url.endsWith('/api/gp/tasks/task_123/otp')) {
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' }),
};
}
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.qlhazycoder.top/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,53 +893,70 @@ 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.qlhazycoder.top/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: {},
fetchImpl: async (url, options = {}) => { fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options }); fetchCalls.push({ url, options });
if (url.startsWith('http://127.0.0.1:18767/otp')) { if (url.startsWith('http://127.0.0.1:18767/latest-otp')) {
return { return {
ok: true, ok: true,
status: 200, status: 200,
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.qlhazycoder.top/api/gp/tasks/task_sms') {
pollCount += 1;
if (pollCount === 1) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_sms', status: 'active', remote_stage: 'sms_otp_wait', api_waiting_for: 'otp' }),
};
}
if (pollCount === 2) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_sms', status: 'otp_ready', remote_stage: 'otp_ready', api_waiting_for: 'pin' }),
};
}
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: 'completed', remote_stage: 'completed' }),
}; };
} }
if (url.endsWith('/api/gopay/pin')) { if (url.endsWith('/api/gp/tasks/task_sms/otp')) {
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' }),
};
}
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,224 @@ 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.qlhazycoder.top/',
gopayHelperPin: '654321', gopayHelperPin: '654321',
gopayHelperCardKey: 'card_sms', gopayHelperApiKey: 'gpc_sms',
gopayHelperOtpChannel: 'sms',
gopayHelperLocalSmsHelperEnabled: true,
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
gopayHelperCountryCode: '+86',
gopayHelperPhoneNumber: '13800138000',
gopayHelperOrderCreatedAt: 1710000000000,
});
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '654321'), true);
const helperUrl = new URL(fetchCalls[1].url);
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/latest-otp');
assert.equal(helperUrl.searchParams.get('task_id'), 'task_sms');
assert.equal(helperUrl.searchParams.get('reference_id'), 'task_sms');
assert.equal(helperUrl.searchParams.get('phone'), '+8613800138000');
assert.equal(helperUrl.searchParams.get('consume'), '1');
assert.equal(helperUrl.searchParams.get('after_ms'), '1710000000000');
assert.deepEqual(JSON.parse(fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_sms/otp')).options.body), {
otp: '654321',
});
assert.equal(events.completed[0].step, 7);
});
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => {
const fetchCalls = [];
let pollCount = 0;
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.startsWith('http://127.0.0.1:18767/latest-otp')) {
return {
ok: true,
status: 200,
json: async () => ({ ok: true, otp: '765432', message_id: 'wa-1' }),
};
}
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_wa') {
pollCount += 1;
if (pollCount === 1) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_wa', 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_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}`);
},
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperTaskId: 'task_wa',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperPin: '654321',
gopayHelperApiKey: 'gpc_wa',
gopayHelperOtpChannel: 'whatsapp',
gopayHelperLocalSmsHelperEnabled: true,
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
gopayHelperCountryCode: '+86',
gopayHelperPhoneNumber: '18984829950',
});
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '765432'), true);
const helperUrl = new URL(fetchCalls.find((call) => call.url.startsWith('http://127.0.0.1:18767/latest-otp')).url);
assert.equal(helperUrl.searchParams.get('phone'), '+8618984829950');
assert.equal(helperUrl.searchParams.get('consume'), '1');
assert.deepEqual(JSON.parse(fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_wa/otp')).options.body), {
otp: '765432',
});
assert.equal(events.completed[0].step, 7);
});
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/latest-otp')) {
return {
ok: true,
status: 200,
json: async () => ({ ok: true, status: 'waiting', otp: '', message: '未查询到验证码' }),
};
}
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_timeout') {
const queryCount = fetchCalls.filter((call) => call.url === 'https://gpc.qlhazycoder.top/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.qlhazycoder.top/',
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/latest-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.qlhazycoder.top/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.qlhazycoder.top/',
gopayHelperPin: '654321',
gopayHelperApiKey: 'gpc_retry',
gopayHelperOtpChannel: 'sms', gopayHelperOtpChannel: 'sms',
gopayHelperLocalSmsHelperEnabled: true, gopayHelperLocalSmsHelperEnabled: true,
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767', gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
@@ -917,81 +1191,101 @@ test('GPC billing reads OTP from local SMS helper when enabled', async () => {
gopayHelperOrderCreatedAt: 1710000000000, gopayHelperOrderCreatedAt: 1710000000000,
}); });
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false); const otpBodies = fetchCalls
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '654321'), true); .filter((call) => call.url.endsWith('/api/gp/tasks/task_retry/otp'))
const helperUrl = new URL(fetchCalls[0].url); .map((call) => JSON.parse(call.options.body));
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp'); assert.deepEqual(otpBodies, [{ otp: '111111' }, { otp: '222222' }]);
assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_sms'); assert.equal(otpPostCount, 2);
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000'); const helperUrls = fetchCalls.filter((call) => call.url.startsWith('http://127.0.0.1:18767/latest-otp')).map((call) => new URL(call.url));
assert.equal(helperUrl.searchParams.get('after_ms'), '1710000000000'); assert.equal(helperUrls.length, 2);
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), { assert.equal(helperUrls[0].searchParams.get('phone'), '+8613800138000');
reference_id: 'ref_sms', assert.equal(helperUrls[0].searchParams.get('consume'), '1');
otp: '654321', assert.equal(helperUrls[0].searchParams.get('after_ms'), '1710000000000');
card_key: 'card_sms', assert.equal(helperUrls[1].searchParams.get('phone'), '+8613800138000');
}); assert.equal(helperUrls[1].searchParams.get('consume'), '1');
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); assert.equal(events.completed[0].step, 7);
}); });
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => { test('GPC billing manual OTP wrong input opens next dialog only after previous one closes', async () => {
const fetchCalls = []; const fetchCalls = [];
let currentState = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: '',
};
let pendingDialogCount = 0;
let pollCount = 0;
const { events, executor } = createExecutorHarness({ const { events, executor } = createExecutorHarness({
frames: [], frames: [],
stateByFrame: {}, stateByFrame: {},
getState: async () => currentState,
fetchImpl: async (url, options = {}) => { fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options }); fetchCalls.push({ url, options });
if (url.startsWith('http://127.0.0.1:18767/otp')) { if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_manual_retry') {
return { pollCount += 1;
ok: true, if (pollCount === 1) {
status: 200, return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'active', remote_stage: 'whatsapp_otp_wait', api_waiting_for: 'otp' }) };
json: async () => ({ ok: true, otp: '765432', message_id: 'wa-1' }), }
}; 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/gopay/otp')) { if (url.endsWith('/api/gp/tasks/task_manual_retry/otp')) {
return { return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'active', remote_stage: 'otp_submitted_local' }) };
ok: true,
status: 200,
json: async () => ({ reference_id: 'ref_wa', challenge_id: 'challenge_wa' }),
};
} }
if (url.endsWith('/api/gopay/pin')) { if (url.endsWith('/api/gp/tasks/task_manual_retry/pin')) {
return { return { ok: true, status: 200, json: async () => createGpcTaskResponse({ task_id: 'task_manual_retry', status: 'active', remote_stage: 'payment_processing' }) };
ok: true,
status: 200,
json: async () => ({ stage: 'gopay_complete' }),
};
} }
throw new Error(`unexpected url: ${url}`); 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);
},
}); });
await executor.executePlusCheckoutBilling({ const run = executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper', plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper', plusCheckoutSource: 'gpc-helper',
gopayHelperReferenceId: 'ref_wa', gopayHelperTaskId: 'task_manual_retry',
gopayHelperApiUrl: 'https://gopay.hwork.pro/', gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperPin: '654321', gopayHelperPin: '654321',
gopayHelperCardKey: 'card_wa', gopayHelperApiKey: 'gpc_manual_retry',
gopayHelperOtpChannel: 'whatsapp',
gopayHelperLocalSmsHelperEnabled: true,
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
gopayHelperPhoneNumber: '+8613800138000',
}); });
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false); await new Promise((resolve) => setTimeout(resolve, 20));
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '765432'), true); const firstPending = events.states.find((state) => state.plusManualConfirmationMethod === 'gopay-otp');
const helperUrl = new URL(fetchCalls[0].url); assert.ok(firstPending);
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp'); assert.equal(events.states.filter((state) => state.plusManualConfirmationMethod === 'gopay-otp').length, 1);
assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_wa'); await new Promise((resolve) => setTimeout(resolve, 650));
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000'); const pendingDialogs = events.states.filter((state) => state.plusManualConfirmationMethod === 'gopay-otp');
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), { assert.equal(pendingDialogs.length, 2);
reference_id: 'ref_wa', assert.notEqual(pendingDialogs[1].plusManualConfirmationRequestId, firstPending.plusManualConfirmationRequestId);
otp: '765432', assert.match(pendingDialogs[1].plusManualConfirmationMessage, /OTP 校验失败/);
card_key: 'card_wa', 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); assert.equal(events.completed[0].step, 7);
}); });
test('GPC billing retries OTP with compatibility field after HTTP 400', async () => { 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 +1297,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.qlhazycoder.top/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 +1310,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.qlhazycoder.top/',
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.qlhazycoder.top/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.qlhazycoder.top/',
gopayHelperPin: '654321',
gopayHelperApiKey: 'gpc_pin_failed',
}),
/GPC_TASK_ENDED::GoPay PIN validation failedgopay_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.qlhazycoder.top/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.qlhazycoder.top/',
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.qlhazycoder.top/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.qlhazycoder.top/',
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');
}); });
+59 -52
View File
@@ -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.qlhazycoder.top/',
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.qlhazycoder.top/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.qlhazycoder.top/',
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.qlhazycoder.top/',
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.qlhazycoder.top/',
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.qlhazycoder.top/',
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/
); );
}); });
+2 -2
View File
@@ -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, '');
+5 -2
View File
@@ -136,8 +136,11 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
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, />购买卡密</);
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, /id="btn-gpc-helper-convert-api-key"/);
assert.match(html, />转换 API Key</);
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"/);
+11 -10
View File
@@ -46,7 +46,7 @@
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro``v` 版本误显示为比 `Ultra` 更新 - 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro``v` 版本误显示为比 `Ultra` 更新
- 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证码页时继续复用同一手机号续跑短信验证 - 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证码页时继续复用同一手机号续跑短信验证
- 侧栏在接码卡内提供一个独立运行态“注册手机号”输入框,位于接码订单运行状态下方;第 2 步自动拿到号码后立即回填,用户手动接管手机号注册时也可以直接改写这一个运行态槽位。这个输入框表达账号身份,不等同于接码订单;后续自动拉短信仍依赖 `signupPhoneActivation / signupPhoneCompletedActivation` - 侧栏在接码卡内提供一个独立运行态“注册手机号”输入框,位于接码订单运行状态下方;第 2 步自动拿到号码后立即回填,用户手动接管手机号注册时也可以直接改写这一个运行态槽位。这个输入框表达账号身份,不等同于接码订单;后续自动拉短信仍依赖 `signupPhoneActivation / signupPhoneCompletedActivation`
- 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay / GPCPayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN,GPC 展示卡密、专用手机号、OTP 渠道、本地 OTP helper 开关与 URL、PIN;步骤列表切换为 Plus 模式 13 步定义,普通模式的注册成功等待步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步 - 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay / GPCPayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN,GPC 展示只读 API 地址、API Key、专用手机号、OTP 渠道、本地 OTP helper 开关与 URL、PINGPC 的 `购买卡密` 按钮打开卡密购买页,`转换 API Key` 按钮打开 `https://gpc.qlhazycoder.top/`步骤列表切换为 Plus 模式步定义,普通模式的注册成功等待步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作 - 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
### 2.2 Background Service Worker ### 2.2 Background Service Worker
@@ -183,7 +183,7 @@
- Codex2API 配置 - Codex2API 配置
- IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值 - IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值
- Plus 模式开关 `plusModeEnabled` - Plus 模式开关 `plusModeEnabled`
- Plus 支付方式 `plusPaymentMethod`GoPay 配置 `gopayPhone / gopayPin`GPC helper 配置 `gopayHelperCardKey / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin` - Plus 支付方式 `plusPaymentMethod`GoPay 配置 `gopayPhone / gopayPin`GPC helper 配置 `gopayHelperApiUrl / gopayHelperApiKey / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin`;其中 GPC API 地址固定归一为 `https://gpc.qlhazycoder.top`
- PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword` - PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword`
- 邮箱 provider 配置 - 邮箱 provider 配置
- Hotmail 账号池 - Hotmail 账号池
@@ -420,9 +420,10 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
流程: 流程:
1. 固定等待 20 秒 1. 固定等待 20 秒,等待注册成功状态和页面跳转稳定
2. 不删除 ChatGPT / OpenAI cookies 2. 默认不删除 ChatGPT / OpenAI cookies
3. 完成后进入后续登录链路 3. 如果侧栏第六步 `清 Cookies` 开关已开启,则在等待结束后清理 ChatGPT / OpenAI 相关 cookies
4. 完成后进入后续登录链路
### Step 7 ### Step 7
@@ -543,15 +544,15 @@ Codex2API 补充:
## 6.1 Plus 模式链路 ## 6.1 Plus 模式链路
Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完成后,不执行普通模式 Step 6 注册成功等待,也不执行原 Step 8 登录验证码步骤,而是先完成 Plus Checkout 与 PayPal 授权,再复用现有 OAuth 后半段。 Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完成后,不执行普通模式 Step 6 注册成功等待,而是先完成 Plus 支付链路,再复用现有 OAuth 后半段。
Plus 模式可见步骤: Plus 模式可见步骤:
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。 1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR``https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`GoPay 使用 `ID / IDR``https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`GPC helper 模式改为把 accessToken、卡密、手机号、`otp_channel` 等提交给 helper API 创建 GPC 订单 2. 第 6 步 `创建 Plus Checkout`PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR``https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`GoPay 使用 `ID / IDR``https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`GPC helper 模式改为把 accessToken、手机号、国家区号、`otp_channel` 等提交`gopayHelperApiUrl``/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`,旧的 `https://gpc.leftcode.xyz` 配置会兼容归一到新地址
3. 第 7 步 `填写账单并提交订阅``plusPaymentMethod` 选择 PayPal、GoPay 或 GPC。PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是先提交 OTP,再提交 PIN。若侧栏启用本地 OTP helper,后台会轮询 `gopayHelperLocalSmsHelperUrl``/otp` 接口读取当前 OTP 通道的验证码,读取失败再回退到手动 OTP 输入弹窗;仓库内置的 `scripts/gpc_sms_helper_macos.py` 是 macOS Messages 短信读取实现,其他通道需要提供兼容的本地 `/otp` 接口。 3. 第 7 步 `填写账单并提交订阅`PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是轮询 `/api/gp/tasks/{task_id}`,根据远端 `api_waiting_for` 依次向 `/otp``/pin` 提交验证码与 PIN。若侧栏启用本地 OTP helper,后台会轮询 `gopayHelperLocalSmsHelperUrl``/latest-otp?phone=...&consume=1` 接口,按当前 GPC 手机号读取并消费当前 OTP 通道的验证码;未开启本地 helper 时才弹出手动 OTP 输入框。仓库内置的 `scripts/gpc_sms_helper_macos.py` 是 macOS Messages/本地通知兼容读取实现,其他通道需要提供兼容的本地 `/latest-otp` `/otp` 接口。
4. 第 8 步按当前支付方式显示为 `PayPal 登录与授权``GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`PayPal 模式后台优先读取侧边栏当前选中的 PayPal 账号;GoPay 模式读取侧边栏的国家区号、手机号、可选验证码和 PIN,先在 GoPay 页面填写手机号;验证码优先用侧边栏已填值,否则弹出插件输入框让用户手动填写,提交后继续填写 PIN。 4. 仅 PayPal / GoPay 会继续显示第 8 步:该步按当前支付方式显示为 `PayPal 登录与授权``GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`
5. 第 9 步 `订阅回跳确认`:等待 PayPal / GoPay 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。 5. 第 9 步 `订阅回跳确认` 仅在 PayPal / GoPay 模式下等待授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。GPC helper 模式在第 7 步任务完成后会直接进入 Plus 可见第 10 步 OAuth 登录。
6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。 6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。
7. 第 11 步:复用原 Step 8 登录验证码执行器,但状态和日志按 Plus 可见第 11 步记录。 7. 第 11 步:复用原 Step 8 登录验证码执行器,但状态和日志按 Plus 可见第 11 步记录。
8. 第 12 步:复用原 Step 9 OAuth 同意页点击和 localhost callback 捕获执行器,但状态和日志按 Plus 可见第 12 步记录。 8. 第 12 步:复用原 Step 9 OAuth 同意页点击和 localhost callback 捕获执行器,但状态和日志按 Plus 可见第 12 步记录。
+8 -7
View File
@@ -23,6 +23,7 @@
- `README.md`:面向使用者的项目介绍、安装说明、能力清单与操作指引。 - `README.md`:面向使用者的项目介绍、安装说明、能力清单与操作指引。
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前新增 `oauthFlowTimeoutEnabled` 持久化配置,并统一承接 OAuth 授权后链总预算开关与剩余预算计算。 - `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前新增 `oauthFlowTimeoutEnabled` 持久化配置,并统一承接 OAuth 授权后链总预算开关与剩余预算计算。
- `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。 - `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
- `gopay-utils.js`GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。
- `hotmail-utils.js`:Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。 - `hotmail-utils.js`:Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。
- `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。 - `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。
- `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。 - `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。
@@ -62,12 +63,12 @@
## `background/steps/` ## `background/steps/`
- `background/steps/wait-registration-success.js`:步骤 6 实现,负责注册资料提交后等待 20 秒,让注册成功状态和页面跳转稳定。 - `background/steps/wait-registration-success.js`:步骤 6 实现,负责注册资料提交后等待 20 秒,让注册成功状态和页面跳转稳定;默认不清理 cookies,只有侧栏第六步 `清 Cookies` 开关开启时才会在等待结束后清理 ChatGPT / OpenAI 相关 cookies
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。 - `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。
- `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 Plus checkout session打开 `chatgpt.com/checkout/openai_ie/{checkout_session_id}` 短链,并记录 checkout 运行态。 - `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 PayPal / GoPay checkout session或在 GPC 模式下读取 accessToken 并通过 `https://gpc.qlhazycoder.top/api/gp/tasks` 创建队列任务,记录 checkout / GPC task 运行态。
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用手机号验证码共享流程;手机号注册登录后若进入 `add-email`,会先生成/解析邮箱并提交绑定,再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。 - `background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用手机号验证码共享流程;手机号注册登录后若进入 `add-email`,会先生成/解析邮箱并提交绑定,再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。 - `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
- `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 checkout 页面选择 PayPal、生成账单姓名、读取本地地址 seed、触发地址推荐、提交订阅并等待跳转到 PayPal - `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 PayPal / GoPay checkout 页面选择付款方式、填写账单地址、提交订阅并等待跳转;GPC 模式则轮询队列任务,按远端 `api_waiting_for` 提交 OTP / PIN,并在任务失败、过期或取消时清理运行态
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。 - `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。 - `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;当前会根据 `accountIdentifierType / accountIdentifier` 选择邮箱或手机号登录,手机号账号会先探测并切换手机号登录入口;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。 - `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;当前会根据 `accountIdentifierType / accountIdentifier` 选择邮箱或手机号登录,手机号账号会先探测并切换手机号登录入口;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
@@ -125,7 +126,7 @@
## `scripts/` ## `scripts/`
- `scripts/gpc_sms_helper_macos.py`GPC Plus helper 的 macOS 本地短信验证码 helper,读取本机 Messages 数据库中的 GoPay/OpenAI 短信 OTP,并通过 `http://127.0.0.1:18767/otp` 提供给扩展自动轮询 - `scripts/gpc_sms_helper_macos.py`GPC Plus helper 的 macOS 本地短信验证码 helper,读取本机 Messages 数据库中的 GoPay/OpenAI 短信 OTP,并通过 `http://127.0.0.1:18767/latest-otp?phone=...&consume=1` 提供给扩展按手机号自动轮询;`/otp` 仍保留兼容,`consume=1` 会消费本次返回的验证码记录,避免下次重复读取
- `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供账号记录 JSON 快照同步接口;旧的文本追加接口仍保留作兼容。 - `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供账号记录 JSON 快照同步接口;旧的文本追加接口仍保留作兼容。
## `sidepanel/` ## `sidepanel/`
@@ -150,7 +151,7 @@
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱`
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收 额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密 码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;GPC Plus 配置额外提供 OTP 渠道选择、本地 OTP helper 开关helper URLHotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。 钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;GPC Plus 配置额外提供只读 API 地址、API Key、专用手机号、OTP 渠道、本地 OTP helper 开关helper URL 与 PIN,并提供 `购买卡密``转换 API Key` 入口Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。
- `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。 - `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装 - `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启 配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
@@ -204,7 +205,7 @@
- `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成。 - `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成。
- `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。 - `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。
- `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。 - `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。
- `tests/background-step6-retry-limit.test.js`:测试步骤 6 的注册成功等待,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。 - `tests/background-step6-retry-limit.test.js`:测试步骤 6 的注册成功等待、可选 cookies 清理开关,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。
- `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),覆盖邮箱验证码页、真实手机验证码页、`add-email` 绑定邮箱后收码、2925 固定回看窗口与关闭重发间隔。 - `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),覆盖邮箱验证码页、真实手机验证码页、`add-email` 绑定邮箱后收码、2925 固定回看窗口与关闭重发间隔。
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。 - `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。 - `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。
@@ -215,7 +216,7 @@
- `tests/paypal-approve-detection.test.js`:测试 Plus 第 8 步后台执行器对 PayPal 标签页发现、分离式账号/密码页识别、联合登录页识别,以及登录后直接离开 PayPal 页的分支判断。 - `tests/paypal-approve-detection.test.js`:测试 Plus 第 8 步后台执行器对 PayPal 标签页发现、分离式账号/密码页识别、联合登录页识别,以及登录后直接离开 PayPal 页的分支判断。
- `tests/paypal-flow-content.test.js`:测试 PayPal 内容脚本对可见邮箱/密码输入框的识别,并覆盖邮箱页即使已预填相同账号也会先清空再重填后继续下一步。 - `tests/paypal-flow-content.test.js`:测试 PayPal 内容脚本对可见邮箱/密码输入框的识别,并覆盖邮箱页即使已预填相同账号也会先清空再重填后继续下一步。
- `tests/gpc-sms-helper-script.test.js`:测试 GPC macOS 本地短信 helper 在非 macOS 环境下会提示平台与 iPhone 短信转发要求。 - `tests/gpc-sms-helper-script.test.js`:测试 GPC macOS 本地短信 helper 在非 macOS 环境下会提示平台与 iPhone 短信转发要求。
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC helper 的本地 OTP 自动读取分支。 - `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC 队列任务轮询、本地 OTP 自动读取、手动 OTP 输入、PIN 提交与任务终止分支。
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。 - `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。 - `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
- `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。 - `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。