feat(gpc): add auto mode permission checks
This commit is contained in:
+41
-2
@@ -456,6 +456,11 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneMode(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'auto' || normalized === 'builtin' ? 'auto' : 'manual';
|
||||
}
|
||||
|
||||
function normalizeContributionModeSource(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === CONTRIBUTION_SOURCE_SUB2API
|
||||
@@ -634,6 +639,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL,
|
||||
gopayHelperApiKey: '',
|
||||
gopayHelperCardKey: '',
|
||||
gopayHelperPhoneMode: 'manual',
|
||||
gopayHelperPhoneNumber: '',
|
||||
gopayHelperCountryCode: '+86',
|
||||
gopayHelperPin: '',
|
||||
@@ -665,6 +671,9 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
gopayHelperBalancePayload: null,
|
||||
gopayHelperBalanceUpdatedAt: 0,
|
||||
gopayHelperBalanceError: '',
|
||||
gopayHelperRemainingUses: 0,
|
||||
gopayHelperAutoModeEnabled: false,
|
||||
gopayHelperApiKeyStatus: '',
|
||||
autoRunSkipFailures: false,
|
||||
autoRunFallbackThreadIntervalMinutes: 0,
|
||||
oauthFlowTimeoutEnabled: true,
|
||||
@@ -2330,6 +2339,10 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return self.GoPayUtils?.normalizeGoPayPin
|
||||
? self.GoPayUtils.normalizeGoPayPin(value)
|
||||
: String(value || '');
|
||||
case 'gopayHelperPhoneMode':
|
||||
return self.GoPayUtils?.normalizeGpcHelperPhoneMode
|
||||
? self.GoPayUtils.normalizeGpcHelperPhoneMode(value)
|
||||
: (String(value || '').trim().toLowerCase() === 'auto' || String(value || '').trim().toLowerCase() === 'builtin' ? 'auto' : 'manual');
|
||||
case 'gopayHelperPhoneNumber':
|
||||
return self.GoPayUtils?.normalizeGoPayPhone
|
||||
? self.GoPayUtils.normalizeGoPayPhone(value)
|
||||
@@ -2404,6 +2417,7 @@ function normalizePersistentSettingValue(key, value) {
|
||||
case 'gopayHelperFailureDetail':
|
||||
case 'gopayHelperBalance':
|
||||
case 'gopayHelperBalanceError':
|
||||
case 'gopayHelperApiKeyStatus':
|
||||
return String(value || '').trim();
|
||||
case 'gopayHelperBalancePayload':
|
||||
case 'gopayHelperStartPayload':
|
||||
@@ -2412,10 +2426,12 @@ function normalizePersistentSettingValue(key, value) {
|
||||
case 'gopayHelperBalanceUpdatedAt':
|
||||
case 'gopayHelperApiInputWaitSeconds':
|
||||
case 'gopayHelperOtpInvalidCount':
|
||||
case 'gopayHelperRemainingUses':
|
||||
return Math.max(0, Number(value) || 0);
|
||||
case 'autoRunSkipFailures':
|
||||
case 'oauthFlowTimeoutEnabled':
|
||||
case 'gopayHelperLocalSmsHelperEnabled':
|
||||
case 'gopayHelperAutoModeEnabled':
|
||||
case 'autoRunDelayEnabled':
|
||||
case 'step6CookieCleanupEnabled':
|
||||
case 'phoneVerificationEnabled':
|
||||
@@ -9953,12 +9969,27 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) {
|
||||
const balancePayload = self.GoPayUtils?.unwrapGpcResponse
|
||||
? self.GoPayUtils.unwrapGpcResponse(payload)
|
||||
: (payload?.data && typeof payload === 'object' ? payload.data : payload);
|
||||
const balanceData = balancePayload && typeof balancePayload === 'object' && !Array.isArray(balancePayload)
|
||||
? balancePayload
|
||||
: {};
|
||||
const remainingUses = self.GoPayUtils?.getGpcBalanceRemainingUses
|
||||
? self.GoPayUtils.getGpcBalanceRemainingUses(balanceData)
|
||||
: Math.max(0, Number(balanceData.remaining_uses ?? balanceData.remainingUses ?? balanceData.balance ?? balanceData.remaining) || 0);
|
||||
const autoModeEnabled = self.GoPayUtils?.isGpcAutoModeEnabled
|
||||
? self.GoPayUtils.isGpcAutoModeEnabled(balanceData)
|
||||
: Boolean(balanceData.auto_mode_enabled ?? balanceData.autoModeEnabled);
|
||||
const apiKeyStatus = self.GoPayUtils?.getGpcApiKeyStatus
|
||||
? self.GoPayUtils.getGpcApiKeyStatus(balanceData)
|
||||
: String(balanceData.status || balanceData.card_status || balanceData.cardStatus || '').trim();
|
||||
const balanceText = formatGpcApiKeyBalancePayload(payload) || rawText || '未知';
|
||||
const updates = {
|
||||
gopayHelperBalance: balanceText,
|
||||
gopayHelperBalancePayload: balancePayload && typeof balancePayload === 'object' && !Array.isArray(balancePayload) ? balancePayload : { raw: String(balancePayload || '') },
|
||||
gopayHelperBalancePayload: Object.keys(balanceData).length > 0 ? balanceData : { raw: String(balancePayload || '') },
|
||||
gopayHelperBalanceUpdatedAt: Date.now(),
|
||||
gopayHelperBalanceError: '',
|
||||
gopayHelperRemainingUses: Math.max(0, Number(remainingUses) || 0),
|
||||
gopayHelperAutoModeEnabled: Boolean(autoModeEnabled),
|
||||
gopayHelperApiKeyStatus: apiKeyStatus,
|
||||
};
|
||||
const flowId = String(balancePayload?.flow_id || balancePayload?.flowId || '').trim();
|
||||
if (flowId) {
|
||||
@@ -9987,7 +10018,15 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) {
|
||||
: `GPC 余额查询成功:${balanceText}`,
|
||||
'info'
|
||||
);
|
||||
return { balance: balanceText, payload, updatedAt: updates.gopayHelperBalanceUpdatedAt };
|
||||
return {
|
||||
balance: balanceText,
|
||||
payload,
|
||||
data: updates.gopayHelperBalancePayload,
|
||||
remainingUses: updates.gopayHelperRemainingUses,
|
||||
autoModeEnabled: updates.gopayHelperAutoModeEnabled,
|
||||
apiKeyStatus: updates.gopayHelperApiKeyStatus,
|
||||
updatedAt: updates.gopayHelperBalanceUpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
const refreshGpcCardBalance = refreshGpcApiKeyBalance;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
|
||||
function createPlusCheckoutCreateExecutor(deps = {}) {
|
||||
const {
|
||||
@@ -86,6 +88,17 @@
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneMode(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||||
? GPC_HELPER_PHONE_MODE_AUTO
|
||||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function normalizeGpcOtpChannel(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
|
||||
@@ -143,6 +156,17 @@
|
||||
return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks');
|
||||
}
|
||||
|
||||
function buildGpcBalanceUrl(apiUrl = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.buildGpcApiKeyBalanceUrl) {
|
||||
return rootScope.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl);
|
||||
}
|
||||
if (rootScope.GoPayUtils?.buildGpcCardBalanceUrl) {
|
||||
return rootScope.GoPayUtils.buildGpcCardBalanceUrl(apiUrl);
|
||||
}
|
||||
return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance');
|
||||
}
|
||||
|
||||
function unwrapGpcResponse(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.unwrapGpcResponse) {
|
||||
@@ -176,6 +200,55 @@
|
||||
return payload?.data?.detail || payload?.detail || payload?.message || payload?.error || `HTTP ${status || 0}`;
|
||||
}
|
||||
|
||||
function getGpcRemainingUses(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.getGpcBalanceRemainingUses) {
|
||||
return rootScope.GoPayUtils.getGpcBalanceRemainingUses(payload);
|
||||
}
|
||||
const data = unwrapGpcResponse(payload);
|
||||
const numeric = Number(data?.remaining_uses ?? data?.remainingUses ?? data?.balance ?? data?.remaining);
|
||||
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
|
||||
}
|
||||
|
||||
function isGpcAutoModeEnabled(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.isGpcAutoModeEnabled) {
|
||||
return rootScope.GoPayUtils.isGpcAutoModeEnabled(payload);
|
||||
}
|
||||
const data = unwrapGpcResponse(payload);
|
||||
return data?.auto_mode_enabled === true || data?.autoModeEnabled === true;
|
||||
}
|
||||
|
||||
async function assertGpcApiKeyReadyForCreate(state = {}, phoneMode = GPC_HELPER_PHONE_MODE_MANUAL, apiKey = '') {
|
||||
const apiUrl = buildGpcBalanceUrl(state?.gopayHelperApiUrl);
|
||||
if (!apiUrl) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
||||
}
|
||||
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-API-Key': apiKey,
|
||||
},
|
||||
}, 30000);
|
||||
if (!response?.ok || !isGpcUnifiedResponseOk(data)) {
|
||||
const detail = getGpcResponseErrorDetail(data, response?.status || 0);
|
||||
throw new Error(`创建 GPC 订单失败:API Key 校验失败:${detail}`);
|
||||
}
|
||||
const balanceData = unwrapGpcResponse(data);
|
||||
const remainingUses = getGpcRemainingUses(balanceData);
|
||||
const status = String(balanceData?.status || balanceData?.card_status || balanceData?.cardStatus || '').trim().toLowerCase();
|
||||
if (status && status !== 'active') {
|
||||
throw new Error(`创建 GPC 订单失败:API Key 状态不可用(${status})。`);
|
||||
}
|
||||
if (remainingUses !== null && remainingUses <= 0) {
|
||||
throw new Error('创建 GPC 订单失败:API Key 剩余次数不足。');
|
||||
}
|
||||
if (phoneMode === GPC_HELPER_PHONE_MODE_AUTO && !isGpcAutoModeEnabled(balanceData)) {
|
||||
throw new Error('创建 GPC 订单失败:当前 GPC API Key 未开通自动模式。');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
|
||||
const fetcher = typeof fetchImpl === 'function'
|
||||
? fetchImpl
|
||||
@@ -226,25 +299,31 @@
|
||||
if (!apiUrl) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
||||
}
|
||||
const phoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode);
|
||||
const isAutoMode = phoneMode === GPC_HELPER_PHONE_MODE_AUTO;
|
||||
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
|
||||
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
|
||||
const pin = String(state?.gopayHelperPin || '').trim();
|
||||
const apiKey = resolveGpcHelperApiKey(state);
|
||||
if (!phoneNumber) {
|
||||
throw new Error('创建 GPC 订单失败:缺少手机号。');
|
||||
if (!isAutoMode && !phoneNumber) {
|
||||
throw new Error('创建 GPC 订单失败:手动模式缺少手机号。');
|
||||
}
|
||||
if (!pin) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 PIN。');
|
||||
if (!isAutoMode && !pin) {
|
||||
throw new Error('创建 GPC 订单失败:手动模式缺少 PIN。');
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
await assertGpcApiKeyReadyForCreate(state, phoneMode, apiKey);
|
||||
throwIfStopped();
|
||||
const payload = {
|
||||
access_token: token,
|
||||
phone_mode: 'manual',
|
||||
country_code: countryCode,
|
||||
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
|
||||
otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel),
|
||||
phone_mode: phoneMode,
|
||||
};
|
||||
if (!isAutoMode) {
|
||||
payload.country_code = countryCode;
|
||||
payload.phone_number = normalizeHelperPhoneNumber(phoneNumber, countryCode);
|
||||
payload.otp_channel = normalizeGpcOtpChannel(state?.gopayHelperOtpChannel);
|
||||
}
|
||||
|
||||
const orderCreatedAt = Date.now();
|
||||
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
|
||||
@@ -273,6 +352,7 @@
|
||||
remoteStage: String(taskData?.remote_stage || taskData?.remoteStage || '').trim(),
|
||||
orderCreatedAt,
|
||||
responsePayload: taskData && typeof taskData === 'object' && !Array.isArray(taskData) ? taskData : null,
|
||||
phoneMode: normalizeGpcHelperPhoneMode(taskData?.phone_mode || taskData?.phoneMode || phoneMode),
|
||||
country: 'ID',
|
||||
currency: 'IDR',
|
||||
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
@@ -308,6 +388,7 @@
|
||||
gopayHelperTaskStatus: result.taskStatus,
|
||||
gopayHelperStatusText: result.statusText,
|
||||
gopayHelperRemoteStage: result.remoteStage,
|
||||
gopayHelperPhoneMode: result.phoneMode || normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode),
|
||||
gopayHelperTaskPayload: result.responsePayload,
|
||||
gopayHelperReferenceId: '',
|
||||
gopayHelperGoPayGuid: '',
|
||||
@@ -318,7 +399,7 @@
|
||||
gopayHelperStartPayload: null,
|
||||
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
|
||||
});
|
||||
await addLog(`步骤 6:GPC 任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
|
||||
await addLog(`步骤 6:GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
|
||||
await completeStepFromBackground(6, {
|
||||
plusCheckoutCountry: result.country || 'ID',
|
||||
plusCheckoutCurrency: result.currency || 'IDR',
|
||||
|
||||
@@ -11,7 +11,27 @@
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const GPC_TASK_POLL_INTERVAL_MS = 3000;
|
||||
const GPC_REMOTE_STAGE_LABELS = {
|
||||
auto_otp_wait: '等待自动 OTP',
|
||||
checkout_order_start: '创建订单',
|
||||
checkout_start: '创建订单',
|
||||
completed: '充值完成',
|
||||
gopay_validate_pin: '校验 PIN',
|
||||
otp_ready: '等待 PIN',
|
||||
otp_submitted_local: 'OTP 已提交',
|
||||
payment_processing: '支付处理中',
|
||||
pin_submitted_local: 'PIN 已提交',
|
||||
sms_otp_wait: '等待短信 OTP',
|
||||
whatsapp_otp_wait: '等待 WhatsApp OTP',
|
||||
};
|
||||
const GPC_WAITING_FOR_LABELS = {
|
||||
auto_otp: '自动 OTP',
|
||||
otp: 'OTP',
|
||||
pin: 'PIN',
|
||||
};
|
||||
const PAYMENT_METHOD_CONFIGS = {
|
||||
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
||||
id: PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
@@ -112,6 +132,56 @@
|
||||
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneMode(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||||
? GPC_HELPER_PHONE_MODE_AUTO
|
||||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function formatGpcRemoteStageLabel(stage = '') {
|
||||
const normalized = String(stage || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
return GPC_REMOTE_STAGE_LABELS[normalized] || normalized;
|
||||
}
|
||||
|
||||
function formatGpcWaitingForLabel(waitingFor = '') {
|
||||
const normalized = String(waitingFor || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
return GPC_WAITING_FOR_LABELS[normalized] || normalized.toUpperCase();
|
||||
}
|
||||
|
||||
function formatGpcTaskStatusLog(task = {}) {
|
||||
const statusText = String(task?.status_text || task?.statusText || '').trim();
|
||||
const status = String(task?.status || '').trim();
|
||||
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
|
||||
const stageText = formatGpcRemoteStageLabel(remoteStage);
|
||||
const waitingForText = formatGpcWaitingForLabel(task?.api_waiting_for || task?.apiWaitingFor || '');
|
||||
const mainText = stageText || statusText || status || '处理中';
|
||||
const parts = [`步骤 7:GPC 任务状态:${mainText}`];
|
||||
if (waitingForText && !mainText.includes(waitingForText)) {
|
||||
parts.push(`,等待 ${waitingForText}`);
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function getGpcHelperPhoneMode(state = {}, task = null) {
|
||||
return normalizeGpcHelperPhoneMode(
|
||||
task?.phone_mode
|
||||
|| task?.phoneMode
|
||||
|| state?.gopayHelperPhoneMode
|
||||
|| state?.phoneMode
|
||||
);
|
||||
}
|
||||
|
||||
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
|
||||
}
|
||||
@@ -299,7 +369,7 @@
|
||||
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.phone_mode = normalizeGpcHelperPhoneMode(task.phone_mode || task.phoneMode || '');
|
||||
task.remote_stage = String(task.remote_stage || task.remoteStage || '').trim().toLowerCase();
|
||||
task.api_waiting_for = String(task.api_waiting_for || task.apiWaitingFor || '').trim().toLowerCase();
|
||||
task.api_input_deadline_at = String(task.api_input_deadline_at || task.apiInputDeadlineAt || '').trim();
|
||||
@@ -318,6 +388,7 @@
|
||||
gopayHelperTaskId: task.task_id,
|
||||
gopayHelperTaskStatus: task.status,
|
||||
gopayHelperStatusText: task.status_text,
|
||||
gopayHelperPhoneMode: task.phone_mode,
|
||||
gopayHelperRemoteStage: task.remote_stage,
|
||||
gopayHelperApiWaitingFor: task.api_waiting_for,
|
||||
gopayHelperApiInputDeadlineAt: task.api_input_deadline_at,
|
||||
@@ -675,12 +746,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
function isGpcTaskOtpWait(task = {}) {
|
||||
return task?.phone_mode === 'manual' && task?.api_waiting_for === 'otp';
|
||||
function isGpcTaskManualMode(task = {}, state = {}) {
|
||||
return getGpcHelperPhoneMode(state, task) === GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function isGpcTaskPinWait(task = {}) {
|
||||
return task?.phone_mode === 'manual'
|
||||
function isGpcTaskOtpWait(task = {}, state = {}) {
|
||||
return isGpcTaskManualMode(task, state) && task?.api_waiting_for === 'otp';
|
||||
}
|
||||
|
||||
function isGpcTaskPinWait(task = {}, state = {}) {
|
||||
return isGpcTaskManualMode(task, state)
|
||||
&& (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready');
|
||||
}
|
||||
|
||||
@@ -840,27 +915,25 @@
|
||||
throw new Error('步骤 7:GPC 模式缺少 API Key。');
|
||||
}
|
||||
|
||||
const configuredPhoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode || GPC_HELPER_PHONE_MODE_MANUAL);
|
||||
const rawPin = String(state?.gopayHelperPin || '').trim();
|
||||
const pinDigits = rawPin.replace(/[^\d]/g, '');
|
||||
const pin = normalizeSixDigitPin(rawPin);
|
||||
if (!pin) {
|
||||
if (configuredPhoneMode === GPC_HELPER_PHONE_MODE_MANUAL && !pin) {
|
||||
if (taskId && apiUrl && apiKey) {
|
||||
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
|
||||
}
|
||||
throw new Error(pinDigits
|
||||
? '步骤 7:GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
|
||||
: '步骤 7:GPC 模式缺少 PIN 配置。');
|
||||
: '步骤 7:GPC 手动模式缺少 PIN 配置。');
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId})...`, 'info');
|
||||
await addLog(`步骤 7:GPC ${configuredPhoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式开始轮询任务(task_id: ${taskId})...`, 'info');
|
||||
try {
|
||||
while (Date.now() <= deadline) {
|
||||
throwIfStopped();
|
||||
const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey);
|
||||
const statusText = task?.status_text || task?.status || '处理中';
|
||||
const remoteStage = task?.remote_stage || '';
|
||||
const waitingFor = task?.api_waiting_for || '';
|
||||
await addLog(`步骤 7:GPC 任务状态:${statusText}${remoteStage ? `(${remoteStage})` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info');
|
||||
await addLog(formatGpcTaskStatusLog(task), 'info');
|
||||
|
||||
if (task.status === 'completed') {
|
||||
terminalReached = true;
|
||||
@@ -879,7 +952,7 @@
|
||||
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
|
||||
}
|
||||
|
||||
if (isGpcTaskOtpWait(task)) {
|
||||
if (isGpcTaskOtpWait(task, state)) {
|
||||
if (isGpcTaskInputDeadlineExpired(task)) {
|
||||
throw buildGpcInputDeadlineError(task, 'OTP');
|
||||
}
|
||||
@@ -940,7 +1013,7 @@
|
||||
otpLastSubmittedAt = Date.now();
|
||||
lastSubmittedOtp = normalizedOtp;
|
||||
await addLog('步骤 7:OTP 已提交,继续等待 GPC 任务状态更新。', 'ok');
|
||||
} else if (isGpcTaskPinWait(task) && !pinSubmitted) {
|
||||
} else if (isGpcTaskPinWait(task, state) && !pinSubmitted) {
|
||||
if (isGpcTaskInputDeadlineExpired(task)) {
|
||||
throw buildGpcInputDeadlineError(task, 'PIN');
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
|
||||
{ id: 6, order: 60, key: 'plus-checkout-create', title: '创建 GPC 订单' },
|
||||
{ id: 7, order: 70, key: 'plus-checkout-billing', title: 'GPC OTP/PIN 验证' },
|
||||
{ id: 7, order: 70, key: 'plus-checkout-billing', title: '等待 GPC 任务完成' },
|
||||
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
|
||||
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' },
|
||||
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' },
|
||||
|
||||
+98
-1
@@ -5,6 +5,8 @@
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const ALLOWED_GPC_HELPER_REMOTE_HOST = 'gpc.qlhazycoder.top';
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
@@ -47,6 +49,85 @@
|
||||
return String(value || '').trim().replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneMode(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||||
? GPC_HELPER_PHONE_MODE_AUTO
|
||||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function normalizeGpcRemainingUses(value) {
|
||||
if (value === undefined || value === null || String(value).trim() === '') {
|
||||
return null;
|
||||
}
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
|
||||
}
|
||||
|
||||
function unwrapGpcBalancePayload(payload = {}) {
|
||||
const data = unwrapGpcResponse(payload);
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
const hasBalanceFields = [
|
||||
'remaining_uses',
|
||||
'remainingUses',
|
||||
'balance',
|
||||
'remaining',
|
||||
'uses',
|
||||
'available_uses',
|
||||
'availableUses',
|
||||
'auto_mode_enabled',
|
||||
'autoModeEnabled',
|
||||
'auto_enabled',
|
||||
'autoEnabled',
|
||||
'status',
|
||||
'card_status',
|
||||
'cardStatus',
|
||||
].some((key) => Object.prototype.hasOwnProperty.call(data, key));
|
||||
if (!hasBalanceFields && data.data && typeof data.data === 'object' && !Array.isArray(data.data)) {
|
||||
return data.data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function getGpcBalanceRemainingUses(payload = {}) {
|
||||
const data = unwrapGpcBalancePayload(payload);
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return normalizeGpcRemainingUses(
|
||||
data.remaining_uses
|
||||
?? data.remainingUses
|
||||
?? data.balance
|
||||
?? data.remaining
|
||||
?? data.uses
|
||||
?? data.available_uses
|
||||
?? data.availableUses
|
||||
);
|
||||
}
|
||||
|
||||
function isGpcAutoModeEnabled(payload = {}) {
|
||||
const data = unwrapGpcBalancePayload(payload);
|
||||
if (!data || typeof data !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const raw = data.auto_mode_enabled ?? data.autoModeEnabled ?? data.auto_enabled ?? data.autoEnabled;
|
||||
if (typeof raw === 'boolean') {
|
||||
return raw;
|
||||
}
|
||||
const normalized = String(raw ?? '').trim().toLowerCase();
|
||||
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'enabled';
|
||||
}
|
||||
|
||||
function getGpcApiKeyStatus(payload = {}) {
|
||||
const data = unwrapGpcBalancePayload(payload);
|
||||
if (!data || typeof data !== 'object') {
|
||||
return '';
|
||||
}
|
||||
return String(data.status || data.card_status || data.cardStatus || '').trim();
|
||||
}
|
||||
|
||||
function normalizeGpcOtpChannel(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'sms') {
|
||||
@@ -291,7 +372,7 @@
|
||||
}
|
||||
|
||||
function formatGpcBalancePayload(payload = {}) {
|
||||
const data = unwrapGpcResponse(payload);
|
||||
const data = unwrapGpcBalancePayload(payload);
|
||||
if (!data || typeof data !== 'object') {
|
||||
return '';
|
||||
}
|
||||
@@ -308,6 +389,11 @@
|
||||
const totalUses = data.total_uses ?? data.totalUses;
|
||||
const usedUses = data.used_uses ?? data.usedUses;
|
||||
const status = String(data.status || data.card_status || data.cardStatus || '').trim();
|
||||
const hasAutoModeField = data.auto_mode_enabled !== undefined
|
||||
|| data.autoModeEnabled !== undefined
|
||||
|| data.auto_enabled !== undefined
|
||||
|| data.autoEnabled !== undefined;
|
||||
const autoModeEnabled = isGpcAutoModeEnabled(data);
|
||||
const flowId = String(data.flow_id || data.flowId || '').trim();
|
||||
const parts = [];
|
||||
if (firstValue !== undefined) {
|
||||
@@ -321,6 +407,9 @@
|
||||
if (status) {
|
||||
parts.push(`状态 ${status}`);
|
||||
}
|
||||
if (hasAutoModeField) {
|
||||
parts.push(`自动模式 ${autoModeEnabled ? '已开通' : '未开通'}`);
|
||||
}
|
||||
if (flowId) {
|
||||
parts.push(`flow_id ${flowId}`);
|
||||
}
|
||||
@@ -330,6 +419,8 @@
|
||||
return {
|
||||
DEFAULT_GOPAY_COUNTRY_CODE,
|
||||
DEFAULT_GPC_HELPER_API_URL,
|
||||
GPC_HELPER_PHONE_MODE_AUTO,
|
||||
GPC_HELPER_PHONE_MODE_MANUAL,
|
||||
PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
PLUS_PAYMENT_METHOD_GOPAY,
|
||||
PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
@@ -348,8 +439,13 @@
|
||||
buildGpcTaskQueryUrl,
|
||||
extractGpcResponseErrorDetail,
|
||||
formatGpcBalancePayload,
|
||||
getGpcApiKeyStatus,
|
||||
getGpcBalanceRemainingUses,
|
||||
isGpcUnifiedResponseOk,
|
||||
isGpcAutoModeEnabled,
|
||||
normalizeGpcHelperBaseUrl,
|
||||
normalizeGpcHelperPhoneMode,
|
||||
normalizeGpcRemainingUses,
|
||||
normalizeGpcTaskId,
|
||||
normalizeGoPayCountryCode,
|
||||
normalizeGoPayPhone,
|
||||
@@ -358,6 +454,7 @@
|
||||
normalizeGoPayPin,
|
||||
normalizeGpcOtpChannel,
|
||||
normalizePlusPaymentMethod,
|
||||
unwrapGpcBalancePayload,
|
||||
unwrapGpcResponse,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -305,6 +305,13 @@
|
||||
<span id="display-gpc-helper-balance" class="data-value mono">余额未获取</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-gpc-helper-phone-mode" style="display:none;">
|
||||
<span class="data-label">GPC 模式</span>
|
||||
<select id="select-gpc-helper-phone-mode" class="data-select">
|
||||
<option value="manual">手动模式</option>
|
||||
<option value="auto">自动模式</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-gpc-helper-country-code" style="display:none;">
|
||||
<span class="data-label">GPC 区号</span>
|
||||
<select id="select-gpc-helper-country-code" class="data-select">
|
||||
|
||||
+270
-8
@@ -193,6 +193,8 @@ const inputGpcHelperCardKey = document.getElementById('input-gpc-helper-card-key
|
||||
const btnToggleGpcHelperCardKey = document.getElementById('btn-toggle-gpc-helper-card-key');
|
||||
const btnGpcHelperBalance = document.getElementById('btn-gpc-helper-balance');
|
||||
const displayGpcHelperBalance = document.getElementById('display-gpc-helper-balance');
|
||||
const rowGpcHelperPhoneMode = document.getElementById('row-gpc-helper-phone-mode');
|
||||
const selectGpcHelperPhoneMode = document.getElementById('select-gpc-helper-phone-mode');
|
||||
const rowGpcHelperCountryCode = document.getElementById('row-gpc-helper-country-code');
|
||||
const selectGpcHelperCountryCode = document.getElementById('select-gpc-helper-country-code');
|
||||
const rowGpcHelperPhone = document.getElementById('row-gpc-helper-phone');
|
||||
@@ -511,6 +513,8 @@ const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PORTAL_URL = 'https://gpc.qlhazycoder.top/';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
const SIGNUP_METHOD_EMAIL = 'email';
|
||||
const SIGNUP_METHOD_PHONE = 'phone';
|
||||
@@ -805,6 +809,15 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function getStepIdByKeyForCurrentMode(stepKey = '') {
|
||||
const normalizedKey = String(stepKey || '').trim();
|
||||
if (!normalizedKey) {
|
||||
return 0;
|
||||
}
|
||||
const match = (stepDefinitions || []).find((step) => String(step?.key || '') === normalizedKey);
|
||||
return Number(match?.id) || 0;
|
||||
}
|
||||
|
||||
function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
|
||||
currentPlusModeEnabled = Boolean(plusModeEnabled);
|
||||
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
|
||||
@@ -2073,6 +2086,86 @@ function getSelectedPlusPaymentMethod(state = latestState) {
|
||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneModeValue(value = '') {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||||
? GPC_HELPER_PHONE_MODE_AUTO
|
||||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function getGpcHelperAutoModeEnabled(state = latestState) {
|
||||
return Boolean(state?.gopayHelperAutoModeEnabled);
|
||||
}
|
||||
|
||||
function hasGpcAutoModePermissionField(payload = {}) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return false;
|
||||
}
|
||||
return payload.auto_mode_enabled !== undefined
|
||||
|| payload.autoModeEnabled !== undefined
|
||||
|| payload.auto_enabled !== undefined
|
||||
|| payload.autoEnabled !== undefined
|
||||
|| (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) && hasGpcAutoModePermissionField(payload.data));
|
||||
}
|
||||
|
||||
function isGpcAutoModePermissionDenied(state = latestState) {
|
||||
if (getGpcHelperAutoModeEnabled(state)) {
|
||||
return false;
|
||||
}
|
||||
return hasGpcAutoModePermissionField(state?.gopayHelperBalancePayload);
|
||||
}
|
||||
|
||||
function normalizeGpcRemainingUsesValue(value) {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcRemainingUses) {
|
||||
return rootScope.GoPayUtils.normalizeGpcRemainingUses(value);
|
||||
}
|
||||
if (value === undefined || value === null || String(value).trim() === '') {
|
||||
return null;
|
||||
}
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
|
||||
}
|
||||
|
||||
function getGpcBalanceRemainingUsesFromResponse(response = {}) {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
if (rootScope.GoPayUtils?.getGpcBalanceRemainingUses) {
|
||||
const remaining = rootScope.GoPayUtils.getGpcBalanceRemainingUses(response?.data || response?.payload || response);
|
||||
if (remaining !== null && remaining !== undefined) {
|
||||
return remaining;
|
||||
}
|
||||
}
|
||||
return normalizeGpcRemainingUsesValue(
|
||||
response?.remainingUses
|
||||
?? response?.data?.remaining_uses
|
||||
?? response?.data?.remainingUses
|
||||
?? response?.payload?.data?.remaining_uses
|
||||
?? response?.payload?.remaining_uses
|
||||
?? response?.payload?.remainingUses
|
||||
);
|
||||
}
|
||||
|
||||
function getGpcAutoModeEnabledFromResponse(response = {}) {
|
||||
if (typeof response?.autoModeEnabled === 'boolean') {
|
||||
return response.autoModeEnabled;
|
||||
}
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
if (rootScope.GoPayUtils?.isGpcAutoModeEnabled) {
|
||||
return rootScope.GoPayUtils.isGpcAutoModeEnabled(response?.data || response?.payload || response);
|
||||
}
|
||||
return Boolean(
|
||||
response?.data?.auto_mode_enabled
|
||||
?? response?.data?.autoModeEnabled
|
||||
?? response?.payload?.data?.auto_mode_enabled
|
||||
?? response?.payload?.auto_mode_enabled
|
||||
?? response?.payload?.autoModeEnabled
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeGpcOtpChannelValue(value = '') {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
|
||||
@@ -3222,12 +3315,23 @@ function collectSettingsPayload() {
|
||||
: (String(latestState?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'))
|
||||
);
|
||||
const plusPaymentMethod = getSelectedPlusPaymentMethod();
|
||||
const normalizeGpcHelperPhoneModeSafe = typeof normalizeGpcHelperPhoneModeValue === 'function'
|
||||
? normalizeGpcHelperPhoneModeValue
|
||||
: ((value = '') => String(value || '').trim().toLowerCase() === 'auto' || String(value || '').trim().toLowerCase() === 'builtin' ? 'auto' : 'manual');
|
||||
const selectedGpcPhoneMode = normalizeGpcHelperPhoneModeSafe(
|
||||
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
|
||||
? selectGpcHelperPhoneMode.value
|
||||
: (latestState?.gopayHelperPhoneMode || 'manual')
|
||||
);
|
||||
const effectiveGpcPhoneMode = (typeof isGpcAutoModePermissionDenied === 'function' && isGpcAutoModePermissionDenied(latestState))
|
||||
? 'manual'
|
||||
: selectedGpcPhoneMode;
|
||||
const selectedGpcOtpChannel = normalizeGpcOtpChannelSafe(
|
||||
typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel
|
||||
? selectGpcHelperOtpChannel.value
|
||||
: (latestState?.gopayHelperOtpChannel || 'whatsapp')
|
||||
);
|
||||
const selectedGpcLocalSmsHelperEnabled = Boolean(
|
||||
const selectedGpcLocalSmsHelperEnabled = effectiveGpcPhoneMode === 'auto' ? false : Boolean(
|
||||
typeof inputGpcHelperLocalSmsEnabled !== 'undefined' && inputGpcHelperLocalSmsEnabled
|
||||
? inputGpcHelperLocalSmsEnabled.checked
|
||||
: latestState?.gopayHelperLocalSmsHelperEnabled
|
||||
@@ -3342,6 +3446,7 @@ function collectSettingsPayload() {
|
||||
? String(inputGpcHelperCardKey.value || '').trim()
|
||||
: String(latestState?.gopayHelperApiKey || latestState?.gopayHelperCardKey || '').trim(),
|
||||
gopayHelperCardKey: '',
|
||||
gopayHelperPhoneMode: effectiveGpcPhoneMode,
|
||||
gopayHelperCountryCode: window.GoPayUtils?.normalizeGoPayCountryCode
|
||||
? window.GoPayUtils.normalizeGoPayCountryCode(typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode ? selectGpcHelperCountryCode.value : latestState?.gopayHelperCountryCode)
|
||||
: (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode
|
||||
@@ -7192,6 +7297,14 @@ function updatePlusModeUI() {
|
||||
? Boolean(inputPlusModeEnabled.checked)
|
||||
: false;
|
||||
const method = enabled ? getSelectedPlusPaymentMethod() : defaultMethod;
|
||||
const gpcPhoneMode = normalizeGpcHelperPhoneModeValue(
|
||||
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
|
||||
? selectGpcHelperPhoneMode.value
|
||||
: (latestState?.gopayHelperPhoneMode || 'manual')
|
||||
);
|
||||
const gpcAutoModeDenied = isGpcAutoModePermissionDenied(latestState);
|
||||
const gpcAutoModeEnabled = getGpcHelperAutoModeEnabled(latestState);
|
||||
const isGpcAutoMode = !gpcAutoModeDenied && gpcPhoneMode === GPC_HELPER_PHONE_MODE_AUTO;
|
||||
const gpcOtpChannel = normalizeGpcOtpChannelValue(
|
||||
typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel
|
||||
? selectGpcHelperOtpChannel.value
|
||||
@@ -7206,8 +7319,9 @@ function updatePlusModeUI() {
|
||||
? normalizePlusPaymentMethod(selectPlusPaymentMethod.value)
|
||||
: method;
|
||||
const gpcRowsVisible = enabled && selectedMethod === gpcValue;
|
||||
const localSmsControlsVisible = gpcRowsVisible;
|
||||
const effectiveLocalSmsEnabled = localSmsEnabled;
|
||||
const canShowGpcModeSelector = gpcRowsVisible && (gpcAutoModeEnabled || !gpcAutoModeDenied);
|
||||
const localSmsControlsVisible = gpcRowsVisible && !isGpcAutoMode;
|
||||
const effectiveLocalSmsEnabled = !isGpcAutoMode && localSmsEnabled;
|
||||
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
|
||||
selectPlusPaymentMethod.value = method;
|
||||
if (selectPlusPaymentMethod.style) {
|
||||
@@ -7216,7 +7330,7 @@ function updatePlusModeUI() {
|
||||
}
|
||||
if (typeof plusPaymentMethodCaption !== 'undefined' && plusPaymentMethodCaption) {
|
||||
plusPaymentMethodCaption.textContent = method === gpcValue
|
||||
? 'GPC 订阅链路'
|
||||
? `GPC ${isGpcAutoMode ? '自动' : '手动'}订阅链路`
|
||||
: method === gopayValue
|
||||
? 'GoPay 印尼订阅链路'
|
||||
: 'PayPal 订阅链路';
|
||||
@@ -7240,6 +7354,19 @@ function updatePlusModeUI() {
|
||||
[
|
||||
typeof rowGpcHelperApi !== 'undefined' ? rowGpcHelperApi : null,
|
||||
typeof rowGpcHelperCardKey !== 'undefined' ? rowGpcHelperCardKey : null,
|
||||
].forEach((row) => {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.style.display = gpcRowsVisible ? '' : 'none';
|
||||
});
|
||||
if (typeof rowGpcHelperPhoneMode !== 'undefined' && rowGpcHelperPhoneMode) {
|
||||
rowGpcHelperPhoneMode.style.display = canShowGpcModeSelector ? '' : 'none';
|
||||
}
|
||||
if (typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode) {
|
||||
selectGpcHelperPhoneMode.value = gpcAutoModeDenied ? GPC_HELPER_PHONE_MODE_MANUAL : gpcPhoneMode;
|
||||
}
|
||||
[
|
||||
typeof rowGpcHelperCountryCode !== 'undefined' ? rowGpcHelperCountryCode : null,
|
||||
typeof rowGpcHelperPhone !== 'undefined' ? rowGpcHelperPhone : null,
|
||||
typeof rowGpcHelperOtpChannel !== 'undefined' ? rowGpcHelperOtpChannel : null,
|
||||
@@ -7248,7 +7375,7 @@ function updatePlusModeUI() {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.style.display = gpcRowsVisible ? '' : 'none';
|
||||
row.style.display = gpcRowsVisible && !isGpcAutoMode ? '' : 'none';
|
||||
});
|
||||
if (typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel) {
|
||||
selectGpcHelperOtpChannel.value = gpcOtpChannel;
|
||||
@@ -7464,6 +7591,101 @@ async function persistSignupPhoneInputForAction() {
|
||||
await persistSignupPhoneInputValue({ final: true, silent: true });
|
||||
}
|
||||
|
||||
function isGpcHelperCheckoutSelected() {
|
||||
const gpcValue = typeof PLUS_PAYMENT_METHOD_GPC_HELPER !== 'undefined' ? PLUS_PAYMENT_METHOD_GPC_HELPER : 'gpc-helper';
|
||||
const plusEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
|
||||
? Boolean(inputPlusModeEnabled.checked)
|
||||
: Boolean(latestState?.plusModeEnabled);
|
||||
return plusEnabled && getSelectedPlusPaymentMethod() === gpcValue;
|
||||
}
|
||||
|
||||
function getSelectedGpcHelperPhoneMode() {
|
||||
return normalizeGpcHelperPhoneModeValue(
|
||||
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
|
||||
? selectGpcHelperPhoneMode.value
|
||||
: (latestState?.gopayHelperPhoneMode || GPC_HELPER_PHONE_MODE_MANUAL)
|
||||
);
|
||||
}
|
||||
|
||||
async function showGpcStartBlockedDialog(message) {
|
||||
await openConfirmModal({
|
||||
title: 'GPC 任务无法开启',
|
||||
message,
|
||||
confirmLabel: '知道了',
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshGpcBalanceForStart() {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'REFRESH_GPC_CARD_BALANCE',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
gopayHelperApiUrl: inputGpcHelperApi?.value || DEFAULT_GPC_HELPER_API_URL,
|
||||
gopayHelperApiKey: inputGpcHelperCardKey?.value || latestState?.gopayHelperApiKey || '',
|
||||
reason: 'before_start',
|
||||
},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
const nextState = {
|
||||
gopayHelperBalance: response?.balance || latestState?.gopayHelperBalance || '',
|
||||
gopayHelperBalancePayload: response?.data || response?.payload?.data || response?.payload || latestState?.gopayHelperBalancePayload || null,
|
||||
gopayHelperBalanceUpdatedAt: response?.updatedAt || Date.now(),
|
||||
gopayHelperBalanceError: '',
|
||||
gopayHelperRemainingUses: getGpcBalanceRemainingUsesFromResponse(response) ?? 0,
|
||||
gopayHelperAutoModeEnabled: getGpcAutoModeEnabledFromResponse(response),
|
||||
gopayHelperApiKeyStatus: response?.apiKeyStatus || response?.data?.status || response?.payload?.data?.status || response?.payload?.status || '',
|
||||
};
|
||||
syncLatestState(nextState);
|
||||
if (displayGpcHelperBalance && nextState.gopayHelperBalance) {
|
||||
displayGpcHelperBalance.textContent = nextState.gopayHelperBalance;
|
||||
}
|
||||
updatePlusModeUI();
|
||||
return nextState;
|
||||
}
|
||||
|
||||
async function ensureGpcApiKeyReadyForStart(options = {}) {
|
||||
if (!isGpcHelperCheckoutSelected()) {
|
||||
return true;
|
||||
}
|
||||
const selectedMode = getSelectedGpcHelperPhoneMode();
|
||||
let balanceState;
|
||||
try {
|
||||
balanceState = await refreshGpcBalanceForStart();
|
||||
} catch (error) {
|
||||
await showGpcStartBlockedDialog(`API Key 余额校验失败:${error?.message || '未知错误'}。请先确认 API Key 是否正确。`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const remainingUses = normalizeGpcRemainingUsesValue(balanceState.gopayHelperRemainingUses);
|
||||
const apiKeyStatus = String(balanceState.gopayHelperApiKeyStatus || '').trim().toLowerCase();
|
||||
if (apiKeyStatus && apiKeyStatus !== 'active') {
|
||||
await showGpcStartBlockedDialog(`当前 GPC API Key 状态为 ${balanceState.gopayHelperApiKeyStatus},不能开启任务。`);
|
||||
return false;
|
||||
}
|
||||
if (remainingUses !== null && remainingUses <= 0) {
|
||||
await showGpcStartBlockedDialog('当前 GPC API Key 剩余次数不足,不能开启任务。');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedMode === GPC_HELPER_PHONE_MODE_AUTO && !balanceState.gopayHelperAutoModeEnabled) {
|
||||
if (typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode) {
|
||||
selectGpcHelperPhoneMode.value = GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
syncLatestState({ gopayHelperPhoneMode: GPC_HELPER_PHONE_MODE_MANUAL });
|
||||
updatePlusModeUI();
|
||||
await saveSettings({ silent: true, force: true }).catch(() => {});
|
||||
await showGpcStartBlockedDialog('当前 GPC API Key 未开通自动模式,已切回手动模式,不能以自动模式开启任务。');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options?.notify) {
|
||||
showToast('GPC API Key 余额和权限校验通过。', 'success', 1800);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function openPlusManualConfirmationDialog(options = {}) {
|
||||
const method = String(options.method || '').trim().toLowerCase();
|
||||
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
|
||||
@@ -7987,6 +8209,9 @@ function applySettingsState(state) {
|
||||
if (typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey) {
|
||||
inputGpcHelperCardKey.value = state?.gopayHelperApiKey || state?.gopayHelperCardKey || '';
|
||||
}
|
||||
if (typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode) {
|
||||
selectGpcHelperPhoneMode.value = normalizeGpcHelperPhoneModeValue(state?.gopayHelperPhoneMode || 'manual');
|
||||
}
|
||||
if (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode) {
|
||||
const normalizedCountryCode = window.GoPayUtils?.normalizeGoPayCountryCode
|
||||
? window.GoPayUtils.normalizeGoPayCountryCode(state?.gopayHelperCountryCode)
|
||||
@@ -10849,6 +11074,10 @@ stepsList?.addEventListener('click', async (event) => {
|
||||
return;
|
||||
}
|
||||
await persistCurrentSettingsForAction();
|
||||
const gpcCreateStep = getStepIdByKeyForCurrentMode('plus-checkout-create') || 6;
|
||||
if (step === gpcCreateStep && !(await ensureGpcApiKeyReadyForStart())) {
|
||||
return;
|
||||
}
|
||||
if (step === 3) {
|
||||
if (inputPassword.value !== (latestState?.customPassword || '')) {
|
||||
await chrome.runtime.sendMessage({
|
||||
@@ -11105,6 +11334,10 @@ async function startAutoRunFromCurrentSettings() {
|
||||
if (typeof persistCurrentSettingsForAction === 'function') {
|
||||
await persistCurrentSettingsForAction();
|
||||
}
|
||||
if (!(await ensureGpcApiKeyReadyForStart())) {
|
||||
clearPendingAutoRunStartRunCount();
|
||||
return false;
|
||||
}
|
||||
|
||||
const customEmailPoolEnabled = typeof usesCustomEmailPoolGenerator === 'function'
|
||||
&& usesCustomEmailPoolGenerator();
|
||||
@@ -11435,7 +11668,7 @@ btnGpcHelperBalance?.addEventListener('click', async () => {
|
||||
type: 'REFRESH_GPC_CARD_BALANCE',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL,
|
||||
gopayHelperApiUrl: inputGpcHelperApi?.value || DEFAULT_GPC_HELPER_API_URL,
|
||||
gopayHelperApiKey: inputGpcHelperCardKey?.value || '',
|
||||
gopayHelperCountryCode: selectGpcHelperCountryCode?.value || '+86',
|
||||
reason: 'manual',
|
||||
@@ -11447,7 +11680,25 @@ btnGpcHelperBalance?.addEventListener('click', async () => {
|
||||
if (displayGpcHelperBalance) {
|
||||
displayGpcHelperBalance.textContent = response?.balance || '余额已更新';
|
||||
}
|
||||
showToast('GPC 余额已更新。', 'success');
|
||||
const nextState = {
|
||||
gopayHelperBalance: response?.balance || latestState?.gopayHelperBalance || '',
|
||||
gopayHelperBalancePayload: response?.data || response?.payload?.data || response?.payload || latestState?.gopayHelperBalancePayload || null,
|
||||
gopayHelperBalanceUpdatedAt: response?.updatedAt || Date.now(),
|
||||
gopayHelperBalanceError: '',
|
||||
gopayHelperRemainingUses: getGpcBalanceRemainingUsesFromResponse(response) ?? 0,
|
||||
gopayHelperAutoModeEnabled: getGpcAutoModeEnabledFromResponse(response),
|
||||
gopayHelperApiKeyStatus: response?.apiKeyStatus || response?.data?.status || response?.payload?.data?.status || response?.payload?.status || '',
|
||||
};
|
||||
syncLatestState(nextState);
|
||||
if (!nextState.gopayHelperAutoModeEnabled && getSelectedGpcHelperPhoneMode() === GPC_HELPER_PHONE_MODE_AUTO) {
|
||||
selectGpcHelperPhoneMode.value = GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
syncLatestState({ gopayHelperPhoneMode: GPC_HELPER_PHONE_MODE_MANUAL });
|
||||
await saveSettings({ silent: true, force: true }).catch(() => {});
|
||||
showToast('当前 API Key 未开通自动模式,已切回手动模式。', 'warn');
|
||||
} else {
|
||||
showToast(nextState.gopayHelperAutoModeEnabled ? 'GPC 余额已更新,自动模式可用。' : 'GPC 余额已更新,当前 API Key 只能使用手动模式。', 'success');
|
||||
}
|
||||
updatePlusModeUI();
|
||||
} catch (error) {
|
||||
showToast(error?.message || '查询 GPC 余额失败。', 'error');
|
||||
}
|
||||
@@ -11466,6 +11717,7 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
|
||||
[
|
||||
inputGpcHelperApi,
|
||||
inputGpcHelperCardKey,
|
||||
selectGpcHelperPhoneMode,
|
||||
selectGpcHelperCountryCode,
|
||||
inputGpcHelperPhone,
|
||||
selectGpcHelperOtpChannel,
|
||||
@@ -11482,7 +11734,7 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
input?.addEventListener('change', () => {
|
||||
if (input === selectGpcHelperOtpChannel || input === inputGpcHelperLocalSmsEnabled) {
|
||||
if (input === selectGpcHelperPhoneMode || input === selectGpcHelperOtpChannel || input === inputGpcHelperLocalSmsEnabled) {
|
||||
updatePlusModeUI();
|
||||
}
|
||||
markSettingsDirty(true);
|
||||
@@ -13393,6 +13645,14 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message.payload.plusPaymentMethod !== undefined && selectPlusPaymentMethod) {
|
||||
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(message.payload.plusPaymentMethod);
|
||||
}
|
||||
if (message.payload.gopayHelperPhoneMode !== undefined && selectGpcHelperPhoneMode) {
|
||||
selectGpcHelperPhoneMode.value = normalizeGpcHelperPhoneModeValue(message.payload.gopayHelperPhoneMode);
|
||||
}
|
||||
if (message.payload.gopayHelperAutoModeEnabled === false && selectGpcHelperPhoneMode?.value === GPC_HELPER_PHONE_MODE_AUTO) {
|
||||
selectGpcHelperPhoneMode.value = GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
syncLatestState({ gopayHelperPhoneMode: GPC_HELPER_PHONE_MODE_MANUAL });
|
||||
showToast('当前 API Key 未开通自动模式,已切回手动模式。', 'warn', 2200);
|
||||
}
|
||||
if (message.payload.gopayHelperOtpChannel !== undefined && selectGpcHelperOtpChannel) {
|
||||
selectGpcHelperOtpChannel.value = normalizeGpcOtpChannelValue(message.payload.gopayHelperOtpChannel);
|
||||
}
|
||||
@@ -13414,6 +13674,8 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (
|
||||
message.payload.plusModeEnabled !== undefined
|
||||
|| message.payload.plusPaymentMethod !== undefined
|
||||
|| message.payload.gopayHelperPhoneMode !== undefined
|
||||
|| message.payload.gopayHelperAutoModeEnabled !== undefined
|
||||
|| message.payload.gopayHelperOtpChannel !== undefined
|
||||
|| message.payload.gopayHelperLocalSmsHelperEnabled !== undefined
|
||||
) {
|
||||
|
||||
@@ -55,6 +55,7 @@ test('background account history settings are normalized independently from hotm
|
||||
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
|
||||
extractFunction('normalizeVerificationResendCount'),
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('normalizeGpcHelperPhoneMode'),
|
||||
extractFunction('normalizePhoneSmsProvider'),
|
||||
extractFunction('normalizePhoneSmsProviderOrder'),
|
||||
extractFunction('normalizeSignupMethod'),
|
||||
@@ -207,6 +208,12 @@ return {
|
||||
);
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiUrl', ''), 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiKey', ' gpc-123 '), 'gpc-123');
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'auto'), 'auto');
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'builtin'), 'auto');
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'unknown'), 'manual');
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperRemainingUses', '998'), 998);
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperAutoModeEnabled', 1), true);
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiKeyStatus', ' active '), 'active');
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperCountryCode', ' 86 '), '+86');
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneNumber', ' +86 138-0013-8000 '), '+8613800138000');
|
||||
assert.equal(api.normalizePersistentSettingValue('gopayHelperPin', ' 12-34-56 '), '123456');
|
||||
|
||||
@@ -146,7 +146,7 @@ function createRouter(overrides = {}) {
|
||||
verifyHotmailAccount: async () => {},
|
||||
refreshGpcCardBalance: overrides.refreshGpcCardBalance || (async (state, options) => {
|
||||
events.balanceRefreshes.push({ state, options });
|
||||
return { balance: '余额 3' };
|
||||
return { balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -556,7 +556,7 @@ test('message router refreshes GPC balance through explicit sidepanel message',
|
||||
},
|
||||
}, {});
|
||||
|
||||
assert.deepStrictEqual(response, { ok: true, balance: '余额 3' });
|
||||
assert.deepStrictEqual(response, { ok: true, balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' });
|
||||
assert.equal(events.balanceRefreshes.length, 1);
|
||||
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiUrl, 'http://localhost:18473/');
|
||||
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiKey, 'payload_api_key');
|
||||
|
||||
@@ -15,6 +15,10 @@ test('GoPay utils normalize manual OTP input', () => {
|
||||
assert.equal(api.normalizeGpcOtpChannel('sms'), 'sms');
|
||||
assert.equal(api.normalizeGpcOtpChannel('wa'), 'whatsapp');
|
||||
assert.equal(api.normalizeGpcOtpChannel('unknown'), 'whatsapp');
|
||||
assert.equal(api.normalizeGpcHelperPhoneMode('auto'), 'auto');
|
||||
assert.equal(api.normalizeGpcHelperPhoneMode('builtin'), 'auto');
|
||||
assert.equal(api.normalizeGpcHelperPhoneMode('manual'), 'manual');
|
||||
assert.equal(api.normalizeGpcHelperPhoneMode('unknown'), 'manual');
|
||||
});
|
||||
|
||||
test('GoPay utils keeps GPC helper payment method distinct', () => {
|
||||
@@ -81,10 +85,21 @@ test('GoPay utils formats balance and maps linked-account errors', () => {
|
||||
api.formatGpcBalancePayload({
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: { remaining_uses: 0, total_uses: 3, used_uses: 3, status: 'active' },
|
||||
data: { remaining_uses: 0, total_uses: 3, used_uses: 3, status: 'active', auto_mode_enabled: false },
|
||||
}),
|
||||
'余额 0/3,已用 3,状态 active'
|
||||
'余额 0/3,已用 3,状态 active,自动模式 未开通'
|
||||
);
|
||||
assert.equal(
|
||||
api.formatGpcBalancePayload({
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: { remaining_uses: 998, total_uses: 1000, used_uses: 2, status: 'active', auto_mode_enabled: true },
|
||||
}),
|
||||
'余额 998/1000,已用 2,状态 active,自动模式 已开通'
|
||||
);
|
||||
assert.equal(api.getGpcBalanceRemainingUses({ data: { remaining_uses: 998 } }), 998);
|
||||
assert.equal(api.isGpcAutoModeEnabled({ data: { auto_mode_enabled: true } }), true);
|
||||
assert.equal(api.isGpcAutoModeEnabled({ data: { auto_mode_enabled: false } }), false);
|
||||
assert.deepEqual(
|
||||
api.unwrapGpcResponse({ code: 200, message: 'ok', data: { task_id: 'task_1' } }),
|
||||
{ task_id: 'task_1' }
|
||||
|
||||
@@ -903,11 +903,119 @@ test('GPC billing polls queue task, submits WhatsApp OTP then PIN, and waits unt
|
||||
assert.equal(pinCall.options.headers['X-API-Key'], 'gpc_billing_123');
|
||||
assert.ok(fetchCalls.findIndex((call) => call.url.endsWith('/api/gp/tasks/task_123/pin')) < fetchCalls.length - 1);
|
||||
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_123' && state.gopayHelperTaskStatus === 'completed'), true);
|
||||
assert.equal(events.logs.some((entry) => entry.message === '步骤 7:GPC 任务状态:等待 WhatsApp OTP'), true);
|
||||
assert.equal(events.logs.some((entry) => /whatsapp_otp_wait/.test(entry.message)), false);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
|
||||
assert.ok(events.sleeps.includes(3000));
|
||||
});
|
||||
|
||||
|
||||
test('GPC billing auto mode only polls until completed without OTP or PIN submission', async () => {
|
||||
const fetchCalls = [];
|
||||
let pollCount = 0;
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [],
|
||||
stateByFrame: {},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_auto') {
|
||||
pollCount += 1;
|
||||
if (pollCount === 1) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'queued', status_text: '排队中', api_waiting_for: '' }),
|
||||
};
|
||||
}
|
||||
if (pollCount === 2) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'active', status_text: '处理中', remote_stage: 'auto_otp_wait', api_waiting_for: 'auto_otp' }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'completed', status_text: '充值完成', remote_stage: 'completed', api_waiting_for: '' }),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
gopayHelperTaskId: 'task_auto',
|
||||
gopayHelperPhoneMode: 'auto',
|
||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||
gopayHelperApiKey: 'gpc_auto',
|
||||
});
|
||||
|
||||
assert.equal(fetchCalls.length, 3);
|
||||
assert.equal(fetchCalls.some((call) => /\/api\/gp\/tasks\/task_auto\/(otp|pin)$/.test(call.url)), false);
|
||||
assert.equal(events.logs.some((entry) => entry.message === '步骤 7:GPC 任务状态:等待自动 OTP'), true);
|
||||
assert.equal(events.logs.some((entry) => /auto_otp_wait/.test(entry.message)), false);
|
||||
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
|
||||
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_auto' && state.gopayHelperPhoneMode === 'auto' && state.gopayHelperTaskStatus === 'completed'), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
|
||||
});
|
||||
|
||||
test('GPC billing logs checkout order stage in Chinese', async () => {
|
||||
let pollCount = 0;
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [],
|
||||
stateByFrame: {},
|
||||
fetchImpl: async (url) => {
|
||||
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_stage') {
|
||||
pollCount += 1;
|
||||
if (pollCount === 1) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcTaskResponse({
|
||||
task_id: 'task_stage',
|
||||
phone_mode: 'auto',
|
||||
status: 'active',
|
||||
status_text: '处理中',
|
||||
remote_stage: 'checkout_order_start',
|
||||
api_waiting_for: '',
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcTaskResponse({
|
||||
task_id: 'task_stage',
|
||||
phone_mode: 'auto',
|
||||
status: 'completed',
|
||||
status_text: '充值完成',
|
||||
remote_stage: 'completed',
|
||||
api_waiting_for: '',
|
||||
}),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
gopayHelperTaskId: 'task_stage',
|
||||
gopayHelperPhoneMode: 'auto',
|
||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||
gopayHelperApiKey: 'gpc_auto',
|
||||
});
|
||||
|
||||
assert.equal(events.logs.some((entry) => entry.message === '步骤 7:GPC 任务状态:创建订单'), true);
|
||||
assert.equal(events.logs.some((entry) => /checkout_order_start/.test(entry.message)), false);
|
||||
});
|
||||
|
||||
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
|
||||
const fetchCalls = [];
|
||||
let pollCount = 0;
|
||||
|
||||
@@ -3,9 +3,42 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/steps/create-plus-checkout.js', 'utf8');
|
||||
const gopayUtilsSource = fs.readFileSync('gopay-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
new Function('self', `${gopayUtilsSource};`)(globalScope);
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPlusCheckoutCreate;`)(globalScope);
|
||||
|
||||
function createGpcBalanceResponse(overrides = {}) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
api_key: 'gpc_test',
|
||||
status: 'active',
|
||||
auto_mode_enabled: false,
|
||||
total_uses: 1000,
|
||||
remaining_uses: 998,
|
||||
used_uses: 2,
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createGpcTaskResponse(overrides = {}) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
task_id: 'task_123',
|
||||
status: 'active',
|
||||
status_text: '处理中',
|
||||
phone_mode: 'manual',
|
||||
remote_stage: 'checkout_start',
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('Plus checkout create does not wait 20 seconds after opening checkout page', async () => {
|
||||
const events = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
@@ -107,7 +140,7 @@ test('GoPay plus checkout create forwards gopay payment method to the checkout c
|
||||
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
|
||||
});
|
||||
|
||||
test('GPC checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
|
||||
test('GPC manual checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
|
||||
const events = [];
|
||||
const fetchCalls = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
@@ -128,18 +161,9 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: {
|
||||
task_id: 'task_123',
|
||||
status: 'active',
|
||||
status_text: '处理中',
|
||||
phone_mode: 'manual',
|
||||
remote_stage: 'checkout_start',
|
||||
otp_channel: 'whatsapp',
|
||||
},
|
||||
}),
|
||||
json: async () => url.endsWith('/api/gp/balance')
|
||||
? createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 })
|
||||
: createGpcTaskResponse({ otp_channel: 'whatsapp' }),
|
||||
};
|
||||
},
|
||||
registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }),
|
||||
@@ -155,6 +179,7 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
|
||||
await executor.executePlusCheckoutCreate({
|
||||
email: 'Current.Round+GPC@Example.COM',
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
gopayHelperPhoneMode: 'manual',
|
||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||
gopayHelperPhoneNumber: '+8613800138000',
|
||||
gopayPhone: '',
|
||||
@@ -172,9 +197,11 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
|
||||
includeSession: true,
|
||||
includeAccessToken: true,
|
||||
});
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
|
||||
const helperPayload = JSON.parse(fetchCalls[0].options.body);
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
|
||||
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_test_123');
|
||||
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
|
||||
const helperPayload = JSON.parse(fetchCalls[1].options.body);
|
||||
assert.deepEqual(helperPayload, {
|
||||
access_token: 'session-access-token',
|
||||
phone_mode: 'manual',
|
||||
@@ -182,7 +209,7 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
|
||||
phone_number: '13800138000',
|
||||
otp_channel: 'whatsapp',
|
||||
});
|
||||
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_test_123');
|
||||
assert.equal(fetchCalls[1].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);
|
||||
@@ -199,6 +226,165 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
|
||||
});
|
||||
|
||||
|
||||
test('GPC auto checkout only sends access token and API Key', async () => {
|
||||
const events = [];
|
||||
const fetchCalls = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }),
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async () => {
|
||||
throw new Error('should not open token tab when direct access token exists');
|
||||
},
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => url.endsWith('/api/gp/balance')
|
||||
? createGpcBalanceResponse({ auto_mode_enabled: true, remaining_uses: 998 })
|
||||
: createGpcTaskResponse({
|
||||
task_id: 'task_auto',
|
||||
status: 'queued',
|
||||
status_text: '排队中',
|
||||
phone_mode: 'auto',
|
||||
api_waiting_for: '',
|
||||
}),
|
||||
};
|
||||
},
|
||||
registerTab: async () => {},
|
||||
sendTabMessageUntilStopped: async () => ({}),
|
||||
setState: async (payload) => events.push({ type: 'set-state', payload }),
|
||||
sleepWithStop: async () => {},
|
||||
waitForTabCompleteUntilStopped: async () => {},
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutCreate({
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
gopayHelperPhoneMode: 'auto',
|
||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||
chatgptAccessToken: 'state-access-token',
|
||||
gopayHelperApiKey: 'gpc_auto_123',
|
||||
});
|
||||
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
|
||||
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_auto_123');
|
||||
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
|
||||
const helperPayload = JSON.parse(fetchCalls[1].options.body);
|
||||
assert.deepEqual(helperPayload, {
|
||||
access_token: 'state-access-token',
|
||||
phone_mode: 'auto',
|
||||
});
|
||||
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_auto_123');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'country_code'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'phone_number'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'otp_channel'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'pin'), false);
|
||||
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
|
||||
assert.equal(statePayload.gopayHelperTaskId, 'task_auto');
|
||||
assert.equal(statePayload.gopayHelperPhoneMode, 'auto');
|
||||
assert.equal(statePayload.gopayHelperTaskStatus, 'queued');
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
|
||||
});
|
||||
|
||||
test('GPC auto checkout blocks API Keys without auto mode permission', async () => {
|
||||
const fetchCalls = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async () => {
|
||||
throw new Error('should not open token tab when direct access token exists');
|
||||
},
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 }),
|
||||
};
|
||||
},
|
||||
registerTab: async () => {},
|
||||
sendTabMessageUntilStopped: async () => ({}),
|
||||
setState: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
waitForTabCompleteUntilStopped: async () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executePlusCheckoutCreate({
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
gopayHelperPhoneMode: 'auto',
|
||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||
chatgptAccessToken: 'state-access-token',
|
||||
gopayHelperApiKey: 'gpc_auto_disabled',
|
||||
}),
|
||||
/未开通自动模式/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
|
||||
});
|
||||
|
||||
test('GPC checkout blocks exhausted API Keys before creating task', async () => {
|
||||
const fetchCalls = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async () => {
|
||||
throw new Error('should not open token tab when direct access token exists');
|
||||
},
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 0 }),
|
||||
};
|
||||
},
|
||||
registerTab: async () => {},
|
||||
sendTabMessageUntilStopped: async () => ({}),
|
||||
setState: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
waitForTabCompleteUntilStopped: async () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executePlusCheckoutCreate({
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
gopayHelperPhoneMode: 'manual',
|
||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||
chatgptAccessToken: 'state-access-token',
|
||||
gopayHelperPhoneNumber: '+8613800138000',
|
||||
gopayHelperCountryCode: '+86',
|
||||
gopayHelperPin: '123456',
|
||||
gopayHelperApiKey: 'gpc_exhausted',
|
||||
}),
|
||||
/剩余次数不足/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
|
||||
});
|
||||
|
||||
test('GPC checkout forwards selected SMS OTP channel', async () => {
|
||||
const fetchCalls = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
@@ -216,11 +402,9 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
code: 200,
|
||||
message: 'ok',
|
||||
data: { task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' },
|
||||
}),
|
||||
json: async () => url.endsWith('/api/gp/balance')
|
||||
? createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 })
|
||||
: createGpcTaskResponse({ task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' }),
|
||||
};
|
||||
},
|
||||
registerTab: async () => {},
|
||||
@@ -241,10 +425,13 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
|
||||
gopayHelperOtpChannel: 'sms',
|
||||
});
|
||||
|
||||
const helperPayload = JSON.parse(fetchCalls[0].options.body);
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
|
||||
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_sms');
|
||||
const helperPayload = JSON.parse(fetchCalls[1].options.body);
|
||||
assert.equal(helperPayload.phone_mode, 'manual');
|
||||
assert.equal(helperPayload.otp_channel, 'sms');
|
||||
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_sms');
|
||||
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_sms');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
|
||||
});
|
||||
|
||||
@@ -264,6 +451,13 @@ test('GPC checkout surfaces unified queue API errors', async () => {
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
if (url.endsWith('/api/gp/balance')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
status: 400,
|
||||
@@ -295,9 +489,11 @@ test('GPC checkout surfaces unified queue API errors', async () => {
|
||||
/创建 GPC 订单失败:access_token 无效/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(JSON.parse(fetchCalls[0].options.body), 'card_key'), false);
|
||||
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_paid_456');
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
|
||||
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(JSON.parse(fetchCalls[1].options.body), 'card_key'), false);
|
||||
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_paid_456');
|
||||
});
|
||||
|
||||
test('GPC checkout does not fall back to browser GoPay phone fields', async () => {
|
||||
@@ -326,6 +522,7 @@ test('GPC checkout does not fall back to browser GoPay phone fields', async () =
|
||||
await assert.rejects(
|
||||
() => executor.executePlusCheckoutCreate({
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
gopayHelperPhoneMode: 'manual',
|
||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||
chatgptAccessToken: 'state-access-token',
|
||||
email: 'helper-phone-test@example.com',
|
||||
|
||||
@@ -113,6 +113,9 @@ async function refreshContributionContentHint() {
|
||||
events.push({ type: 'refresh' });
|
||||
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
|
||||
}
|
||||
async function ensureGpcApiKeyReadyForStart() {
|
||||
return true;
|
||||
}
|
||||
${bundle}
|
||||
return {
|
||||
startAutoRunFromCurrentSettings,
|
||||
|
||||
@@ -132,6 +132,10 @@ test('sidepanel Plus UI hides PayPal account selector while GoPay is selected',
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('getSelectedPlusPaymentMethod'),
|
||||
extractFunction('normalizeGpcHelperPhoneModeValue'),
|
||||
extractFunction('getGpcHelperAutoModeEnabled'),
|
||||
extractFunction('hasGpcAutoModePermissionField'),
|
||||
extractFunction('isGpcAutoModePermissionDenied'),
|
||||
extractFunction('normalizeGpcOtpChannelValue'),
|
||||
extractFunction('updatePlusModeUI'),
|
||||
].join('\n');
|
||||
@@ -141,6 +145,8 @@ let latestState = { plusPaymentMethod: 'gopay' };
|
||||
let currentPlusPaymentMethod = 'paypal';
|
||||
const inputPlusModeEnabled = { checked: true };
|
||||
const selectPlusPaymentMethod = { value: 'gopay', style: { display: 'none' } };
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
${bundle}
|
||||
return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
|
||||
@@ -209,21 +215,29 @@ test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () =
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('getSelectedPlusPaymentMethod'),
|
||||
extractFunction('normalizeGpcHelperPhoneModeValue'),
|
||||
extractFunction('getGpcHelperAutoModeEnabled'),
|
||||
extractFunction('hasGpcAutoModePermissionField'),
|
||||
extractFunction('isGpcAutoModePermissionDenied'),
|
||||
extractFunction('normalizeGpcOtpChannelValue'),
|
||||
extractFunction('updatePlusModeUI'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = { plusPaymentMethod: 'gpc-helper' };
|
||||
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperAutoModeEnabled: true };
|
||||
let currentPlusPaymentMethod = 'paypal';
|
||||
const inputPlusModeEnabled = { checked: true };
|
||||
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
const rowPlusPaymentMethod = { style: { display: 'none' } };
|
||||
const rowGpcHelperApi = { style: { display: 'none' } };
|
||||
const rowGpcHelperCardKey = { style: { display: 'none' } };
|
||||
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
|
||||
const selectGpcHelperPhoneMode = { value: 'manual' };
|
||||
const rowGpcHelperCountryCode = { style: { display: 'none' } };
|
||||
const rowGpcHelperPhone = { style: { display: 'none' } };
|
||||
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
|
||||
@@ -240,12 +254,13 @@ ${bundle}
|
||||
return {
|
||||
updatePlusModeUI,
|
||||
selectPlusPaymentMethod,
|
||||
selectGpcHelperPhoneMode,
|
||||
selectGpcHelperOtpChannel,
|
||||
inputGpcHelperLocalSmsEnabled,
|
||||
btnGpcCardKeyPurchase,
|
||||
rowPayPalAccount,
|
||||
plusPaymentMethodCaption,
|
||||
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
|
||||
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperPhoneMode, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
|
||||
};
|
||||
`)();
|
||||
|
||||
@@ -255,6 +270,7 @@ return {
|
||||
assert.equal(api.btnGpcCardKeyPurchase.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperApi.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
|
||||
@@ -272,6 +288,15 @@ return {
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
|
||||
|
||||
api.selectGpcHelperPhoneMode.value = 'auto';
|
||||
api.updatePlusModeUI();
|
||||
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
|
||||
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, 'none');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
|
||||
assert.match(api.plusPaymentMethodCaption.textContent, /自动/);
|
||||
|
||||
api.selectPlusPaymentMethod.value = 'gopay';
|
||||
api.updatePlusModeUI();
|
||||
assert.equal(api.btnGpcCardKeyPurchase.style.display, 'none');
|
||||
@@ -279,6 +304,103 @@ return {
|
||||
assert.equal(api.rowPayPalAccount.style.display, 'none');
|
||||
});
|
||||
|
||||
test('sidepanel hides GPC auto mode selector when API Key has no auto permission', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('getSelectedPlusPaymentMethod'),
|
||||
extractFunction('normalizeGpcHelperPhoneModeValue'),
|
||||
extractFunction('getGpcHelperAutoModeEnabled'),
|
||||
extractFunction('hasGpcAutoModePermissionField'),
|
||||
extractFunction('isGpcAutoModePermissionDenied'),
|
||||
extractFunction('normalizeGpcOtpChannelValue'),
|
||||
extractFunction('updatePlusModeUI'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: { auto_mode_enabled: false } };
|
||||
let currentPlusPaymentMethod = 'gpc-helper';
|
||||
const inputPlusModeEnabled = { checked: true };
|
||||
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
const rowPlusPaymentMethod = { style: { display: 'none' } };
|
||||
const rowGpcHelperApi = { style: { display: 'none' } };
|
||||
const rowGpcHelperCardKey = { style: { display: 'none' } };
|
||||
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
|
||||
const selectGpcHelperPhoneMode = { value: 'auto' };
|
||||
const rowGpcHelperCountryCode = { style: { display: 'none' } };
|
||||
const rowGpcHelperPhone = { style: { display: 'none' } };
|
||||
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
|
||||
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
|
||||
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
|
||||
const inputGpcHelperLocalSmsEnabled = { checked: false };
|
||||
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
|
||||
const rowGpcHelperPin = { style: { display: 'none' } };
|
||||
${bundle}
|
||||
return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, rows: { rowGpcHelperPhoneMode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperPin } };
|
||||
`)();
|
||||
|
||||
api.updatePlusModeUI();
|
||||
|
||||
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, 'none');
|
||||
assert.equal(api.selectGpcHelperPhoneMode.value, 'manual');
|
||||
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperPin.style.display, '');
|
||||
assert.match(api.plusPaymentMethodCaption.textContent, /手动/);
|
||||
});
|
||||
|
||||
test('sidepanel keeps selected GPC auto mode before permission has been queried', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('getSelectedPlusPaymentMethod'),
|
||||
extractFunction('normalizeGpcHelperPhoneModeValue'),
|
||||
extractFunction('getGpcHelperAutoModeEnabled'),
|
||||
extractFunction('hasGpcAutoModePermissionField'),
|
||||
extractFunction('isGpcAutoModePermissionDenied'),
|
||||
extractFunction('normalizeGpcOtpChannelValue'),
|
||||
extractFunction('updatePlusModeUI'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: null };
|
||||
let currentPlusPaymentMethod = 'gpc-helper';
|
||||
const inputPlusModeEnabled = { checked: true };
|
||||
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
const rowPlusPaymentMethod = { style: { display: 'none' } };
|
||||
const rowGpcHelperApi = { style: { display: 'none' } };
|
||||
const rowGpcHelperCardKey = { style: { display: 'none' } };
|
||||
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
|
||||
const selectGpcHelperPhoneMode = { value: 'auto' };
|
||||
const rowGpcHelperCountryCode = { style: { display: 'none' } };
|
||||
const rowGpcHelperPhone = { style: { display: 'none' } };
|
||||
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
|
||||
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
|
||||
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
|
||||
const inputGpcHelperLocalSmsEnabled = { checked: false };
|
||||
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
|
||||
const rowGpcHelperPin = { style: { display: 'none' } };
|
||||
${bundle}
|
||||
return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, rows: { rowGpcHelperPhoneMode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperPin } };
|
||||
`)();
|
||||
|
||||
api.updatePlusModeUI();
|
||||
|
||||
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
|
||||
assert.equal(api.selectGpcHelperPhoneMode.value, 'auto');
|
||||
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
|
||||
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, 'none');
|
||||
assert.equal(api.rows.rowGpcHelperPin.style.display, 'none');
|
||||
assert.match(api.plusPaymentMethodCaption.textContent, /自动/);
|
||||
});
|
||||
|
||||
test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED state', async () => {
|
||||
const bundle = [
|
||||
extractFunction('openPlusManualConfirmationDialog'),
|
||||
|
||||
@@ -111,7 +111,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13);
|
||||
assert.equal(gpcSteps[5].title, '创建 GPC 订单');
|
||||
assert.equal(gpcSteps[6].title, 'GPC OTP/PIN 验证');
|
||||
assert.equal(gpcSteps[6].title, '等待 GPC 任务完成');
|
||||
});
|
||||
|
||||
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
|
||||
@@ -142,6 +142,9 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
|
||||
assert.match(html, />转换 API Key</);
|
||||
assert.match(html, /GPC API Key/);
|
||||
assert.match(html, /id="input-gpc-helper-card-key"/);
|
||||
assert.match(html, /GPC 模式/);
|
||||
assert.match(html, /id="select-gpc-helper-phone-mode"/);
|
||||
assert.match(html, /<option value="auto">自动模式<\/option>/);
|
||||
assert.match(html, /id="btn-gpc-helper-balance"/);
|
||||
assert.match(html, /id="input-gpc-helper-phone"/);
|
||||
assert.match(html, /id="select-gpc-helper-otp-channel"/);
|
||||
|
||||
+3
-3
@@ -183,7 +183,7 @@
|
||||
- Codex2API 配置
|
||||
- IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值
|
||||
- Plus 模式开关 `plusModeEnabled`
|
||||
- Plus 支付方式 `plusPaymentMethod`,GoPay 配置 `gopayPhone / gopayPin`,GPC helper 配置 `gopayHelperApiUrl / gopayHelperApiKey / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin`;其中 GPC API 地址固定归一为 `https://gpc.qlhazycoder.top`
|
||||
- Plus 支付方式 `plusPaymentMethod`,GoPay 配置 `gopayPhone / gopayPin`,GPC helper 配置 `gopayHelperApiUrl / gopayHelperApiKey / gopayHelperPhoneMode / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin`;其中 GPC API 地址固定归一为 `https://gpc.qlhazycoder.top`
|
||||
- PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword`
|
||||
- 邮箱 provider 配置
|
||||
- Cloud Mail / SkyMail API 配置:`cloudMailBaseUrl / cloudMailAdminEmail / cloudMailAdminPassword / cloudMailToken / cloudMailReceiveMailbox / cloudMailDomain / cloudMailDomains`
|
||||
@@ -551,8 +551,8 @@ Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完
|
||||
Plus 模式可见步骤:
|
||||
|
||||
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
|
||||
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`。
|
||||
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` 接口。
|
||||
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 和 `gopayHelperPhoneMode` 提交到 `gopayHelperApiUrl` 的 `/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证;自动模式只传 `phone_mode=auto`,手动模式额外传手机号、国家区号和 `otp_channel`,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`。
|
||||
3. 第 7 步 `填写账单并提交订阅`:PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是轮询 `/api/gp/tasks/{task_id}`。自动模式只等待远端 `completed` 后继续后续步骤;手动模式根据远端 `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. 仅 PayPal / GoPay 会继续显示第 8 步:该步按当前支付方式显示为 `PayPal 登录与授权` 或 `GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`。
|
||||
5. 第 9 步 `订阅回跳确认` 仅在 PayPal / GoPay 模式下等待授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。GPC helper 模式在第 7 步任务完成后会直接进入 Plus 可见第 10 步 OAuth 登录。
|
||||
6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。
|
||||
|
||||
Reference in New Issue
Block a user