Refactor GPC plus flow to page recharge

This commit is contained in:
QLHazyCoder
2026-05-29 10:09:26 +08:00
parent 9ef9cb4c46
commit 56f52cb858
40 changed files with 1278 additions and 5203 deletions
+1 -1
View File
@@ -71,7 +71,7 @@
## 邮箱与验证码能力
- 支持网页邮箱轮询、API 邮箱轮询和本地 helper 读取三类模式。
- 支持注册验证码、登录验证码、绑定邮箱验证码,以及 Plus / GPC 场景下的 OTP 处理。
- 支持注册验证码、登录验证码、绑定邮箱验证码,以及 GoPay 场景下的 OTP / PIN 处理。
- `2925` 支持多账号池、自动登录、自动切号、24 小时冷却。
- `Hotmail` 支持远程服务模式和本地 helper 模式。
- `自定义邮箱池``自定义邮箱服务号池` 都可以和自动运行轮数联动。
+101 -237
View File
@@ -578,7 +578,7 @@ const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
const DEFAULT_SUB2API_URL = '';
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
const DEFAULT_GPC_BASE_URL = 'https://gpc.qlhazycoder.top';
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
const DEFAULT_SUB2API_PROXY_NAME = '';
const DEFAULT_SUB2API_ACCOUNT_PRIORITY = 1;
@@ -861,11 +861,6 @@ 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 normalizeOpenAiContributionSource(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === CONTRIBUTION_SOURCE_SUB2API
@@ -1342,47 +1337,16 @@ const PERSISTED_SETTING_DEFAULTS = {
gopayPhone: '',
gopayOtp: '',
gopayPin: '',
gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL,
gopayHelperApiKey: '',
gopayHelperCardKey: '',
gopayHelperPhoneMode: 'manual',
gopayHelperPhoneNumber: '',
gopayHelperCountryCode: '+86',
gopayHelperPin: '',
gopayHelperOtpChannel: 'whatsapp',
gopayHelperLocalSmsHelperEnabled: false,
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
gopayHelperLocalSmsTimeoutSeconds: 90,
gopayHelperLocalSmsPollIntervalSeconds: 2,
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
gopayHelperRedirectUrl: '',
gopayHelperNextAction: '',
gopayHelperFlowId: '',
gopayHelperChallengeId: '',
gopayHelperStartPayload: null,
gopayHelperTaskId: '',
gopayHelperTaskStatus: '',
gopayHelperStatusText: '',
gopayHelperRemoteStage: '',
gopayHelperApiWaitingFor: '',
gopayHelperApiInputDeadlineAt: '',
gopayHelperApiInputWaitSeconds: 0,
gopayHelperLastInputError: '',
gopayHelperOtpInvalidCount: 0,
gopayHelperFailureStage: '',
gopayHelperFailureDetail: '',
gopayHelperTaskPayload: null,
gopayHelperTaskProgressSignature: '',
gopayHelperTaskProgressAt: 0,
gopayHelperTaskProgressTaskId: '',
gopayHelperBalance: '',
gopayHelperBalancePayload: null,
gopayHelperBalanceUpdatedAt: 0,
gopayHelperBalanceError: '',
gopayHelperRemainingUses: 0,
gopayHelperAutoModeEnabled: false,
gopayHelperApiKeyStatus: '',
gpcBaseUrl: DEFAULT_GPC_BASE_URL,
gpcCardKey: '',
gpcBalance: '',
gpcBalancePayload: null,
gpcBalanceUpdatedAt: 0,
gpcBalanceError: '',
gpcRemainingUses: 0,
gpcCardStatus: '',
gpcPageStatus: '',
gpcPageStatusText: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
operationDelayEnabled: true,
@@ -3396,98 +3360,42 @@ 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)
: String(value || '').trim();
case 'gopayHelperPin':
return self.GoPayUtils?.normalizeGoPayPin
? self.GoPayUtils.normalizeGoPayPin(value)
: String(value || '');
case 'gopayHelperCountryCode':
return self.GoPayUtils?.normalizeGoPayCountryCode
? self.GoPayUtils.normalizeGoPayCountryCode(value)
: String(value || '+86').trim();
case 'gopayHelperOtpChannel':
return self.GoPayUtils?.normalizeGpcOtpChannel
? self.GoPayUtils.normalizeGpcOtpChannel(value)
: (String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp');
case 'gopayHelperLocalSmsHelperUrl':
return normalizeLocalHttpBaseUrl(
value,
PERSISTED_SETTING_DEFAULTS.gopayHelperLocalSmsHelperUrl || 'http://127.0.0.1:18767'
);
case 'gopayHelperLocalSmsTimeoutSeconds':
return normalizeBoundedIntegerSetting(
value,
PERSISTED_SETTING_DEFAULTS.gopayHelperLocalSmsTimeoutSeconds,
10,
300
);
case 'gopayHelperLocalSmsPollIntervalSeconds':
return normalizeBoundedIntegerSetting(
value,
PERSISTED_SETTING_DEFAULTS.gopayHelperLocalSmsPollIntervalSeconds,
1,
30
);
case 'gopayHelperApiUrl':
case 'gpcBaseUrl':
{
const defaultGpcHelperApiUrl = PERSISTED_SETTING_DEFAULTS.gopayHelperApiUrl
|| (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gpc.qlhazycoder.top');
const normalizedGpcHelperApiUrl = self.GoPayUtils?.normalizeGpcHelperBaseUrl
? self.GoPayUtils.normalizeGpcHelperBaseUrl(value || defaultGpcHelperApiUrl)
: String(value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, '');
if (!self.GoPayUtils?.normalizeGpcHelperBaseUrl) {
const defaultGpcBaseUrl = PERSISTED_SETTING_DEFAULTS.gpcBaseUrl
|| (typeof DEFAULT_GPC_BASE_URL !== 'undefined' ? DEFAULT_GPC_BASE_URL : 'https://gpc.qlhazycoder.top');
const normalizedGpcBaseUrl = self.GoPayUtils?.normalizeGpcBaseUrl
? self.GoPayUtils.normalizeGpcBaseUrl(value || defaultGpcBaseUrl)
: String(value || defaultGpcBaseUrl).trim().replace(/\/+$/g, '');
if (!self.GoPayUtils?.normalizeGpcBaseUrl) {
try {
const parsed = new URL(normalizedGpcHelperApiUrl);
const parsed = new URL(normalizedGpcBaseUrl);
const hostname = parsed.hostname.toLowerCase();
if (hostname !== 'gpc.qlhazycoder.top' && hostname !== 'localhost' && hostname !== '127.0.0.1') {
return defaultGpcHelperApiUrl;
return defaultGpcBaseUrl;
}
} catch {
return defaultGpcHelperApiUrl;
return defaultGpcBaseUrl;
}
}
return normalizedGpcHelperApiUrl;
return normalizedGpcBaseUrl;
}
case 'gopayHelperApiKey':
case 'gopayHelperCardKey':
case 'gopayHelperReferenceId':
case 'gopayHelperGoPayGuid':
case 'gopayHelperRedirectUrl':
case 'gopayHelperNextAction':
case 'gopayHelperFlowId':
case 'gopayHelperChallengeId':
case 'gopayHelperTaskId':
case 'gopayHelperTaskStatus':
case 'gopayHelperStatusText':
case 'gopayHelperRemoteStage':
case 'gopayHelperApiWaitingFor':
case 'gopayHelperApiInputDeadlineAt':
case 'gopayHelperLastInputError':
case 'gopayHelperFailureStage':
case 'gopayHelperFailureDetail':
case 'gopayHelperBalance':
case 'gopayHelperBalanceError':
case 'gopayHelperApiKeyStatus':
case 'gpcCardKey':
return self.GoPayUtils?.normalizeGpcCardKey
? self.GoPayUtils.normalizeGpcCardKey(value)
: String(value || '').trim().toUpperCase();
case 'gpcBalance':
case 'gpcBalanceError':
case 'gpcCardStatus':
case 'gpcPageStatus':
case 'gpcPageStatusText':
return String(value || '').trim();
case 'gopayHelperBalancePayload':
case 'gopayHelperStartPayload':
case 'gopayHelperTaskPayload':
case 'gpcBalancePayload':
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
case 'gopayHelperBalanceUpdatedAt':
case 'gopayHelperApiInputWaitSeconds':
case 'gopayHelperOtpInvalidCount':
case 'gopayHelperRemainingUses':
case 'gpcBalanceUpdatedAt':
case 'gpcRemainingUses':
return Math.max(0, Number(value) || 0);
case 'autoRunSkipFailures':
case 'gopayHelperLocalSmsHelperEnabled':
case 'gopayHelperAutoModeEnabled':
return Boolean(value);
case 'operationDelayEnabled':
return true;
@@ -9637,7 +9545,7 @@ function getErrorMessage(error) {
return loggingStatus.getErrorMessage(error);
}
return String(typeof error === 'string' ? error : error?.message || '')
.replace(/^GPC_TASK_ENDED::/i, '')
.replace(/^GPC_PAGE_FLOW_ENDED::/i, '')
.replace(/^AUTO_RUN_STEP_IDLE_RESTART::/i, '');
}
@@ -9872,9 +9780,9 @@ function isPlusCheckoutNonFreeTrialFailure(error) {
return /PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格|该账号已经开通过\s*ChatGPT\s*订阅套餐,不能重复订阅(?:。)?(?:\s*checkout_order\s*|\(\s*checkout_order\s*\))?/i.test(message);
}
function isGpcTaskEndedFailure(error) {
function isGpcPageFlowEndedFailure(error) {
const message = String(typeof error === 'string' ? error : error?.message || '');
return /GPC_TASK_ENDED::/i.test(message);
return /GPC_PAGE_FLOW_ENDED::/i.test(message);
}
function isGpcCheckoutRestartRequiredFailure(error) {
@@ -9884,10 +9792,10 @@ function isGpcCheckoutRestartRequiredFailure(error) {
if (/PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(combinedMessage)) {
return false;
}
if (/GPC_TASK_ENDED::/i.test(rawMessage)) {
if (/GPC_PAGE_FLOW_ENDED::/i.test(rawMessage)) {
return true;
}
return /GPC\s*API\s*请求超时|GPC\s*任务状态超过\s*\d+\s*秒无进展|GPC[\s\S]*请重新创建任务|步骤\s*[67][\s\S]*GPC[\s\S]*(?:access\s*token|accessToken|任务轮询超时|请求超时|超时|timeout|timed\s*out|卡死|无响应|失败)|account\s+already\s+linked|GOPAY已经绑了订阅|(?:账号|账户|GoPay|GOPAY)[\s\S]*(?:已绑定|已经绑定|已绑|绑了订阅|绑定了订阅)|创建\s*GPC\s*订单失败[\s\S]*(?:任务已结束|任务结束|failed|expired|discarded|请求超时|timeout|timed\s*out)/i.test(message);
return /GPC\s*页面[\s\S]*(?:请重新准备|请求超时|超时|timeout|timed\s*out|卡死|无响应|失败|未检测到订阅完成|已尝试启动)|步骤\s*[67][\s\S]*GPC[\s\S]*(?:页面|请求超时|超时|timeout|timed\s*out|卡死|无响应|失败)|account\s+already\s+linked|GOPAY已经绑了订阅|(?:账号|账户|GoPay|GOPAY)[\s\S]*(?:已绑定|已经绑定|已绑|绑了订阅|绑定了订阅)/i.test(message);
}
function isPlusCheckoutRestartStep(step, stepExecutionKey = '', state = {}) {
@@ -10060,33 +9968,8 @@ function getDownstreamStateResets(step, state = {}) {
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
gopayHelperRedirectUrl: '',
gopayHelperNextAction: '',
gopayHelperFlowId: '',
gopayHelperChallengeId: '',
gopayHelperStartPayload: null,
gopayHelperTaskId: '',
gopayHelperTaskStatus: '',
gopayHelperStatusText: '',
gopayHelperRemoteStage: '',
gopayHelperApiWaitingFor: '',
gopayHelperApiInputDeadlineAt: '',
gopayHelperApiInputWaitSeconds: 0,
gopayHelperLastInputError: '',
gopayHelperOtpInvalidCount: 0,
gopayHelperFailureStage: '',
gopayHelperFailureDetail: '',
gopayHelperTaskPayload: null,
gopayHelperOrderCreatedAt: 0,
gopayHelperTaskProgressSignature: '',
gopayHelperTaskProgressAt: 0,
gopayHelperTaskProgressTaskId: '',
gopayHelperPinPayload: null,
gopayHelperResolvedOtp: '',
gopayHelperOtpRequestId: '',
gopayHelperOtpReferenceId: '',
gpcPageStatus: '',
gpcPageStatusText: '',
};
if (step <= 1) {
@@ -10185,10 +10068,8 @@ function getDownstreamStateResets(step, state = {}) {
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
gopayHelperResolvedOtp: '',
gopayHelperLastInputError: '',
gopayHelperOtpRequestId: '',
gopayHelperOtpReferenceId: '',
gpcPageStatus: '',
gpcPageStatusText: '',
} : {}),
...(isApprovalNode ? {
plusPaypalApprovedAt: null,
@@ -12581,49 +12462,44 @@ async function maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload = {}) {
return switchResult;
}
function resolveGpcHelperBaseUrl(apiUrl = '') {
if (self.GoPayUtils?.normalizeGpcHelperBaseUrl) {
return self.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl || DEFAULT_GPC_HELPER_API_URL);
function resolveGpcBaseUrl(apiUrl = '') {
if (self.GoPayUtils?.normalizeGpcBaseUrl) {
return self.GoPayUtils.normalizeGpcBaseUrl(apiUrl || DEFAULT_GPC_BASE_URL);
}
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
let normalized = String(apiUrl || DEFAULT_GPC_BASE_URL).trim().replace(/\/+$/g, '');
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
normalized = normalized.replace(/\/api\/gp\/tasks(?:\/[^/?#]+)?(?:\/(?:otp|pin|stop))?(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/web\/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_BASE_URL;
}
function buildGpcApiKeyBalanceRequestUrl(apiUrl = '') {
if (self.GoPayUtils?.buildGpcApiKeyBalanceUrl) {
return self.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl);
function normalizeGpcCardKey(value = '') {
if (self.GoPayUtils?.normalizeGpcCardKey) {
return self.GoPayUtils.normalizeGpcCardKey(value);
}
return String(value || '').trim().toUpperCase();
}
function isGpcCardKeyFormat(value = '') {
if (self.GoPayUtils?.isGpcCardKeyFormat) {
return self.GoPayUtils.isGpcCardKeyFormat(value);
}
return /^GPC-[A-F0-9]{8}-[A-F0-9]{8}-[A-F0-9]{8}$/.test(normalizeGpcCardKey(value));
}
function buildGpcCardBalanceRequestUrl(apiUrl = '', cardKey = '') {
if (self.GoPayUtils?.buildGpcCardBalanceUrl) {
return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl);
return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl, cardKey);
}
const baseUrl = resolveGpcHelperBaseUrl(apiUrl);
const baseUrl = resolveGpcBaseUrl(apiUrl);
if (!baseUrl) {
return '';
}
return `${baseUrl}/api/gp/balance`;
const normalizedCardKey = normalizeGpcCardKey(cardKey);
return `${baseUrl}/api/web/card/balance${normalizedCardKey ? `?card_key=${encodeURIComponent(normalizedCardKey)}` : ''}`;
}
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 = {}) {
function formatGpcCardBalancePayload(payload = {}) {
if (self.GoPayUtils?.formatGpcBalancePayload) {
return self.GoPayUtils.formatGpcBalancePayload(payload);
}
@@ -12643,28 +12519,26 @@ function formatGpcApiKeyBalancePayload(payload = {}) {
].filter(Boolean).join('');
}
async function refreshGpcApiKeyBalance(state = {}, options = {}) {
const apiUrl = resolveGpcHelperBaseUrl(state?.gopayHelperApiUrl || DEFAULT_GPC_HELPER_API_URL);
const apiKey = String(
state?.gopayHelperApiKey
|| state?.gpcApiKey
|| state?.apiKey
|| ''
).trim();
async function refreshGpcCardBalance(state = {}, options = {}) {
const apiUrl = resolveGpcBaseUrl(state?.gpcBaseUrl || DEFAULT_GPC_BASE_URL);
const cardKey = normalizeGpcCardKey(state?.gpcCardKey || state?.cardKey || '');
if (!apiUrl) {
throw new Error('缺少 GPC API 地址。');
throw new Error('缺少 GPC 页面地址。');
}
if (!apiKey) {
throw new Error('缺少 GPC API Key。');
if (!cardKey) {
throw new Error('缺少 GPC 卡密。');
}
const requestUrl = buildGpcApiKeyBalanceRequestUrl(apiUrl);
if (!isGpcCardKeyFormat(cardKey)) {
throw new Error('GPC 卡密格式不正确,应类似 GPC-6C9F1A32-45734795-914E6F00。');
}
const requestUrl = buildGpcCardBalanceRequestUrl(apiUrl, cardKey);
if (!requestUrl) {
throw new Error('缺少 GPC API 地址。');
throw new Error('缺少 GPC 卡密查询接口。');
}
const response = await fetch(requestUrl, {
method: 'GET',
headers: buildGpcApiKeyHeaders(apiKey, { Accept: 'application/json' }),
headers: { Accept: 'application/json' },
});
const rawText = await response.text();
let payload = {};
@@ -12682,26 +12556,19 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) {
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)
const cardStatus = self.GoPayUtils?.getGpcCardStatus
? self.GoPayUtils.getGpcCardStatus(balanceData)
: String(balanceData.status || balanceData.card_status || balanceData.cardStatus || '').trim();
const balanceText = formatGpcApiKeyBalancePayload(payload) || rawText || '未知';
const balanceText = formatGpcCardBalancePayload(payload) || rawText || '未知';
const updates = {
gopayHelperBalance: balanceText,
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,
gpcCardKey: cardKey,
gpcBalance: balanceText,
gpcBalancePayload: Object.keys(balanceData).length > 0 ? balanceData : { raw: String(balancePayload || '') },
gpcBalanceUpdatedAt: Date.now(),
gpcBalanceError: '',
gpcRemainingUses: Math.max(0, Number(remainingUses) || 0),
gpcCardStatus: cardStatus,
};
const flowId = String(balancePayload?.flow_id || balancePayload?.flowId || '').trim();
if (flowId) {
updates.gopayHelperFlowId = flowId;
}
const unifiedOk = self.GoPayUtils?.isGpcUnifiedResponseOk
? self.GoPayUtils.isGpcUnifiedResponseOk(payload)
@@ -12710,10 +12577,10 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) {
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, gpcBalanceError: String(detail || 'GPC 卡密查询失败') };
await setPersistentSettings(errorUpdates);
broadcastDataUpdate(errorUpdates);
throw new Error(String(detail || '余额查询失败'));
throw new Error(String(detail || 'GPC 卡密查询失败'));
}
await setPersistentSettings(updates);
@@ -12721,23 +12588,20 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) {
const reason = String(options?.reason || '').trim();
await addLog(
reason === 'round_success'
? `GPC 余额已更新:${balanceText}`
: `GPC 余额查询成功:${balanceText}`,
? `GPC 卡密剩余次数已更新:${balanceText}`
: `GPC 卡密查询成功:${balanceText}`,
'info'
);
return {
balance: balanceText,
payload,
data: updates.gopayHelperBalancePayload,
remainingUses: updates.gopayHelperRemainingUses,
autoModeEnabled: updates.gopayHelperAutoModeEnabled,
apiKeyStatus: updates.gopayHelperApiKeyStatus,
updatedAt: updates.gopayHelperBalanceUpdatedAt,
data: updates.gpcBalancePayload,
remainingUses: updates.gpcRemainingUses,
cardStatus: updates.gpcCardStatus,
updatedAt: updates.gpcBalanceUpdatedAt,
};
}
const refreshGpcCardBalance = refreshGpcApiKeyBalance;
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
addLog,
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
@@ -12764,7 +12628,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR
isAddPhoneAuthFailure,
isPhoneSmsPlatformRateLimitFailure,
isPlusCheckoutNonFreeTrialFailure,
isGpcTaskEndedFailure,
isGpcPageFlowEndedFailure,
isKiroProxyFailure,
isRestartCurrentAttemptError,
isStep4Route405RecoveryLimitFailure,
@@ -13538,10 +13402,10 @@ async function runAutoSequenceFromNodeGraph(startNodeId, context = {}) {
? gpcCheckoutRestartCount
: (isGoPayCheckoutStep ? goPayCheckoutRestartCount : plusCheckoutRestartCount);
const checkoutLabel = isGpcCheckoutStep
? 'GPC 任务'
? 'GPC 页面流程'
: (isGoPayCheckoutStep ? 'GoPay 订阅' : 'Plus Checkout');
const recreateLabel = isGpcCheckoutStep
? '重新创建 GPC 任务'
? '重新准备 GPC 页面'
: (isGoPayCheckoutStep ? '重新创建 GoPay 订阅' : '重新创建 Plus Checkout');
await addLog(
`节点 ${getNodeLabel(nodeId, latestState)}:检测到 ${checkoutLabel} 失败/卡住,准备回到节点 plus-checkout-create ${recreateLabel}(第 ${checkoutRestartCount} 次)。原因:${getErrorMessage(err)}`,
+11 -11
View File
@@ -25,7 +25,7 @@
getState,
hasSavedNodeProgress,
isAddPhoneAuthFailure,
isGpcTaskEndedFailure,
isGpcPageFlowEndedFailure,
isKiroProxyFailure,
isPhoneSmsPlatformRateLimitFailure,
isPlusCheckoutNonFreeTrialFailure,
@@ -806,9 +806,9 @@
&& isAddPhoneAuthFailure(err);
const blockedByPlusNonFreeTrial = typeof isPlusCheckoutNonFreeTrialFailure === 'function'
&& isPlusCheckoutNonFreeTrialFailure(err);
const blockedByGpcTaskEnded = typeof isGpcTaskEndedFailure === 'function'
? isGpcTaskEndedFailure(err)
: /GPC_TASK_ENDED::/i.test(err?.message || String(err || ''));
const blockedByGpcPageFlowEnded = typeof isGpcPageFlowEndedFailure === 'function'
? isGpcPageFlowEndedFailure(err)
: /GPC_PAGE_FLOW_ENDED::/i.test(err?.message || String(err || ''));
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
&& !keepSameEmailUntilAddPhone
&& isSignupUserAlreadyExistsFailure(err);
@@ -819,7 +819,7 @@
const canRetry = !blockedByAddPhone
&& !blockedByPhoneNoSupply
&& !blockedByPlusNonFreeTrial
&& !blockedByGpcTaskEnded
&& !blockedByGpcPageFlowEnded
&& !blockedBySignupUserAlreadyExists
&& !blockedByStep4Route405
&& !blockedByKiroProxy
@@ -935,18 +935,18 @@
break;
}
if (blockedByGpcTaskEnded) {
if (blockedByGpcPageFlowEnded) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因 GPC 任务已结束。');
cancelPendingCommands('当前轮因 GPC 页面流程已结束。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮 GPC 任务已结束,自动重试未开启,当前自动运行将停止。`,
`${targetRun}/${totalRuns} 轮 GPC 页面流程已结束,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
@@ -959,11 +959,11 @@
break;
}
await addLog(`${targetRun}/${totalRuns} 轮 GPC 任务已结束,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(`${targetRun}/${totalRuns} 轮 GPC 页面流程已结束,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,已无后续轮次,本次自动运行结束。`,
? `${targetRun}/${totalRuns} 轮因 GPC 页面流程结束提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因 GPC 页面流程结束提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
+1 -1
View File
@@ -92,7 +92,7 @@
function getErrorMessage(error) {
return String(typeof error === 'string' ? error : error?.message || '')
.replace(/^GPC_TASK_ENDED::/i, '')
.replace(/^GPC_PAGE_FLOW_ENDED::/i, '')
.replace(/^AUTO_RUN_STEP_IDLE_RESTART::/i, '');
}
+2 -20
View File
@@ -1181,7 +1181,6 @@
const requestId = String(message.payload?.requestId || '').trim();
const currentRequestId = String(currentState?.plusManualConfirmationRequestId || '').trim();
const method = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase();
const isGpcOtp = method === 'gopay-otp';
if (!currentState?.plusManualConfirmationPending) {
return { ok: true, ignored: true };
}
@@ -1198,23 +1197,6 @@
plusManualConfirmationMessage: '',
};
if (isGpcOtp && confirmed) {
const otp = String(message.payload?.otp || message.payload?.code || '').trim().replace(/[^\d]/g, '');
if (!otp) {
throw new Error('请输入 GPC OTP 验证码。');
}
const otpUpdates = {
...clearManualConfirmationState,
gopayHelperResolvedOtp: otp,
};
await setState(otpUpdates);
if (typeof broadcastDataUpdate === 'function') {
broadcastDataUpdate(otpUpdates);
}
await addLog(`步骤 ${step}:已收到 GPC OTP,准备提交验证。`, 'ok');
return { ok: true };
}
await setState(clearManualConfirmationState);
if (typeof broadcastDataUpdate === 'function') {
broadcastDataUpdate(clearManualConfirmationState);
@@ -1232,7 +1214,7 @@
const cancelMessage = method === 'gopay'
? '已取消 GoPay 订阅确认'
: (isGpcOtp ? '已取消 GPC OTP 输入' : '已取消当前手动确认');
: '已取消当前手动确认';
await setNodeStatus(confirmationNodeId, 'failed');
await addLog(`步骤 ${step}${cancelMessage}`, 'warn');
await appendManualAccountRunRecordIfNeeded(
@@ -1688,7 +1670,7 @@
case 'REFRESH_GPC_CARD_BALANCE': {
if (typeof refreshGpcCardBalance !== 'function') {
throw new Error('GPC API Key 余额查询能力尚未接入。');
throw new Error('GPC 卡密查询能力尚未接入。');
}
const state = await getState();
const result = await refreshGpcCardBalance({
+1 -1
View File
@@ -92,7 +92,7 @@
function getErrorMessage(error) {
return String(typeof error === 'string' ? error : error?.message || '')
.replace(/^GPC_TASK_ENDED::/i, '')
.replace(/^GPC_PAGE_FLOW_ENDED::/i, '')
.replace(/^AUTO_RUN_STEP_IDLE_RESTART::/i, '');
}
+2 -27
View File
@@ -58,33 +58,8 @@
'plusManualConfirmationMethod',
'plusManualConfirmationTitle',
'plusManualConfirmationMessage',
'gopayHelperReferenceId',
'gopayHelperGoPayGuid',
'gopayHelperRedirectUrl',
'gopayHelperNextAction',
'gopayHelperFlowId',
'gopayHelperChallengeId',
'gopayHelperStartPayload',
'gopayHelperTaskId',
'gopayHelperTaskStatus',
'gopayHelperStatusText',
'gopayHelperRemoteStage',
'gopayHelperApiWaitingFor',
'gopayHelperApiInputDeadlineAt',
'gopayHelperApiInputWaitSeconds',
'gopayHelperLastInputError',
'gopayHelperOtpInvalidCount',
'gopayHelperFailureStage',
'gopayHelperFailureDetail',
'gopayHelperTaskPayload',
'gopayHelperOrderCreatedAt',
'gopayHelperTaskProgressSignature',
'gopayHelperTaskProgressAt',
'gopayHelperTaskProgressTaskId',
'gopayHelperPinPayload',
'gopayHelperResolvedOtp',
'gopayHelperOtpRequestId',
'gopayHelperOtpReferenceId',
'gpcPageStatus',
'gpcPageStatusText',
]),
phoneVerification: Object.freeze([
'currentPhoneActivation',
+2 -2
View File
@@ -28,7 +28,7 @@
| 06 | PayPal 注册与绑卡使用教程 | `PayPal`、注册、绑卡、钱包、身份认证、右上角通知 | `docs/使用教程/分部分/06-PayPal-注册与绑卡使用教程.md` |
| 07 | ChatGPT Plus 订阅说明(待人工审核) | `ChatGPT Plus`、订阅说明、支付流程说明、人工审核 | `docs/使用教程/分部分/07-0元试用-ChatGPT-Plus-教程.md` |
| 08 | Clash Verge 非港轮询配置 | `Clash Verge``非港轮询`、扩展脚本、规则模式、系统代理 | `docs/使用教程/分部分/08-Clash-Verge-非港轮询配置.md` |
| 09 | GPC 卡密与 API 使用说明 | `GPC`、充值卡密、`API` 卡密、`API Key`、自动化扩展、短信 `Helper` | `docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md` |
| 09 | GPC 卡密页面充值说明 | `GPC`、充值卡密、卡密余额、网页充值、`ChatGPT Plus` | `docs/使用教程/分部分/09-GPC-卡密页面充值说明.md` |
---
@@ -69,7 +69,7 @@
3. 根据你当前使用的邮箱方案,选择 `03``04``05`
4. 如果需要支付和订阅,再看 `06``07`
5. 如果需要代理配置,再看 `08`
6. 如果需要 `GPC` 卡密`API Key` 或自动化接入,再看 `09`
6. 如果需要使用 `GPC` 卡密网页充值,再看 `09`
---
@@ -78,7 +78,7 @@ AI 维护时默认按下面的范围归类:
| 06 | PayPal 注册与绑卡使用教程 | `PayPal`、注册、绑卡、钱包、身份认证、右上角通知 | `docs/使用教程/分部分/06-PayPal-注册与绑卡使用教程.md` |
| 07 | ChatGPT Plus 订阅说明(待人工审核) | `ChatGPT Plus`、订阅说明、支付流程说明、人工审核 | `docs/使用教程/分部分/07-0元试用-ChatGPT-Plus-教程.md` |
| 08 | Clash Verge 非港轮询配置 | `Clash Verge``非港轮询`、扩展脚本、规则模式、系统代理 | `docs/使用教程/分部分/08-Clash-Verge-非港轮询配置.md` |
| 09 | GPC 卡密与 API 使用说明 | `GPC`、充值卡密、`API` 卡密、`API Key`、自动化扩展、短信 `Helper` | `docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md` |
| 09 | GPC 卡密页面充值说明 | `GPC`、充值卡密、卡密余额、网页充值、`ChatGPT Plus` | `docs/使用教程/分部分/09-GPC-卡密页面充值说明.md` |
---
@@ -1,129 +0,0 @@
# 第九部分: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` 已正确启用
@@ -0,0 +1,66 @@
# 第九部分:GPC 卡密页面充值说明
## 部分信息
- `section_slug`: `gpc-card-page-recharge`
- `适用主题`: `GPC`、充值卡密、卡密余额、网页充值、`ChatGPT Plus`
- `维护方式`: `直接更新本文件`
## 适用场景
- 需要在扩展中使用 `GPC` 卡密开通 `ChatGPT Plus`
- 需要自动打开 `GPC` 页面、填写卡密和 ChatGPT session
- 需要在充值前查看卡密格式、状态和剩余次数
## 准备内容
- 一个格式类似 `GPC-6C9F1A32-45734795-914E6F00``GPC` 卡密
- 当前浏览器中可访问 [https://gpc.qlhazycoder.top/](https://gpc.qlhazycoder.top/)
- 当前 ChatGPT 账号已经登录,并能正常访问 `https://chatgpt.com/api/auth/session`
- 如果还没有卡密,可以通过侧边栏的 `购买卡密` 按钮打开购买入口
## 扩展中的配置
1. 打开侧边栏并启用 `Plus 模式`
2.`Plus 支付` 中选择 `GPC`
3.`GPC 卡密` 输入框填写卡密
4. 停止输入 1 秒后,扩展会自动检查格式并查询剩余次数
5. 状态显示正常后,再启动自动流程或手动执行 GPC 步骤
## 自动流程
### 第六步:打开 GPC 页面并准备
扩展会打开或复用 `GPC` 页面,确认页面中的卡密输入框内容和侧边栏保存的卡密一致;如果不一致,会直接覆盖为当前卡密。
同一步还会异步读取 ChatGPT session 的完整 JSON 内容,并填入 `GPC` 页面左下角的文本框;如果页面里已有旧内容,也会覆盖为本轮读取到的 session。
准备完成后,流程进入下一步。
### 第七步:启动并等待 GPC 完成
扩展会点击 `开始plus充值`,页面按钮会进入 `任务进行中` 状态。随后扩展持续读取右侧日志和按钮文案:
- 如果日志出现 `订阅完成`,并且按钮恢复为 `开始plus充值`,本轮 GPC 充值完成
- 如果没有出现 `订阅完成`,但按钮恢复为 `开始plus充值`,扩展会再次点击启动
- 如果日志出现 `该账户没有试用资格`,当前轮直接失败
- 如果长时间没有完成,会按后台超时规则结束当前 GPC 页面流程
## 常见问题
### 卡密格式不正确怎么办?
扩展只接受 `GPC-XXXXXXXX-XXXXXXXX-XXXXXXXX` 这种格式,其中 `X` 为大写或小写的十六进制字符。输入后扩展会自动转成大写。
### 为什么没有自动查询余额?
余额检测会在停止输入约 1 秒后触发。没有填写卡密、格式不正确、页面接口不可用或网络失败时,状态区会显示对应错误。
### GPC 页面已有旧卡密或旧 session 会怎样?
第六步会以侧边栏当前保存的卡密和本轮读取到的 ChatGPT session 为准,页面已有内容会被覆盖。
## 注意事项
- 当前扩展只保留 `GPC` 网页充值链路和卡密余额检测
- 充值是否成功以 `GPC` 页面日志中的 `订阅完成` 为准
@@ -15,9 +15,8 @@
const LOCAL_CHECKOUT_PROXY_SETTINGS_SCOPE = 'regular';
const LOCAL_CHECKOUT_PROXY_TIMEOUT_MS = 1200;
const LOCAL_CHECKOUT_PROXY_SETTLE_MS = 350;
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 DEFAULT_GPC_BASE_URL = 'https://gpc.qlhazycoder.top';
const GPC_PORTAL_URL = 'https://gpc.qlhazycoder.top/';
const HOSTED_CHECKOUT_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz';
const HOSTED_CHECKOUT_SUCCESS_URL_PATTERN = /^https:\/\/(?:chatgpt\.com|www\.chatgpt\.com|chat\.openai\.com)\/(?:backend-api\/)?payments\/success(?:[/?#]|$)/i;
const HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS = 120000;
@@ -1221,208 +1220,16 @@ function FindProxyForURL(url, host) {
}
}
function normalizeHelperCountryCode(countryCode = '86') {
const digits = String(countryCode || '').replace(/\D/g, '');
return digits || '86';
}
function normalizeHelperPhoneNumber(phone = '', countryCode = '86') {
const cleaned = String(phone || '').replace(/\D/g, '');
const countryDigits = normalizeHelperCountryCode(countryCode);
if (countryDigits && cleaned.startsWith(countryDigits) && cleaned.length > countryDigits.length) {
return cleaned.slice(countryDigits.length);
}
return cleaned;
}
function normalizeGpcHelperPhoneMode(value = '') {
function normalizeGpcBaseUrl(apiUrl = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
if (rootScope.GoPayUtils?.normalizeGpcBaseUrl) {
return rootScope.GoPayUtils.normalizeGpcBaseUrl(apiUrl);
}
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) {
return rootScope.GoPayUtils.normalizeGpcOtpChannel(value);
}
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
}
function resolveGpcHelperApiKey(state = {}) {
const apiKey = String(
state?.gopayHelperApiKey
|| state?.gpcApiKey
|| state?.apiKey
|| ''
).trim();
if (!apiKey) {
throw new Error('创建 GPC 订单失败:缺少 API Key。');
}
return apiKey;
}
function normalizeGpcHelperBaseUrl(apiUrl = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperBaseUrl) {
return rootScope.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl);
}
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
let normalized = String(apiUrl || DEFAULT_GPC_BASE_URL).trim().replace(/\/+$/g, '');
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
normalized = normalized.replace(/\/api\/gp\/tasks(?:\/[^/?#]+)?(?:\/(?:otp|pin|stop))?(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/web\/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;
}
function buildGpcHelperApiUrl(apiUrl = '', path = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcHelperApiUrl) {
return rootScope.GoPayUtils.buildGpcHelperApiUrl(apiUrl, path);
}
const baseUrl = normalizeGpcHelperBaseUrl(apiUrl);
if (!baseUrl) {
return '';
}
const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${String(path || '')}`;
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 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) {
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}`;
}
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 normalizeGpcAutoModePermissionValue(value) {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
if (value === 1) return true;
if (value === 0) return false;
}
const normalized = String(value ?? '').trim().toLowerCase();
if (!normalized) {
return null;
}
if (['true', '1', 'yes', 'y', 'on', 'enabled', 'enable'].includes(normalized)) {
return true;
}
if (['false', '0', 'no', 'n', 'off', 'disabled', 'disable'].includes(normalized)) {
return false;
}
return null;
}
function getGpcAutoModePermission(payload = {}) {
const data = unwrapGpcResponse(payload);
if (!data || typeof data !== 'object' || Array.isArray(data)) {
return null;
}
return normalizeGpcAutoModePermissionValue(
data.auto_mode_enabled
?? data.autoModeEnabled
?? data.auto_enabled
?? data.autoEnabled
);
}
function isGpcAutoModePermissionDenied(payload = {}) {
return getGpcAutoModePermission(payload) === false;
}
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 && isGpcAutoModePermissionDenied(balanceData)) {
throw new Error('创建 GPC 订单失败:当前 GPC API Key 未开通自动模式。');
}
return normalized || DEFAULT_GPC_BASE_URL;
}
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
@@ -1430,13 +1237,13 @@ function FindProxyForURL(url, host) {
? fetchImpl
: (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
if (typeof fetcher !== 'function') {
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
throw new Error('当前运行环境不支持 fetch,无法调用远端接口。');
}
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000);
let didTimeout = false;
let timer = null;
const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`);
const buildTimeoutError = () => new Error(`远端接口请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`);
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
didTimeout = true;
@@ -1466,13 +1273,100 @@ function FindProxyForURL(url, host) {
}
}
async function readAccessTokenFromChatGptSessionTab(tabId) {
function resolveGpcCardKey(state = {}) {
const cardKey = String(state?.gpcCardKey || '').trim();
if (!cardKey) {
throw new Error('步骤 6:GPC 模式缺少卡密,请先在侧边栏填写 GPC 卡密。');
}
return cardKey;
}
function buildGpcPortalUrl(state = {}) {
const baseUrl = normalizeGpcBaseUrl(state?.gpcBaseUrl || DEFAULT_GPC_BASE_URL)
.replace(/\/+$/g, '');
return `${baseUrl || DEFAULT_GPC_BASE_URL}/`;
}
function isGpcPortalUrl(url = '') {
const text = String(url || '').trim();
if (!text) {
return false;
}
try {
const parsed = new URL(text);
return parsed.hostname === 'gpc.qlhazycoder.top';
} catch {
return /^https:\/\/gpc\.qlhazycoder\.top(?:\/|$)/i.test(text);
}
}
async function findOpenGpcPortalTabId(portalUrl = GPC_PORTAL_URL) {
const queryTabs = typeof queryTabsInAutomationWindow === 'function'
? queryTabsInAutomationWindow
: (chrome?.tabs?.query ? (queryInfo) => chrome.tabs.query(queryInfo) : null);
if (typeof queryTabs !== 'function') {
return 0;
}
const tabs = await queryTabs({}).catch(() => []);
const candidates = (Array.isArray(tabs) ? tabs : [])
.filter((tab) => Number.isInteger(tab?.id) && isGpcPortalUrl(tab.url || ''));
if (!candidates.length) {
return 0;
}
const match = candidates.find((tab) => tab.active && tab.currentWindow)
|| candidates.find((tab) => tab.active)
|| candidates[0];
if (match?.id && chrome?.tabs?.update) {
await chrome.tabs.update(match.id, { url: portalUrl, active: true }).catch(() => (
chrome.tabs.update(match.id, { active: true }).catch(() => {})
));
}
return match?.id || 0;
}
async function openOrReuseGpcPortalTab(state = {}) {
const portalUrl = buildGpcPortalUrl(state);
const storedTab = await getTabById(Number(state?.plusCheckoutTabId) || 0);
if (storedTab?.id && isGpcPortalUrl(storedTab.url || '')) {
if (chrome?.tabs?.update) {
await chrome.tabs.update(storedTab.id, { url: portalUrl, active: true }).catch(() => (
chrome.tabs.update(storedTab.id, { active: true }).catch(() => {})
));
}
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, storedTab.id);
}
return { tabId: storedTab.id, portalUrl };
}
const existingTabId = await findOpenGpcPortalTabId(portalUrl);
if (existingTabId) {
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, existingTabId);
}
return { tabId: existingTabId, portalUrl };
}
const tab = typeof createAutomationTab === 'function'
? await createAutomationTab({ url: portalUrl, active: true })
: await chrome.tabs.create({ url: portalUrl, active: true });
const tabId = Number(tab?.id);
if (!Number.isInteger(tabId)) {
throw new Error('步骤 6:打开 GPC 页面失败。');
}
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, tabId);
}
return { tabId, portalUrl };
}
async function readSessionFromChatGptSessionTab(tabId) {
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
inject: PLUS_CHECKOUT_INJECT_FILES,
injectSource: PLUS_CHECKOUT_SOURCE,
logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续获取 accessToken...',
logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续获取 session...',
});
const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
@@ -1486,125 +1380,148 @@ function FindProxyForURL(url, host) {
if (sessionResult?.error) {
throw new Error(sessionResult.error);
}
return String(sessionResult?.accessToken || sessionResult?.session?.accessToken || '').trim();
const session = sessionResult?.session && typeof sessionResult.session === 'object'
? sessionResult.session
: null;
const accessToken = String(sessionResult?.accessToken || session?.accessToken || '').trim();
if (!session || !accessToken) {
throw new Error('步骤 6GPC 模式获取 ChatGPT session 失败。');
}
return session;
}
async function generateGpcCheckoutFromApi(accessToken = '', state = {}) {
const token = String(accessToken || '').trim();
if (!token) {
throw new Error('创建 GPC 订单失败:缺少 accessToken。');
}
const apiUrl = buildGpcTaskCreateUrl(state?.gopayHelperApiUrl);
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 (!isAutoMode && !phoneNumber) {
throw new Error('创建 GPC 订单失败:手动模式缺少手机号。');
}
if (!isAutoMode && !pin) {
throw new Error('创建 GPC 订单失败:手动模式缺少 PIN。');
async function prepareGpcPortalPage(tabId, cardKey = '', sessionJson = '') {
if (!chrome?.scripting?.executeScript) {
throw new Error('步骤 6:当前运行环境不支持脚本注入,无法填写 GPC 页面。');
}
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(800);
const results = await chrome.scripting.executeScript({
target: { tabId },
func: (rawCardKey, rawSessionJson) => {
const textOf = (element) => String(element?.innerText || element?.textContent || element?.value || '').replace(/\s+/g, ' ').trim();
const isVisible = (element) => {
if (!element) return false;
const style = window.getComputedStyle ? window.getComputedStyle(element) : null;
if (style && (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0)) {
return false;
}
const rect = typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : null;
return !rect || rect.width > 0 || rect.height > 0;
};
const dispatchInput = (element, value) => {
element.focus?.();
element.value = value;
element.dispatchEvent?.(new Event('input', { bubbles: true }));
element.dispatchEvent?.(new Event('change', { bubbles: true }));
element.blur?.();
};
const clickElement = (element) => {
element.scrollIntoView?.({ block: 'center', inline: 'center' });
element.click?.();
};
const modeButtons = Array.from(document.querySelectorAll('button, [role="button"], .design-mode-card, .mode-card'));
const cardModeButton = modeButtons.find((element) => /卡密充值/.test(textOf(element)));
if (cardModeButton && !/\bactive\b/i.test(String(cardModeButton.className || ''))) {
clickElement(cardModeButton);
} else if (cardModeButton) {
clickElement(cardModeButton);
}
throwIfStopped();
await assertGpcApiKeyReadyForCreate(state, phoneMode, apiKey);
throwIfStopped();
const payload = {
access_token: token,
phone_mode: phoneMode,
};
if (!isAutoMode) {
payload.country_code = countryCode;
payload.phone_number = normalizeHelperPhoneNumber(phoneNumber, countryCode);
payload.otp_channel = normalizeGpcOtpChannel(state?.gopayHelperOtpChannel);
}
const compactCardKey = String(rawCardKey || '').trim().replace(/\s+/g, '');
const explicitSegments = compactCardKey.includes('-') || compactCardKey.includes('_')
? compactCardKey.split(/[-_]+/).filter(Boolean)
: [];
const cardSegments = explicitSegments.length
? explicitSegments
: (compactCardKey.match(/.{1,8}/g) || []);
const cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]'))
.filter(isVisible);
cardInputs.forEach((input, index) => {
dispatchInput(input, cardSegments[index] || '');
});
if (!cardInputs.length) {
const fallbackInput = Array.from(document.querySelectorAll('input')).find((input) => /卡密|card/i.test([
input.placeholder,
input.name,
input.id,
input.className,
].join(' ')));
if (fallbackInput) {
dispatchInput(fallbackInput, compactCardKey);
}
}
const orderCreatedAt = Date.now();
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
method: 'POST',
headers: {
Accept: '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Content-Type': 'application/json',
'X-API-Key': apiKey,
const sessionTextarea = Array.from(document.querySelectorAll('textarea')).find((textarea) => /session|accessToken|完整/i.test([
textarea.placeholder,
textarea.name,
textarea.id,
textarea.className,
].join(' '))) || document.querySelector('textarea.design-session-input') || document.querySelector('textarea');
if (!sessionTextarea) {
throw new Error('未找到 GPC session 输入框。');
}
dispatchInput(sessionTextarea, String(rawSessionJson || ''));
const startButton = Array.from(document.querySelectorAll('button, [role="button"]'))
.find((button) => /开始\s*Plus\s*充值|任务进行中/.test(textOf(button)));
return {
ok: true,
cardInputCount: cardInputs.length,
cardSegments: cardSegments.map((segment) => segment ? segment.length : 0),
sessionLength: String(sessionTextarea.value || '').length,
startButtonText: textOf(startButton),
activeModeText: textOf(cardModeButton),
url: location.href,
};
},
body: JSON.stringify(payload),
}, 30000);
const taskData = unwrapGpcResponse(data);
const taskId = String(taskData?.task_id || taskData?.taskId || '').trim();
if (!response?.ok || !isGpcUnifiedResponseOk(data) || !taskId) {
const detail = getGpcResponseErrorDetail(data, response?.status || 0);
throw new Error(`创建 GPC 订单失败:${detail}`);
args: [cardKey, sessionJson],
});
const result = results?.[0]?.result || {};
if (!result?.ok) {
throw new Error('步骤 6GPC 页面准备失败。');
}
return {
taskId,
taskStatus: String(taskData?.status || '').trim(),
statusText: String(taskData?.status_text || taskData?.statusText || '').trim(),
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,
};
return result;
}
async function executeGpcCheckoutCreate(state = {}) {
let accessToken = String(state?.contributionAccessToken || state?.accessToken || state?.chatgptAccessToken || '').trim();
if (!accessToken) {
await addLog('步骤 6:正在获取 accessToken...', 'info');
const tokenTabId = await openFreshChatGptTabForCheckoutCreate();
try {
accessToken = await readAccessTokenFromChatGptSessionTab(tokenTabId);
} finally {
if (chrome?.tabs?.remove && Number.isInteger(tokenTabId)) {
await chrome.tabs.remove(tokenTabId).catch(() => {});
}
const cardKey = resolveGpcCardKey(state);
await addLog('步骤 6:正在打开 GPC 页面并准备卡密充值模式...', 'info');
const { tabId, portalUrl } = await openOrReuseGpcPortalTab(state);
await addLog('步骤 6:正在从 ChatGPT 获取完整 session...', 'info');
const sessionTabId = await openFreshChatGptTabForCheckoutCreate();
let session = null;
try {
session = await readSessionFromChatGptSessionTab(sessionTabId);
} finally {
if (chrome?.tabs?.remove && Number.isInteger(sessionTabId)) {
await chrome.tabs.remove(sessionTabId).catch(() => {});
}
}
if (!accessToken) {
throw new Error('步骤 6GPC 模式获取 accessToken 失败。');
}
await addLog('步骤 6:正在调用 GPC 接口创建订单...', 'info');
const result = await generateGpcCheckoutFromApi(accessToken, state);
const sessionJson = JSON.stringify(session);
if (chrome?.tabs?.update) {
await chrome.tabs.update(tabId, { active: true }).catch(() => {});
}
await addLog('步骤 6:正在填写 GPC 卡密和 ChatGPT session...', 'info');
const prepared = await prepareGpcPortalPage(tabId, cardKey, sessionJson);
await setState({
plusCheckoutTabId: null,
plusCheckoutUrl: '',
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
plusCheckoutSource: result.checkoutSource,
gopayHelperTaskId: result.taskId,
gopayHelperTaskStatus: result.taskStatus,
gopayHelperStatusText: result.statusText,
gopayHelperRemoteStage: result.remoteStage,
gopayHelperTaskPayload: result.responsePayload,
gopayHelperTaskProgressSignature: '',
gopayHelperTaskProgressAt: 0,
gopayHelperTaskProgressTaskId: result.taskId,
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
gopayHelperRedirectUrl: '',
gopayHelperNextAction: '',
gopayHelperFlowId: '',
gopayHelperChallengeId: '',
gopayHelperStartPayload: null,
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
plusCheckoutTabId: tabId,
plusCheckoutUrl: portalUrl,
plusCheckoutCountry: 'US',
plusCheckoutCurrency: 'USD',
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
gpcPageStatus: 'prepared',
gpcPageStatusText: '页面已准备',
});
await addLog(`步骤 6GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
await addLog(
`步骤 6:GPC 页面已准备完成(卡密 ${prepared.cardInputCount || 0} 段,session ${prepared.sessionLength || 0} 字符),准备继续下一步。`,
'ok'
);
await completeNodeFromBackground('plus-checkout-create', {
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
plusCheckoutSource: result.checkoutSource,
plusCheckoutCountry: 'US',
plusCheckoutCurrency: 'USD',
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
});
}
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -2479,7 +2479,7 @@
"id": 6,
"order": 60,
"key": "plus-checkout-create",
"title": "创建 GPC 订单",
"title": "打开 GPC 页面并准备",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-create",
@@ -2489,7 +2489,7 @@
"id": 7,
"order": 70,
"key": "plus-checkout-billing",
"title": "等待 GPC 任务完成",
"title": "启动并等待 GPC 完成",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-billing",
@@ -2603,7 +2603,7 @@
"id": 6,
"order": 60,
"key": "plus-checkout-create",
"title": "创建 GPC 订单",
"title": "打开 GPC 页面并准备",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-create",
@@ -2613,7 +2613,7 @@
"id": 7,
"order": 70,
"key": "plus-checkout-billing",
"title": "等待 GPC 任务完成",
"title": "启动并等待 GPC 完成",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-billing",
@@ -2686,7 +2686,7 @@
"id": 6,
"order": 60,
"key": "plus-checkout-create",
"title": "创建 GPC 订单",
"title": "打开 GPC 页面并准备",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-create",
@@ -2696,7 +2696,7 @@
"id": 7,
"order": 70,
"key": "plus-checkout-billing",
"title": "等待 GPC 任务完成",
"title": "启动并等待 GPC 完成",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-billing",
@@ -2769,7 +2769,7 @@
"id": 6,
"order": 60,
"key": "plus-checkout-create",
"title": "创建 GPC 订单",
"title": "打开 GPC 页面并准备",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-create",
@@ -2779,7 +2779,7 @@
"id": 7,
"order": 70,
"key": "plus-checkout-billing",
"title": "等待 GPC 任务完成",
"title": "启动并等待 GPC 完成",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-billing",
@@ -2904,7 +2904,7 @@
"id": 6,
"order": 60,
"key": "plus-checkout-create",
"title": "创建 GPC 订单",
"title": "打开 GPC 页面并准备",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-create",
@@ -2914,7 +2914,7 @@
"id": 7,
"order": 70,
"key": "plus-checkout-billing",
"title": "等待 GPC 任务完成",
"title": "启动并等待 GPC 完成",
"sourceId": "plus-checkout",
"driverId": "flows/openai/content/plus-checkout",
"command": "plus-checkout-billing",
+37 -156
View File
@@ -6,10 +6,8 @@
const PLUS_PAYMENT_METHOD_NONE = 'none';
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';
const DEFAULT_GPC_BASE_URL = 'https://gpc.qlhazycoder.top';
const ALLOWED_GPC_REMOTE_HOST = 'gpc.qlhazycoder.top';
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
@@ -57,13 +55,6 @@
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;
@@ -72,6 +63,14 @@
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
}
function normalizeGpcCardKey(value = '') {
return String(value || '').trim().toUpperCase();
}
function isGpcCardKeyFormat(value = '') {
return /^GPC-[A-F0-9]{8}-[A-F0-9]{8}-[A-F0-9]{8}$/.test(normalizeGpcCardKey(value));
}
function unwrapGpcBalancePayload(payload = {}) {
const data = unwrapGpcResponse(payload);
if (!data || typeof data !== 'object' || Array.isArray(data)) {
@@ -85,13 +84,13 @@
'uses',
'available_uses',
'availableUses',
'auto_mode_enabled',
'autoModeEnabled',
'auto_enabled',
'autoEnabled',
'status',
'card_status',
'cardStatus',
'card_type',
'cardType',
'expires_at',
'expiresAt',
].some((key) => Object.prototype.hasOwnProperty.call(data, key));
if (!hasBalanceFields && data.data && typeof data.data === 'object' && !Array.isArray(data.data)) {
return data.data;
@@ -115,20 +114,7 @@
);
}
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 = {}) {
function getGpcCardStatus(payload = {}) {
const data = unwrapGpcBalancePayload(payload);
if (!data || typeof data !== 'object') {
return '';
@@ -136,41 +122,30 @@
return String(data.status || data.card_status || data.cardStatus || '').trim();
}
function normalizeGpcOtpChannel(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'sms') {
return 'sms';
}
return 'whatsapp';
}
function normalizeGpcHelperBaseUrl(apiUrl = '') {
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim();
function normalizeGpcBaseUrl(apiUrl = '') {
let normalized = String(apiUrl || DEFAULT_GPC_BASE_URL).trim();
if (!normalized) {
return DEFAULT_GPC_HELPER_API_URL;
return DEFAULT_GPC_BASE_URL;
}
normalized = normalized.replace(/\/+$/g, '');
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
normalized = normalized.replace(/\/api\/gp\/tasks(?:\/[^/?#]+)?(?:\/(?:otp|pin|stop))?(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/web\/card\/balance(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, '');
try {
const parsed = new URL(normalized);
const hostname = parsed.hostname.toLowerCase();
if (hostname === ALLOWED_GPC_HELPER_REMOTE_HOST || hostname === 'localhost' || hostname === '127.0.0.1') {
return normalized || DEFAULT_GPC_HELPER_API_URL;
if (hostname === ALLOWED_GPC_REMOTE_HOST || hostname === 'localhost' || hostname === '127.0.0.1') {
return normalized || DEFAULT_GPC_BASE_URL;
}
return DEFAULT_GPC_HELPER_API_URL;
return DEFAULT_GPC_BASE_URL;
} catch {
return DEFAULT_GPC_HELPER_API_URL;
return DEFAULT_GPC_BASE_URL;
}
}
function buildGpcHelperApiUrl(apiUrl = '', path = '') {
const baseUrl = normalizeGpcHelperBaseUrl(apiUrl);
function buildGpcApiUrl(apiUrl = '', path = '') {
const baseUrl = normalizeGpcBaseUrl(apiUrl);
if (!baseUrl) {
return '';
}
@@ -178,42 +153,13 @@
return `${baseUrl}${normalizedPath}`;
}
function buildGpcApiKeyBalanceUrl(apiUrl = '') {
return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance');
}
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;
function buildGpcCardBalanceUrl(apiUrl = '', cardKey = '') {
const baseUrl = buildGpcApiUrl(apiUrl, '/api/web/card/balance');
const normalizedCardKey = normalizeGpcCardKey(cardKey);
if (!baseUrl || !normalizedCardKey) {
return baseUrl;
}
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}`);
return `${baseUrl}?card_key=${encodeURIComponent(normalizedCardKey)}`;
}
function unwrapGpcResponse(payload = {}) {
@@ -329,56 +275,6 @@
return status ? `HTTP ${status}` : '未知错误';
}
function buildGpcOtpPayload(input = {}) {
const payload = {
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''),
};
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
if (flowId) payload.flow_id = flowId;
if (gopayGuid) payload.gopay_guid = gopayGuid;
if (redirectUrl) payload.redirect_url = redirectUrl;
return payload;
}
function buildGpcOtpRetryPayload(input = {}) {
const payload = buildGpcOtpPayload(input);
return { ...payload, code: payload.otp };
}
function buildGpcPinPayload(input = {}) {
const payload = {
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
pin: normalizeGoPayPin(input.pin ?? ''),
};
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
if (flowId) payload.flow_id = flowId;
if (redirectUrl) payload.redirect_url = redirectUrl;
return payload;
}
function buildGpcPinRetryPayload(input = {}) {
const payload = buildGpcPinPayload(input);
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 = {}) {
const data = unwrapGpcBalancePayload(payload);
if (!data || typeof data !== 'object') {
@@ -418,43 +314,28 @@
return {
DEFAULT_GOPAY_COUNTRY_CODE,
DEFAULT_GPC_HELPER_API_URL,
GPC_HELPER_PHONE_MODE_AUTO,
GPC_HELPER_PHONE_MODE_MANUAL,
DEFAULT_GPC_BASE_URL,
PLUS_PAYMENT_METHOD_GPC_HELPER,
PLUS_PAYMENT_METHOD_GOPAY,
PLUS_PAYMENT_METHOD_NONE,
PLUS_PAYMENT_METHOD_PAYPAL,
PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
buildGpcCardBalanceUrl,
buildGpcApiKeyBalanceUrl,
buildGpcApiKeyHeaders,
buildGpcHelperApiUrl,
buildGpcOtpPayload,
buildGpcOtpRetryPayload,
buildGpcPinPayload,
buildGpcPinRetryPayload,
buildGpcTaskActionUrl,
buildGpcTaskCreateUrl,
buildGpcTaskOtpPayload,
buildGpcTaskPinPayload,
buildGpcTaskQueryUrl,
buildGpcApiUrl,
extractGpcResponseErrorDetail,
formatGpcBalancePayload,
getGpcApiKeyStatus,
getGpcCardStatus,
getGpcBalanceRemainingUses,
isGpcUnifiedResponseOk,
isGpcAutoModeEnabled,
normalizeGpcHelperBaseUrl,
normalizeGpcHelperPhoneMode,
isGpcCardKeyFormat,
normalizeGpcBaseUrl,
normalizeGpcCardKey,
normalizeGpcRemainingUses,
normalizeGpcTaskId,
normalizeGoPayCountryCode,
normalizeGoPayPhone,
normalizeGoPayPhoneForCountry,
normalizeGoPayOtp,
normalizeGoPayPin,
normalizeGpcOtpChannel,
normalizePlusPaymentMethod,
unwrapGpcBalancePayload,
unwrapGpcResponse,
-424
View File
@@ -1,424 +0,0 @@
#!/usr/bin/env python3
"""Local macOS SMS OTP helper for GPC GoPay flows.
Run this on the Mac that receives forwarded iPhone SMS messages. The helper
reads a copy of the macOS Messages database and exposes the latest matching OTP
on a localhost HTTP endpoint for the Chrome extension.
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import os
import re
import shutil
import sqlite3
import sys
import tempfile
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Optional
from urllib.parse import parse_qs, urlparse
HOST = "127.0.0.1"
PORT = 18767
DEFAULT_DB = "~/Library/Messages/chat.db"
MAC_ABSOLUTE_EPOCH = dt.datetime(2001, 1, 1, tzinfo=dt.timezone.utc)
OTP_PATTERNS = (
re.compile(r"(?i)\bOTP\s*[:]?\s*([0-9]{4,8})\b"),
re.compile(r"#([0-9]{4,8})\b"),
re.compile(r"(?<!\d)([0-9]{6})(?!\d)"),
)
KEYWORDS = ("gojek", "gopay", "openai llc", "openai")
STATE_LOCK = threading.Lock()
STATE = {
"started_at": dt.datetime.now(dt.timezone.utc).isoformat(),
"db_path": "",
"last_scan_at": "",
"last_error": "",
"last_rowid": 0,
"last_otp": None,
"otps": [],
}
def is_macos() -> bool:
override = os.environ.get("GPC_SMS_HELPER_ALLOW_NON_MAC", "").strip().lower()
return sys.platform == "darwin" or override in {"1", "true", "yes"}
def require_macos() -> None:
if not is_macos():
raise RuntimeError(
"GPC 本地 SMS Helper 仅支持 macOS:需要读取 ~/Library/Messages/chat.db。"
"请确认接收验证码的 iPhone 已开启短信转发,并能在 Mac 信息 app 中看到短信。"
)
def extract_gopay_otp(text: str, require_keywords: bool = True) -> Optional[str]:
raw = text or ""
lowered = raw.lower()
if require_keywords and not any(keyword in lowered for keyword in KEYWORDS):
return None
for pattern in OTP_PATTERNS:
match = pattern.search(raw)
if match:
return match.group(1)
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:
if not value:
return dt.datetime.now(dt.timezone.utc)
seconds = float(value)
if seconds > 10_000_000_000:
seconds = seconds / 1_000_000_000
return MAC_ABSOLUTE_EPOCH + dt.timedelta(seconds=seconds)
def update_state(**updates: object) -> None:
with STATE_LOCK:
STATE.update(updates)
def get_state() -> dict:
with STATE_LOCK:
return dict(STATE)
def copy_messages_db(db_path: Path) -> Path:
if not db_path.exists():
raise FileNotFoundError(f"Messages 数据库不存在:{db_path}")
tmpdir = Path(tempfile.mkdtemp(prefix="gpc_messages_"))
copied = tmpdir / "chat.db"
shutil.copy2(db_path, copied)
wal = db_path.with_name(db_path.name + "-wal")
shm = db_path.with_name(db_path.name + "-shm")
if wal.exists():
shutil.copy2(wal, copied.with_name("chat.db-wal"))
if shm.exists():
shutil.copy2(shm, copied.with_name("chat.db-shm"))
return copied
def read_recent_messages(db_path: Path, after_rowid: int = 0, limit: int = 80) -> list[dict]:
copied = copy_messages_db(db_path)
try:
conn = sqlite3.connect(str(copied))
conn.row_factory = sqlite3.Row
params = (max(0, int(after_rowid or 0)), max(1, int(limit or 80)))
try:
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.destination_caller_id AS destination_caller_id,
message.account AS account,
handle.id AS handle,
chat.last_addressed_handle AS last_addressed_handle,
chat.account_login AS account_login
FROM message
LEFT JOIN handle ON message.handle_id = handle.ROWID
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]
finally:
try:
conn.close() # type: ignore[name-defined]
except Exception:
pass
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:
received_at = mac_message_time_to_datetime(row.get("date")).isoformat()
phone_e164 = get_record_phone(row)
return {
"otp": otp,
"code": otp,
"message_id": str(row.get("guid") or row.get("rowid") or ""),
"rowid": int(row.get("rowid") or 0),
"sender": str(row.get("handle") or ""),
"service": str(row.get("service") or ""),
"account_phone": phone_e164,
"phone_e164": phone_e164,
"received_at": received_at,
"message_text": str(row.get("text") or ""),
}
def append_otp(record: dict, max_records: int = 30) -> None:
with STATE_LOCK:
records = [item for item in STATE.get("otps", []) if item.get("message_id") != record.get("message_id")]
records.insert(0, record)
STATE["otps"] = records[:max_records]
STATE["last_otp"] = record
STATE["last_rowid"] = max(int(STATE.get("last_rowid") or 0), int(record.get("rowid") or 0))
def parse_timestamp_ms(value: object) -> int:
if value is None:
return 0
if isinstance(value, (int, float)):
numeric = float(value)
else:
raw = str(value).strip()
if not raw:
return 0
try:
numeric = float(raw)
except ValueError:
try:
parsed = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=dt.timezone.utc)
return int(parsed.timestamp() * 1000)
except ValueError:
return 0
if numeric <= 0:
return 0
if numeric < 100_000_000_000:
numeric *= 1000
return int(numeric)
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")
if not isinstance(records, list):
records = []
if after_ms > 0:
for record in records:
if (
isinstance(record, dict)
and record_matches_phone(record, phone)
and parse_timestamp_ms(record.get("received_at")) >= after_ms
):
return record
return None
record = state.get("last_otp") or None
if isinstance(record, dict) and record_matches_phone(record, phone):
return record
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:
state = get_state()
after_rowid = int(state.get("last_rowid") or 0)
rows = read_recent_messages(db_path, after_rowid=after_rowid)
max_rowid = after_rowid
for row in reversed(rows):
max_rowid = max(max_rowid, int(row.get("rowid") or 0))
otp = extract_gopay_otp(row.get("text") or "", require_keywords=require_keywords)
if otp:
record = make_otp_record(row, otp)
append_otp(record)
print(f"captured OTP {otp} from message {record['message_id']} at {record['received_at']}", flush=True)
update_state(last_rowid=max_rowid, last_scan_at=dt.datetime.now(dt.timezone.utc).isoformat(), last_error="")
def scan_loop(db_path: Path, interval_seconds: float, require_keywords: bool) -> None:
update_state(db_path=str(db_path))
while True:
try:
scan_once(db_path, require_keywords=require_keywords)
except Exception as exc:
update_state(last_error=str(exc), last_scan_at=dt.datetime.now(dt.timezone.utc).isoformat())
print(f"scan error: {exc}", file=sys.stderr, flush=True)
time.sleep(max(0.5, float(interval_seconds or 2)))
def write_json(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
handler.send_response(status)
handler.send_header("Content-Type", "application/json; charset=utf-8")
handler.send_header("Access-Control-Allow-Origin", "*")
handler.send_header("Cache-Control", "no-store")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
handler.wfile.write(body)
class HelperHandler(BaseHTTPRequestHandler):
server_version = "GpcSmsHelper/1.0"
def log_message(self, fmt: str, *args: object) -> None:
print(f"{self.address_string()} - {fmt % args}", flush=True)
def do_GET(self) -> None: # noqa: N802
parsed = urlparse(self.path)
if parsed.path == "/health":
state = get_state()
write_json(self, 200, {
"ok": True,
"status": "ok",
"has_otp": bool(state.get("last_otp")),
"last_scan_at": state.get("last_scan_at"),
"last_error": state.get("last_error"),
})
return
if parsed.path in ("/otp", "/latest-otp"):
query = parse_qs(parsed.query)
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])
phone = str((query.get("phone") or query.get("phone_e164") or query.get("phone_number") or [""])[0]).strip()
state = get_state()
record = select_otp_record(state, after_ms=after_ms, phone=phone)
if not record:
write_json(self, 200, {"ok": True, "otp": "", "code": "", "status": "waiting", "message": "未查询到验证码"})
return
payload = {"ok": True, "status": "found", **record}
if consume:
consume_otp_record(phone=phone, record=record)
write_json(self, 200, payload)
return
write_json(self, 404, {"ok": False, "error": "not_found"})
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run a local macOS Messages HTTP helper for GPC SMS OTP.")
parser.add_argument("--host", default=HOST, help="Bind host, default 127.0.0.1")
parser.add_argument("--port", type=int, default=PORT, help="Bind port, default 18767")
parser.add_argument("--db", default=DEFAULT_DB, help="Path to macOS Messages chat.db")
parser.add_argument("--interval", type=float, default=2.0, help="Message scan interval in seconds")
parser.add_argument("--no-keywords", action="store_true", help="Accept any numeric OTP without GPC/OpenAI keywords")
return parser.parse_args()
def main() -> int:
args = parse_args()
try:
require_macos()
db_path = Path(os.path.expanduser(args.db)).resolve()
scanner = threading.Thread(
target=scan_loop,
args=(db_path, args.interval, not args.no_keywords),
daemon=True,
)
scanner.start()
server = ThreadingHTTPServer((args.host, int(args.port)), HelperHandler)
print(f"GPC SMS Helper listening on http://{args.host}:{int(args.port)}", flush=True)
print("请确认 iPhone 短信已转发到本机 Messages。", flush=True)
server.serve_forever()
return 0
except KeyboardInterrupt:
return 0
except Exception as exc:
print(str(exc), file=sys.stderr, flush=True)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+22 -4
View File
@@ -921,6 +921,16 @@ header {
min-width: 0;
}
.gpc-card-key-inline > .input-with-icon {
flex: 0 1 376px;
max-width: 376px;
}
.gpc-card-key-inline > .data-value {
flex: 0 0 auto;
white-space: nowrap;
}
.ip-proxy-fold-row {
display: block;
}
@@ -1287,6 +1297,18 @@ header {
min-width: 0;
}
.data-value[data-tone="ok"] {
color: var(--green);
}
.data-value[data-tone="running"] {
color: var(--blue);
}
.data-value[data-tone="error"] {
color: var(--red);
}
.data-value-fill {
flex: 1;
}
@@ -2094,10 +2116,6 @@ header {
cursor: default;
}
.gpc-helper-api-input {
font-family: 'JetBrains Mono', monospace;
}
.editable-list-picker {
position: relative;
flex: 1 1 0;
+11 -83
View File
@@ -337,8 +337,8 @@
<option value="paypal">PayPal</option>
<!--
<option value="gopay">GoPay</option>
<option value="gpc-helper">GPC</option>
-->
<option value="gpc-helper">GPC</option>
</select>
<button id="btn-gpc-card-key-purchase" class="btn btn-outline btn-sm data-inline-btn" type="button" style="display:none;">购买卡密</button>
<span class="setting-caption" id="plus-payment-method-caption">PayPal 订阅链路</span>
@@ -377,90 +377,18 @@
<span class="setting-caption">支付成功页出现后再继续账号接入</span>
</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;">
<span class="data-label">GPC API Key</span>
<div class="data-inline">
<div class="data-row" id="row-gpc-card-key" style="display:none;">
<span class="data-label">GPC 卡密</span>
<div class="data-inline gpc-card-key-inline">
<div class="input-with-icon">
<input type="password" id="input-gpc-helper-card-key" class="data-input data-input-with-icon"
placeholder="请输入 GPC API Keygpc-..." autocomplete="off" />
<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 API Key"
data-hide-label="隐藏 GPC API Key" aria-label="显示 GPC API Key" title="显示 GPC API Key"></button>
<input type="password" id="input-gpc-card-key" class="data-input data-input-with-icon"
placeholder="请输入 GPC 卡密" autocomplete="off" />
<button id="btn-toggle-gpc-card-key" class="input-icon-btn" type="button"
data-password-toggle="input-gpc-card-key" data-show-label="显示 GPC 卡密"
data-hide-label="隐藏 GPC 卡密" aria-label="显示 GPC 卡密" title="显示 GPC 卡密"></button>
</div>
<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>
</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">
<option value="+86">中国 +86</option>
<option value="+62">印度尼西亚 +62</option>
<option value="+1">美国/加拿大 +1</option>
<option value="+852">香港 +852</option>
<option value="+853">澳门 +853</option>
<option value="+886">台湾 +886</option>
<option value="+60">马来西亚 +60</option>
<option value="+65">新加坡 +65</option>
<option value="+66">泰国 +66</option>
<option value="+84">越南 +84</option>
<option value="+63">菲律宾 +63</option>
<option value="+91">印度 +91</option>
</select>
</div>
<div class="data-row" id="row-gpc-helper-phone" style="display:none;">
<span class="data-label">GPC 手机</span>
<input type="text" id="input-gpc-helper-phone" class="data-input"
placeholder="GPC 专用手机号,不读取 GoPay 手机字段" autocomplete="tel" />
</div>
<div class="data-row" id="row-gpc-helper-otp-channel" style="display:none;">
<span class="data-label">GPC OTP</span>
<select id="select-gpc-helper-otp-channel" class="data-select">
<option value="whatsapp">WhatsApp</option>
<option value="sms">SMS</option>
</select>
</div>
<div class="data-row" id="row-gpc-helper-local-sms-enabled" style="display:none;">
<span class="data-label">本地 OTP</span>
<div class="data-inline">
<label class="toggle-switch">
<input type="checkbox" id="input-gpc-helper-local-sms-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
<span class="setting-caption">从本机 helper 读取当前通道 OTP,开启后不弹手动验证码</span>
</div>
</div>
<div class="data-row" id="row-gpc-helper-local-sms-url" style="display:none;">
<span class="data-label">OTP 接口</span>
<input type="text" id="input-gpc-helper-local-sms-url" class="data-input"
placeholder="http://127.0.0.1:18767" autocomplete="off" />
</div>
<div class="data-row" id="row-gpc-helper-pin" style="display:none;">
<span class="data-label">GPC PIN</span>
<div class="input-with-icon">
<input type="password" id="input-gpc-helper-pin" class="data-input data-input-with-icon"
placeholder="请输入 GoPay PIN" autocomplete="off" />
<button id="btn-toggle-gpc-helper-pin" class="input-icon-btn" type="button"
data-password-toggle="input-gpc-helper-pin" data-show-label="显示 GPC PIN"
data-hide-label="隐藏 GPC PIN" aria-label="显示 GPC PIN"
title="显示 GPC PIN"></button>
<button id="btn-gpc-card-key-query" class="btn btn-outline btn-sm data-inline-btn" type="button"></button>
<span id="display-gpc-card-key-status" class="data-value mono">等待输入</span>
</div>
</div>
<div class="data-row" id="row-gopay-country-code" style="display:none;">
+172 -588
View File
@@ -227,29 +227,10 @@ const rowHostedCheckoutPhone = document.getElementById('row-hosted-checkout-phon
const inputHostedCheckoutPhone = document.getElementById('input-hosted-checkout-phone');
const rowPlusHostedCheckoutOauthDelay = document.getElementById('row-plus-hosted-checkout-oauth-delay');
const inputPlusHostedCheckoutOauthDelaySeconds = document.getElementById('input-plus-hosted-checkout-oauth-delay-seconds');
const rowGpcHelperApi = document.getElementById('row-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 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');
const inputGpcHelperPhone = document.getElementById('input-gpc-helper-phone');
const rowGpcHelperOtpChannel = document.getElementById('row-gpc-helper-otp-channel');
const selectGpcHelperOtpChannel = document.getElementById('select-gpc-helper-otp-channel');
const rowGpcHelperLocalSmsEnabled = document.getElementById('row-gpc-helper-local-sms-enabled');
const inputGpcHelperLocalSmsEnabled = document.getElementById('input-gpc-helper-local-sms-enabled');
const rowGpcHelperLocalSmsUrl = document.getElementById('row-gpc-helper-local-sms-url');
const inputGpcHelperLocalSmsUrl = document.getElementById('input-gpc-helper-local-sms-url');
const rowGpcHelperPin = document.getElementById('row-gpc-helper-pin');
const inputGpcHelperPin = document.getElementById('input-gpc-helper-pin');
const btnToggleGpcHelperPin = document.getElementById('btn-toggle-gpc-helper-pin');
const rowGpcCardKey = document.getElementById('row-gpc-card-key');
const inputGpcCardKey = document.getElementById('input-gpc-card-key');
const displayGpcCardKeyStatus = document.getElementById('display-gpc-card-key-status');
const btnGpcCardKeyQuery = document.getElementById('btn-gpc-card-key-query');
const rowGoPayCountryCode = document.getElementById('row-gopay-country-code');
const selectGoPayCountryCode = document.getElementById('select-gopay-country-code');
const rowGoPayPhone = document.getElementById('row-gopay-phone');
@@ -600,10 +581,7 @@ const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
const PLUS_PAYMENT_METHOD_NONE = 'none';
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_GPC_BASE_URL = 'https://gpc.qlhazycoder.top';
const DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS = 3;
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
@@ -1792,9 +1770,6 @@ const PRIVACY_MASKED_INPUT_IDS = Object.freeze([
'input-codex2api-url',
'input-kiro-rs-url',
'input-grok-webchat2api-url',
'input-gpc-helper-api',
'input-gpc-helper-phone',
'input-gpc-helper-local-sms-url',
'input-gopay-phone',
'input-gopay-otp',
'input-email-prefix',
@@ -3652,147 +3627,6 @@ function resolvePlusManualContinuationActionLabelFromState(state = latestState)
return getPlusAccountAccessStrategyContinuationLabel(effectiveStrategy, targetId);
}
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 normalizeGpcAutoModePermissionValue(value) {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
if (value === 1) return true;
if (value === 0) return false;
}
const normalized = String(value ?? '').trim().toLowerCase();
if (!normalized) {
return null;
}
if (['true', '1', 'yes', 'y', 'on', 'enabled', 'enable'].includes(normalized)) {
return true;
}
if (['false', '0', 'no', 'n', 'off', 'disabled', 'disable'].includes(normalized)) {
return false;
}
return null;
}
function getGpcAutoModePermissionFromPayload(payload = {}) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
return null;
}
for (const key of ['auto_mode_enabled', 'autoModeEnabled', 'auto_enabled', 'autoEnabled']) {
if (payload[key] !== undefined) {
return normalizeGpcAutoModePermissionValue(payload[key]);
}
}
if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
return getGpcAutoModePermissionFromPayload(payload.data);
}
return null;
}
function shouldPreserveSelectedGpcAutoMode(state = latestState) {
const payloadPermission = getGpcAutoModePermissionFromPayload(state?.gopayHelperBalancePayload);
return normalizeGpcHelperPhoneModeValue(state?.gopayHelperPhoneMode) === GPC_HELPER_PHONE_MODE_AUTO
&& (Boolean(state?.gopayHelperAutoModeEnabled) || payloadPermission === true);
}
function hasGpcAutoModePermissionField(payload = {}) {
return getGpcAutoModePermissionFromPayload(payload) !== null;
}
function isGpcAutoModePermissionDenied(state = latestState) {
const payloadPermission = getGpcAutoModePermissionFromPayload(state?.gopayHelperBalancePayload);
return payloadPermission === false;
}
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) {
return rootScope.GoPayUtils.normalizeGpcOtpChannel(value);
}
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
}
function normalizeGpcLocalSmsHelperBaseUrlValue(value = '') {
const fallback = 'http://127.0.0.1:18767';
const rawValue = String(value || fallback).trim();
try {
const parsed = new URL(rawValue);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return fallback;
}
const endpointPath = parsed.pathname.replace(/\/+$/g, '') || '/';
if (['/otp', '/latest-otp', '/health'].includes(endpointPath)) {
parsed.pathname = '';
parsed.search = '';
parsed.hash = '';
}
return parsed.toString().replace(/\/$/, '');
} catch {
return fallback;
}
}
function hasOwnStateValue(source, key) {
return Object.prototype.hasOwnProperty.call(source, key);
}
@@ -4466,8 +4300,8 @@ function applyYydsMailSettingsState(state = {}) {
}
function collectSettingsPayload() {
const defaultGpcHelperApiUrl = typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined'
? DEFAULT_GPC_HELPER_API_URL
const defaultGpcBaseUrl = typeof DEFAULT_GPC_BASE_URL !== 'undefined'
? DEFAULT_GPC_BASE_URL
: 'https://gpc.qlhazycoder.top';
const normalizeYydsBaseUrlValue = typeof normalizeYydsMailBaseUrl === 'function'
? normalizeYydsMailBaseUrl
@@ -4595,36 +4429,6 @@ function collectSettingsPayload() {
.map((line) => line.trim())
.filter(Boolean)
.join('\n'));
const normalizeGpcOtpChannelSafe = typeof normalizeGpcOtpChannelValue === 'function'
? normalizeGpcOtpChannelValue
: ((value = '') => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
return rootScope.GoPayUtils.normalizeGpcOtpChannel(value);
}
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
});
const normalizeGpcLocalSmsHelperBaseUrlSafe = typeof normalizeGpcLocalSmsHelperBaseUrlValue === 'function'
? normalizeGpcLocalSmsHelperBaseUrlValue
: ((value = '') => {
const fallback = 'http://127.0.0.1:18767';
const rawValue = String(value || fallback).trim();
try {
const parsed = new URL(rawValue);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return fallback;
}
const endpointPath = parsed.pathname.replace(/\/+$/g, '') || '/';
if (['/otp', '/latest-otp', '/health'].includes(endpointPath)) {
parsed.pathname = '';
parsed.search = '';
parsed.hash = '';
}
return parsed.toString().replace(/\/$/, '');
} catch {
return fallback;
}
});
const normalizeMaDaoBaseUrlSafe = typeof normalizeMaDaoBaseUrlValue === 'function'
? normalizeMaDaoBaseUrlValue
: ((value = '') => {
@@ -5208,25 +5012,6 @@ function collectSettingsPayload() {
: rawPhoneVerificationEnabled;
const effectiveSignupMethod = capabilityState?.effectiveSignupMethod || selectedSignupMethod;
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 = selectedGpcPhoneMode;
const selectedGpcOtpChannel = normalizeGpcOtpChannelSafe(
typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel
? selectGpcHelperOtpChannel.value
: (latestState?.gopayHelperOtpChannel || 'whatsapp')
);
const selectedGpcLocalSmsHelperEnabled = effectiveGpcPhoneMode === 'auto' ? false : Boolean(
typeof inputGpcHelperLocalSmsEnabled !== 'undefined' && inputGpcHelperLocalSmsEnabled
? inputGpcHelperLocalSmsEnabled.checked
: latestState?.gopayHelperLocalSmsHelperEnabled
);
const selectedSub2ApiGroupName = String(inputSub2ApiGroup.value || '').trim();
const sub2apiGroupNames = [];
const seenSub2ApiGroupNames = new Set();
@@ -5382,36 +5167,12 @@ function collectSettingsPayload() {
: (typeof inputGoPayPin !== 'undefined' && inputGoPayPin
? String(inputGoPayPin.value || '')
: String(latestState?.gopayPin || '')),
gopayHelperApiUrl: window.GoPayUtils?.normalizeGpcHelperBaseUrl
? window.GoPayUtils.normalizeGpcHelperBaseUrl(defaultGpcHelperApiUrl)
: String(defaultGpcHelperApiUrl).trim().replace(/\/+$/g, ''),
gopayHelperApiKey: typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey
? 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
? String(selectGpcHelperCountryCode.value || '+86').trim()
: String(latestState?.gopayHelperCountryCode || '+86').trim()),
gopayHelperPhoneNumber: window.GoPayUtils?.normalizeGoPayPhone
? window.GoPayUtils.normalizeGoPayPhone(typeof inputGpcHelperPhone !== 'undefined' && inputGpcHelperPhone ? inputGpcHelperPhone.value : latestState?.gopayHelperPhoneNumber)
: (typeof inputGpcHelperPhone !== 'undefined' && inputGpcHelperPhone
? String(inputGpcHelperPhone.value || '').trim()
: String(latestState?.gopayHelperPhoneNumber || '').trim()),
gopayHelperPin: window.GoPayUtils?.normalizeGoPayPin
? window.GoPayUtils.normalizeGoPayPin(typeof inputGpcHelperPin !== 'undefined' && inputGpcHelperPin ? inputGpcHelperPin.value : latestState?.gopayHelperPin)
: (typeof inputGpcHelperPin !== 'undefined' && inputGpcHelperPin
? String(inputGpcHelperPin.value || '')
: String(latestState?.gopayHelperPin || '')),
gopayHelperOtpChannel: selectedGpcOtpChannel,
gopayHelperLocalSmsHelperEnabled: selectedGpcLocalSmsHelperEnabled,
gopayHelperLocalSmsHelperUrl: normalizeGpcLocalSmsHelperBaseUrlSafe(
typeof inputGpcHelperLocalSmsUrl !== 'undefined' && inputGpcHelperLocalSmsUrl
? inputGpcHelperLocalSmsUrl.value
: (latestState?.gopayHelperLocalSmsHelperUrl || '')
),
gpcBaseUrl: window.GoPayUtils?.normalizeGpcBaseUrl
? window.GoPayUtils.normalizeGpcBaseUrl(defaultGpcBaseUrl)
: String(defaultGpcBaseUrl).trim().replace(/\/+$/g, ''),
gpcCardKey: typeof inputGpcCardKey !== 'undefined' && inputGpcCardKey
? normalizeGpcCardKeyInput(inputGpcCardKey.value || '')
: normalizeGpcCardKeyInput(latestState?.gpcCardKey || ''),
...(accountContributionEnabled ? {} : {
customPassword: inputPassword.value,
}),
@@ -10494,32 +10255,11 @@ function updatePlusModeUI() {
|| 'cpa'
);
const method = enabled ? getSelectedPlusPaymentMethod() : defaultMethod;
const gpcPhoneMode = normalizeGpcHelperPhoneModeValue(
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
? selectGpcHelperPhoneMode.value
: (latestState?.gopayHelperPhoneMode || 'manual')
);
const gpcAutoModeDenied = isGpcAutoModePermissionDenied(latestState);
const isGpcAutoMode = gpcPhoneMode === GPC_HELPER_PHONE_MODE_AUTO;
const gpcAutoModeBlocked = isGpcAutoMode && gpcAutoModeDenied;
const gpcOtpChannel = normalizeGpcOtpChannelValue(
typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel
? selectGpcHelperOtpChannel.value
: (latestState?.gopayHelperOtpChannel || 'whatsapp')
);
const localSmsEnabled = Boolean(
typeof inputGpcHelperLocalSmsEnabled !== 'undefined' && inputGpcHelperLocalSmsEnabled
? inputGpcHelperLocalSmsEnabled.checked
: latestState?.gopayHelperLocalSmsHelperEnabled
);
const selectedMethod = typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod?.value
? normalizePlusPaymentMethod(selectPlusPaymentMethod.value)
: method;
const hostedRowsVisible = enabled && selectedMethod === paypalHostedValue;
const gpcRowsVisible = enabled && selectedMethod === gpcValue;
const canShowGpcModeSelector = gpcRowsVisible;
const localSmsControlsVisible = gpcRowsVisible && !isGpcAutoMode;
const effectiveLocalSmsEnabled = !isGpcAutoMode && localSmsEnabled;
if (typeof rowPlusMode !== 'undefined' && rowPlusMode) {
rowPlusMode.style.display = supportsPlusMode ? '' : 'none';
}
@@ -10531,7 +10271,7 @@ function updatePlusModeUI() {
}
if (typeof plusPaymentMethodCaption !== 'undefined' && plusPaymentMethodCaption) {
plusPaymentMethodCaption.textContent = method === gpcValue
? `GPC ${isGpcAutoMode ? '自动' : '手动'}订阅链路`
? 'GPC 网页充值链路'
: method === gopayValue
? 'GoPay 印尼订阅链路'
: method === noneValue
@@ -10540,9 +10280,6 @@ function updatePlusModeUI() {
? 'PayPal 无卡直绑链路'
: 'PayPal 订阅链路';
}
if (typeof plusPaymentMethodCaption !== 'undefined' && plusPaymentMethodCaption && method === gpcValue && gpcAutoModeBlocked) {
plusPaymentMethodCaption.textContent = 'GPC 自动订阅链路(需手动切换)';
}
[
typeof rowPlusPaymentMethod !== 'undefined' ? rowPlusPaymentMethod : null,
].forEach((row) => {
@@ -10646,43 +10383,8 @@ function updatePlusModeUI() {
}
row.style.display = hostedRowsVisible ? '' : 'none';
});
[
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 = gpcPhoneMode;
}
[
typeof rowGpcHelperCountryCode !== 'undefined' ? rowGpcHelperCountryCode : null,
typeof rowGpcHelperPhone !== 'undefined' ? rowGpcHelperPhone : null,
typeof rowGpcHelperOtpChannel !== 'undefined' ? rowGpcHelperOtpChannel : null,
typeof rowGpcHelperPin !== 'undefined' ? rowGpcHelperPin : null,
].forEach((row) => {
if (!row) {
return;
}
row.style.display = gpcRowsVisible && !isGpcAutoMode ? '' : 'none';
});
if (typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel) {
selectGpcHelperOtpChannel.value = gpcOtpChannel;
}
if (typeof inputGpcHelperLocalSmsEnabled !== 'undefined' && inputGpcHelperLocalSmsEnabled) {
inputGpcHelperLocalSmsEnabled.checked = effectiveLocalSmsEnabled;
}
if (typeof rowGpcHelperLocalSmsEnabled !== 'undefined' && rowGpcHelperLocalSmsEnabled) {
rowGpcHelperLocalSmsEnabled.style.display = localSmsControlsVisible ? '' : 'none';
}
if (typeof rowGpcHelperLocalSmsUrl !== 'undefined' && rowGpcHelperLocalSmsUrl) {
rowGpcHelperLocalSmsUrl.style.display = localSmsControlsVisible && effectiveLocalSmsEnabled ? '' : 'none';
if (typeof rowGpcCardKey !== 'undefined' && rowGpcCardKey) {
rowGpcCardKey.style.display = gpcRowsVisible ? '' : 'none';
}
if (typeof btnGpcCardKeyPurchase !== 'undefined' && btnGpcCardKeyPurchase) {
btnGpcCardKeyPurchase.style.display = gpcRowsVisible ? '' : 'none';
@@ -10936,83 +10638,122 @@ function isGpcHelperCheckoutSelected() {
return plusEnabled && getSelectedPlusPaymentMethod() === gpcValue;
}
function getSelectedGpcHelperPhoneMode() {
return normalizeGpcHelperPhoneModeValue(
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
? selectGpcHelperPhoneMode.value
: (latestState?.gopayHelperPhoneMode || GPC_HELPER_PHONE_MODE_MANUAL)
);
function normalizeGpcCardKeyInput(value = '') {
if (window.GoPayUtils?.normalizeGpcCardKey) {
return window.GoPayUtils.normalizeGpcCardKey(value);
}
return String(value || '').trim().toUpperCase();
}
function isGpcCardKeyInputFormat(value = '') {
if (window.GoPayUtils?.isGpcCardKeyFormat) {
return window.GoPayUtils.isGpcCardKeyFormat(value);
}
return /^GPC-[A-F0-9]{8}-[A-F0-9]{8}-[A-F0-9]{8}$/.test(normalizeGpcCardKeyInput(value));
}
function setGpcCardKeyStatus(message = '', tone = '') {
if (!displayGpcCardKeyStatus) {
return;
}
displayGpcCardKeyStatus.textContent = message || '等待输入';
displayGpcCardKeyStatus.dataset.tone = tone || '';
}
function formatGpcCardKeyBalanceStatus(result = {}) {
const remaining = result?.remainingUses;
if (remaining !== undefined && remaining !== null && String(remaining).trim() !== '') {
return `剩余 ${remaining}`;
}
return String(result?.balance || '').trim() || '卡密可用';
}
async function refreshGpcCardKeyStatus(options = {}) {
const rawCardKey = String(inputGpcCardKey?.value || '').trim();
const cardKey = normalizeGpcCardKeyInput(rawCardKey);
if (!rawCardKey) {
setGpcCardKeyStatus('等待输入', '');
return null;
}
if (inputGpcCardKey && inputGpcCardKey.value !== cardKey) {
inputGpcCardKey.value = cardKey;
}
if (!isGpcCardKeyInputFormat(cardKey)) {
setGpcCardKeyStatus('格式应为 GPC-XXXXXXXX-XXXXXXXX-XXXXXXXX', 'error');
return null;
}
const requestId = (refreshGpcCardKeyStatus.requestId || 0) + 1;
refreshGpcCardKeyStatus.requestId = requestId;
setGpcCardKeyStatus('正在检测...', 'running');
try {
const response = await sendSidepanelMessage({
type: 'REFRESH_GPC_CARD_BALANCE',
payload: {
gpcCardKey: cardKey,
reason: options.reason || 'input',
},
});
if (requestId !== refreshGpcCardKeyStatus.requestId) {
return response;
}
if (response?.error) {
throw new Error(response.error);
}
setGpcCardKeyStatus(formatGpcCardKeyBalanceStatus(response), 'ok');
syncLatestState({
gpcCardKey: cardKey,
gpcBalance: response?.balance || latestState?.gpcBalance || '',
gpcBalancePayload: response?.data || latestState?.gpcBalancePayload || null,
gpcBalanceUpdatedAt: response?.updatedAt || Date.now(),
gpcBalanceError: '',
gpcRemainingUses: Number(response?.remainingUses) || 0,
gpcCardStatus: String(response?.cardStatus || '').trim(),
});
return response;
} catch (error) {
if (requestId === refreshGpcCardKeyStatus.requestId) {
setGpcCardKeyStatus(error?.message || '卡密检测失败', 'error');
}
return null;
}
}
function scheduleGpcCardKeyStatusRefresh() {
clearTimeout(scheduleGpcCardKeyStatusRefresh.timer);
scheduleGpcCardKeyStatusRefresh.timer = setTimeout(() => {
refreshGpcCardKeyStatus({ reason: 'input' }).catch(() => { });
}, 1000);
}
async function showGpcStartBlockedDialog(message) {
await openConfirmModal({
title: 'GPC 任务无法开启',
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 = {}) {
async function ensureGpcCardKeyReadyForStart(options = {}) {
if (!isGpcHelperCheckoutSelected()) {
return true;
}
const selectedMode = getSelectedGpcHelperPhoneMode();
let balanceState;
try {
balanceState = await refreshGpcBalanceForStart();
} catch (error) {
await showGpcStartBlockedDialog(`API Key 余额校验失败:${error?.message || '未知错误'}。请先确认 API Key 是否正确。`);
const cardKey = normalizeGpcCardKeyInput(inputGpcCardKey?.value || latestState?.gpcCardKey || '');
if (!cardKey) {
await showGpcStartBlockedDialog('请先填写 GPC 卡密。');
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},不能开启任务。`);
if (!isGpcCardKeyInputFormat(cardKey)) {
await showGpcStartBlockedDialog('GPC 卡密格式不正确,应类似 GPC-6C9F1A32-45734795-914E6F00。');
setGpcCardKeyStatus('格式应为 GPC-XXXXXXXX-XXXXXXXX-XXXXXXXX', 'error');
return false;
}
if (remainingUses !== null && remainingUses <= 0) {
await showGpcStartBlockedDialog('当前 GPC API Key 剩余次数不足,不能开启任务。');
return false;
}
if (selectedMode === GPC_HELPER_PHONE_MODE_AUTO && isGpcAutoModePermissionDenied(balanceState)) {
await showGpcStartBlockedDialog('当前 GPC API Key 未开通自动模式,已保留你的当前选择。如需继续,请由你手动切换到手动模式后再开启任务。');
return false;
if (inputGpcCardKey) {
inputGpcCardKey.value = cardKey;
}
if (options?.notify) {
showToast('GPC API Key 余额和权限校验通过。', 'success', 1800);
showToast('GPC 卡密已填写。', 'success', 1800);
}
return true;
}
@@ -11064,36 +10805,6 @@ async function openPlusManualConfirmationDialog(options = {}) {
const continuationActionLabel = useSub2ApiSessionImport
? '导入当前 ChatGPT 会话到 SUB2API'
: 'OAuth 登录';
if (method === 'gopay-otp') {
if (!sharedFormDialog?.open) {
return null;
}
const result = await sharedFormDialog.open({
title: String(options.title || '').trim() || 'GPC OTP 验证',
message: String(options.message || '').trim() || '请在WhatsApp里面获取验证码(耐心等待三十秒左右)',
fields: [
{
key: 'otp',
label: 'OTP',
type: 'text',
placeholder: '请输入 OTP 验证码',
inputMode: 'numeric',
autocomplete: 'one-time-code',
required: true,
requiredMessage: '请输入 OTP 验证码。',
normalize: (value) => String(value || '').trim().replace(/[^\d]/g, ''),
validate: (value) => {
const normalized = String(value || '').trim().replace(/[^\d]/g, '');
if (!normalized) return '请输入 OTP 验证码。';
if (!/^\d{6}$/.test(normalized)) return 'OTP 必须是 6 位数字,请检查。';
return '';
},
},
],
confirmLabel: '提交 OTP',
});
return result ? { action: 'confirm', otp: String(result.otp || '').trim().replace(/[^\d]/g, '') } : { action: 'cancel' };
}
const title = String(options.title || '').trim() || (method === gopayValue ? 'GoPay 订阅确认' : '手动确认');
const message = String(options.message || '').trim()
|| (method === gopayValue
@@ -11189,7 +10900,7 @@ async function syncPlusManualConfirmationDialog() {
return;
}
const confirmed = choice === 'confirm' || choice?.action === 'confirm';
const confirmed = choice === 'confirm';
const response = await chrome.runtime.sendMessage({
type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION',
source: 'sidepanel',
@@ -11197,28 +10908,15 @@ async function syncPlusManualConfirmationDialog() {
step,
requestId,
confirmed,
...(choice?.otp ? { otp: choice.otp } : {}),
},
});
if (response?.error) {
throw new Error(response.error);
}
if (confirmed) {
showToast(
method === 'gopay-otp'
? 'GPC OTP 已提交,正在继续验证...'
: (method === gopayValue ? `GoPay 订阅已确认,正在继续${continuationActionLabel}...` : '已确认,流程继续执行中...'),
'info',
2200
);
showToast(method === gopayValue ? `GoPay 订阅已确认,正在继续${continuationActionLabel}...` : '已确认,流程继续执行中...', 'info', 2200);
} else {
showToast(
method === 'gopay-otp'
? '已取消 GPC OTP 输入。'
: (method === gopayValue ? '已取消 GoPay 订阅等待。' : '已取消当前手动确认。'),
'warn',
2200
);
showToast(method === gopayValue ? '已取消 GoPay 订阅等待。' : '已取消当前手动确认。', 'warn', 2200);
}
} catch (error) {
showToast(error?.message || String(error || '未知错误'), 'error');
@@ -11242,39 +10940,6 @@ async function openPlusManualConfirmationDialog(options = {}) {
const method = String(options.method || '').trim().toLowerCase();
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
const continuationActionLabel = resolvePlusManualContinuationActionLabelFromState(latestState);
if (method === 'gopay-otp') {
if (!sharedFormDialog?.open) {
return null;
}
const result = await sharedFormDialog.open({
title: String(options.title || '').trim() || 'GPC OTP 验证',
message: String(options.message || '').trim() || '请在WhatsApp里面获取验证码(耐心等待三十秒左右)',
fields: [
{
key: 'otp',
label: 'OTP',
type: 'text',
placeholder: '请输入 OTP 验证码',
inputMode: 'numeric',
autocomplete: 'one-time-code',
required: true,
requiredMessage: '请输入 OTP 验证码。',
normalize: (value) => String(value || '').trim().replace(/[^\d]/g, ''),
validate: (value) => {
const normalized = String(value || '').trim().replace(/[^\d]/g, '');
if (!normalized) return '请输入 OTP 验证码。';
if (!/^\d{6}$/.test(normalized)) return 'OTP 必须是 6 位数字,请检查。';
return '';
},
},
],
confirmLabel: '提交 OTP',
});
return result
? { action: 'confirm', otp: String(result.otp || '').trim().replace(/[^\d]/g, '') }
: { action: 'cancel' };
}
const title = String(options.title || '').trim() || (method === gopayValue ? 'GoPay subscription confirmation' : 'Manual confirmation');
const message = String(options.message || '').trim()
|| (method === gopayValue
@@ -11803,54 +11468,19 @@ function applySettingsState(state) {
selectPlusAccountAccessStrategy.dataset.requestedValue = currentPlusAccountAccessStrategy;
selectPlusAccountAccessStrategy.value = normalizePlusAccountAccessStrategyUiValue(currentPlusAccountAccessStrategy);
}
if (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi) {
const defaultGpcHelperApiUrl = typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined'
? DEFAULT_GPC_HELPER_API_URL
: 'https://gpc.qlhazycoder.top';
inputGpcHelperApi.value = `${defaultGpcHelperApiUrl.replace(/\/+$/g, '')}/`;
}
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)
: String(state?.gopayHelperCountryCode || '+86').trim();
const hasOption = Array.from(selectGpcHelperCountryCode.options || [])
.some((option) => option.value === normalizedCountryCode);
if (!hasOption && normalizedCountryCode) {
const option = document.createElement('option');
option.value = normalizedCountryCode;
option.textContent = `自定义 ${normalizedCountryCode}`;
selectGpcHelperCountryCode.appendChild(option);
if (typeof inputGpcCardKey !== 'undefined' && inputGpcCardKey) {
inputGpcCardKey.value = state?.gpcCardKey || '';
if (state?.gpcBalanceError) {
setGpcCardKeyStatus(state.gpcBalanceError, 'error');
} else if (state?.gpcBalance || state?.gpcRemainingUses !== undefined) {
setGpcCardKeyStatus(formatGpcCardKeyBalanceStatus({
balance: state?.gpcBalance,
remainingUses: state?.gpcRemainingUses,
cardStatus: state?.gpcCardStatus,
}), 'ok');
} else {
setGpcCardKeyStatus(inputGpcCardKey.value ? '等待检测' : '等待输入', '');
}
selectGpcHelperCountryCode.value = normalizedCountryCode || '+86';
}
if (typeof inputGpcHelperPhone !== 'undefined' && inputGpcHelperPhone) {
inputGpcHelperPhone.value = state?.gopayHelperPhoneNumber || '';
}
if (typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel) {
selectGpcHelperOtpChannel.value = normalizeGpcOtpChannelValue(state?.gopayHelperOtpChannel || 'whatsapp');
}
if (typeof inputGpcHelperLocalSmsEnabled !== 'undefined' && inputGpcHelperLocalSmsEnabled) {
inputGpcHelperLocalSmsEnabled.checked = Boolean(state?.gopayHelperLocalSmsHelperEnabled);
}
if (typeof inputGpcHelperLocalSmsUrl !== 'undefined' && inputGpcHelperLocalSmsUrl) {
inputGpcHelperLocalSmsUrl.value = normalizeGpcLocalSmsHelperBaseUrlValue(state?.gopayHelperLocalSmsHelperUrl || '');
}
if (typeof inputGpcHelperPin !== 'undefined' && inputGpcHelperPin) {
inputGpcHelperPin.value = state?.gopayHelperPin || '';
}
if (typeof displayGpcHelperBalance !== 'undefined' && displayGpcHelperBalance) {
const balanceText = String(state?.gopayHelperBalance || '').trim();
const balanceError = String(state?.gopayHelperBalanceError || '').trim();
const balanceAt = Number(state?.gopayHelperBalanceUpdatedAt) || 0;
displayGpcHelperBalance.textContent = balanceError
? `余额查询失败:${balanceError}`
: (balanceText || (balanceAt ? '余额已更新' : '余额未获取'));
}
if (typeof selectGoPayCountryCode !== 'undefined' && selectGoPayCountryCode) {
const normalizedGoPayCountryCode = window.GoPayUtils?.normalizeGoPayCountryCode
@@ -15601,7 +15231,7 @@ stepsList?.addEventListener('click', async (event) => {
}
await persistCurrentSettingsForAction();
const gpcCreateStep = getStepIdByKeyForCurrentMode('plus-checkout-create') || 6;
if (step === gpcCreateStep && !(await ensureGpcApiKeyReadyForStart())) {
if (step === gpcCreateStep && !(await ensureGpcCardKeyReadyForStart())) {
return;
}
const shouldPersistSharedPassword = nodeId === 'fill-password'
@@ -15899,7 +15529,7 @@ async function startAutoRunFromCurrentSettings() {
clearPendingAutoRunStartRunCount();
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
}
if (!(await ensureGpcApiKeyReadyForStart())) {
if (!(await ensureGpcCardKeyReadyForStart())) {
clearPendingAutoRunStartRunCount();
return false;
}
@@ -16231,10 +15861,6 @@ btnGpcCardKeyPurchase?.addEventListener('click', () => {
openExternalUrl('https://pay.ldxp.cn/shop/gpc');
});
btnGpcHelperConvertApiKey?.addEventListener('click', () => {
openExternalUrl(GPC_HELPER_PORTAL_URL);
});
btnOpenTargetRepository?.addEventListener('click', () => {
const repositoryUrl = btnOpenTargetRepository.dataset.repositoryUrl
|| getTargetRepositoryUrl(getSelectedFlowId(), getSelectedTargetId());
@@ -16243,53 +15869,6 @@ btnOpenTargetRepository?.addEventListener('click', () => {
}
});
btnGpcHelperBalance?.addEventListener('click', async () => {
try {
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 || '',
gopayHelperCountryCode: selectGpcHelperCountryCode?.value || '+86',
reason: 'manual',
},
});
if (response?.error) {
throw new Error(response.error);
}
if (displayGpcHelperBalance) {
displayGpcHelperBalance.textContent = response?.balance || '余额已更新';
}
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 || '',
};
const nextAutoModePermission = getGpcAutoModePermissionFromPayload(nextState.gopayHelperBalancePayload);
const nextAutoModeDenied = nextAutoModePermission === false;
const nextAutoModeConfirmed = nextAutoModePermission === true || nextState.gopayHelperAutoModeEnabled;
const selectedModeBeforeBalanceState = getSelectedGpcHelperPhoneMode();
syncLatestState(nextState);
if (nextAutoModeDenied && selectedModeBeforeBalanceState === GPC_HELPER_PHONE_MODE_AUTO) {
showToast('当前 API Key 未开通自动模式,已保留当前选择;如需继续请手动切换到手动模式。', 'warn');
} else if (nextAutoModeDenied) {
showToast('GPC 余额已更新,当前 API Key 只能使用手动模式。', 'success');
} else if (nextAutoModeConfirmed) {
showToast('GPC 余额已更新,自动模式可用。', 'success');
} else {
showToast('GPC 余额已更新,当前接口未返回自动模式权限,已保留所选模式。', 'success');
}
updatePlusModeUI();
} catch (error) {
showToast(error?.message || '查询 GPC 余额失败。', 'error');
}
});
btnTestKiroRs?.addEventListener('click', async () => {
const defaultLabel = btnTestKiroRs.textContent || '测试';
btnTestKiroRs.disabled = true;
@@ -16353,15 +15932,6 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
});
[
inputGpcHelperApi,
inputGpcHelperCardKey,
selectGpcHelperPhoneMode,
selectGpcHelperCountryCode,
inputGpcHelperPhone,
selectGpcHelperOtpChannel,
inputGpcHelperLocalSmsEnabled,
inputGpcHelperLocalSmsUrl,
inputGpcHelperPin,
selectGoPayCountryCode,
inputGoPayPhone,
inputGoPayOtp,
@@ -16375,9 +15945,6 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
scheduleSettingsAutoSave();
});
input?.addEventListener('change', () => {
if (input === selectGpcHelperPhoneMode || input === selectGpcHelperOtpChannel || input === inputGpcHelperLocalSmsEnabled) {
updatePlusModeUI();
}
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
@@ -16386,6 +15953,29 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
});
});
inputGpcCardKey?.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
scheduleGpcCardKeyStatusRefresh();
});
inputGpcCardKey?.addEventListener('change', () => {
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
refreshGpcCardKeyStatus({ reason: 'change' }).catch(() => { });
});
inputGpcCardKey?.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
refreshGpcCardKeyStatus({ reason: 'blur' }).catch(() => { });
});
btnGpcCardKeyQuery?.addEventListener('click', async () => {
btnGpcCardKeyQuery.disabled = true;
try {
await refreshGpcCardKeyStatus({ reason: 'manual' });
} finally {
btnGpcCardKeyQuery.disabled = false;
}
});
selectMailProvider.addEventListener('change', async () => {
const previousProvider = latestState?.mailProvider || '';
const previousMail2925Mode = latestState?.mail2925Mode;
@@ -18791,6 +18381,25 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.plusPaymentMethod !== undefined && selectPlusPaymentMethod) {
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(message.payload.plusPaymentMethod);
}
if (message.payload.gpcCardKey !== undefined && inputGpcCardKey) {
inputGpcCardKey.value = message.payload.gpcCardKey || '';
}
if (
message.payload.gpcBalance !== undefined
|| message.payload.gpcRemainingUses !== undefined
|| message.payload.gpcCardStatus !== undefined
|| message.payload.gpcBalanceError !== undefined
) {
if (message.payload.gpcBalanceError) {
setGpcCardKeyStatus(message.payload.gpcBalanceError, 'error');
} else {
setGpcCardKeyStatus(formatGpcCardKeyBalanceStatus({
balance: latestState?.gpcBalance,
remainingUses: latestState?.gpcRemainingUses,
cardStatus: latestState?.gpcCardStatus,
}), 'ok');
}
}
if (message.payload.plusAccountAccessStrategy !== undefined && selectPlusAccountAccessStrategy) {
currentPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(message.payload.plusAccountAccessStrategy);
selectPlusAccountAccessStrategy.dataset.requestedValue = currentPlusAccountAccessStrategy;
@@ -18798,35 +18407,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
selectPlusAccountAccessStrategy.value = normalizePlusAccountAccessStrategyUiValue(currentPlusAccountAccessStrategy);
}
}
if (message.payload.gopayHelperPhoneMode !== undefined && selectGpcHelperPhoneMode) {
selectGpcHelperPhoneMode.value = normalizeGpcHelperPhoneModeValue(message.payload.gopayHelperPhoneMode);
}
if (message.payload.gopayHelperOtpChannel !== undefined && selectGpcHelperOtpChannel) {
selectGpcHelperOtpChannel.value = normalizeGpcOtpChannelValue(message.payload.gopayHelperOtpChannel);
}
if (message.payload.gopayHelperLocalSmsHelperEnabled !== undefined && inputGpcHelperLocalSmsEnabled) {
inputGpcHelperLocalSmsEnabled.checked = Boolean(message.payload.gopayHelperLocalSmsHelperEnabled);
}
if (message.payload.gopayHelperLocalSmsHelperUrl !== undefined && inputGpcHelperLocalSmsUrl) {
inputGpcHelperLocalSmsUrl.value = normalizeGpcLocalSmsHelperBaseUrlValue(message.payload.gopayHelperLocalSmsHelperUrl);
}
if (message.payload.gopayHelperBalance !== undefined || message.payload.gopayHelperBalanceError !== undefined) {
if (typeof displayGpcHelperBalance !== 'undefined' && displayGpcHelperBalance) {
const balanceText = String(message.payload.gopayHelperBalance ?? latestState?.gopayHelperBalance ?? '').trim();
const balanceError = String(message.payload.gopayHelperBalanceError ?? latestState?.gopayHelperBalanceError ?? '').trim();
displayGpcHelperBalance.textContent = balanceError
? `余额查询失败:${balanceError}`
: (balanceText || '余额已更新');
}
}
if (
message.payload.plusModeEnabled !== undefined
|| message.payload.plusPaymentMethod !== undefined
|| message.payload.plusAccountAccessStrategy !== undefined
|| message.payload.gopayHelperPhoneMode !== undefined
|| message.payload.gopayHelperAutoModeEnabled !== undefined
|| message.payload.gopayHelperOtpChannel !== undefined
|| message.payload.gopayHelperLocalSmsHelperEnabled !== undefined
) {
const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
? resolveStepDefinitionCapabilityState(latestState, {
+7 -7
View File
@@ -441,7 +441,7 @@ test('auto-run controller treats phone-number supply exhaustion as round-fatal a
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller treats ended GPC task as round-fatal and skips same-round retries', async () => {
test('auto-run controller treats ended GPC page flow as round-fatal and skips same-round retries', async () => {
const events = {
logs: [],
broadcasts: [],
@@ -528,7 +528,7 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: payload.sessionId ?? 0,
}),
getErrorMessage: (error) => String(error?.message || error || '').replace(/^GPC_TASK_ENDED::/i, ''),
getErrorMessage: (error) => String(error?.message || error || '').replace(/^GPC_PAGE_FLOW_ENDED::/i, ''),
getFirstUnfinishedStep: () => 1,
getPendingAutoRunTimerPlan: () => null,
getRunningSteps: () => [],
@@ -541,7 +541,7 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
getStopRequested: () => false,
hasSavedProgress: () => false,
isAddPhoneAuthFailure: () => false,
isGpcTaskEndedFailure: (error) => /GPC_TASK_ENDED::/i.test(error?.message || String(error || '')),
isGpcPageFlowEndedFailure: (error) => /GPC_PAGE_FLOW_ENDED::/i.test(error?.message || String(error || '')),
isRestartCurrentAttemptError: () => false,
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
launchAutoRunTimerPlan: async () => false,
@@ -558,7 +558,7 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
runAutoSequenceFromStep: async () => {
events.runCalls += 1;
if (events.runCalls === 1) {
throw new Error('GPC_TASK_ENDED::等待 OTP 超过 60 秒,任务已超时');
throw new Error('GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面等待超时,未检测到订阅完成。');
}
},
runtime,
@@ -592,12 +592,12 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
mode: 'restart',
});
assert.equal(events.runCalls, 2, 'ended GPC task should fail current round and continue next round');
assert.equal(events.runCalls, 2, 'ended GPC page flow 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)));
assert.match(events.accountRecords[0].reason, /GPC 页面等待超时/);
assert.ok(events.logs.some(({ message }) => /GPC 页面流程.*继续下一轮|继续下一轮/.test(message)));
});
test('auto-run controller keeps same-round retrying for step9 local replacement exhaustion errors', async () => {
+3 -3
View File
@@ -152,7 +152,7 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
getStopRequested: () => false,
hasSavedNodeProgress: () => false,
isAddPhoneAuthFailure: () => false,
isGpcTaskEndedFailure: () => false,
isGpcPageFlowEndedFailure: () => false,
isPhoneSmsPlatformRateLimitFailure: () => false,
isPlusCheckoutNonFreeTrialFailure: () => false,
isRestartCurrentAttemptError: () => false,
@@ -355,7 +355,7 @@ test('auto-run controller clears stale kiro completed statuses before a fresh re
getStopRequested: () => false,
hasSavedNodeProgress: () => false,
isAddPhoneAuthFailure: () => false,
isGpcTaskEndedFailure: () => false,
isGpcPageFlowEndedFailure: () => false,
isKiroProxyFailure: () => false,
isPhoneSmsPlatformRateLimitFailure: () => false,
isPlusCheckoutNonFreeTrialFailure: () => false,
@@ -536,7 +536,7 @@ test('auto-run controller stops immediately on kiro proxy failures even when ski
getStopRequested: () => false,
hasSavedNodeProgress: () => false,
isAddPhoneAuthFailure: () => false,
isGpcTaskEndedFailure: () => false,
isGpcPageFlowEndedFailure: () => false,
isKiroProxyFailure: (error) => /Kiro 注册页出现 AWS 请求异常|Kiro 注册页返回 403|切换代理|更换代理/i.test(error?.message || String(error || '')),
isPhoneSmsPlatformRateLimitFailure: () => false,
isPlusCheckoutNonFreeTrialFailure: () => false,
+13 -13
View File
@@ -752,7 +752,7 @@ test('auto-run restarts Plus checkout from step 6 when billing fails for non-fre
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 Plus Checkout/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
test('auto-run restarts GPC checkout from step 6 when step 7 page flow stalls', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
@@ -765,7 +765,7 @@ test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls
startStep: 6,
failureStep: 7,
failureBudget: 2,
failureMessage: 'GPC API 请求超时(>30 秒):https://gpc.qlhazycoder.top/api/gp/tasks/task_stalled',
failureMessage: 'GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面等待超时,未检测到订阅完成。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -785,7 +785,7 @@ test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls
events.invalidations.map((entry) => entry.step),
[5, 5]
);
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新准备 GPC 页面/.test(message)));
});
test('auto-run treats GPC account binding as recoverable step 6 restart', async () => {
@@ -801,7 +801,7 @@ test('auto-run treats GPC account binding as recoverable step 6 restart', async
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::GOPAY已经绑了订阅,需要手动解绑',
failureMessage: 'GPC_PAGE_FLOW_ENDED::GOPAY已经绑了订阅,需要手动解绑',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -817,7 +817,7 @@ test('auto-run treats GPC account binding as recoverable step 6 restart', async
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
});
test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read', async () => {
test('auto-run restarts GPC checkout from step 6 when ChatGPT session cannot be read', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
@@ -830,7 +830,7 @@ test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read
startStep: 6,
failureStep: 6,
failureBudget: 1,
failureMessage: '步骤 6GPC 模式获取 accessToken 失败。',
failureMessage: '步骤 6GPC 模式获取 ChatGPT session 失败。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -844,10 +844,10 @@ test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read
assert.deepStrictEqual(events.steps, [6, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新准备 GPC 页面/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when task status has no progress', async () => {
test('auto-run restarts GPC checkout from step 6 when page returns without completion', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
@@ -860,7 +860,7 @@ test('auto-run restarts GPC checkout from step 6 when task status has no progres
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::GPC 任务状态超过 60 秒无进展(已创建),请重新创建任务。',
failureMessage: 'GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面已尝试启动 10 次仍未显示订阅完成。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -874,7 +874,7 @@ test('auto-run restarts GPC checkout from step 6 when task status has no progres
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新准备 GPC 页面/.test(message)));
});
test('auto-run keeps rebuilding GPC checkout beyond three failures', async () => {
@@ -890,7 +890,7 @@ test('auto-run keeps rebuilding GPC checkout beyond three failures', async () =>
startStep: 6,
failureStep: 7,
failureBudget: 4,
failureMessage: 'GPC_TASK_ENDED::GPC task status stalled, recreate the task.',
failureMessage: 'GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面等待超时,未检测到订阅完成。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -947,7 +947,7 @@ test('auto-run does not restart GPC checkout when account already has a ChatGPT
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::该账号已经开通过ChatGPT订阅套餐,不能重复订阅。(checkout_order',
failureMessage: 'GPC_PAGE_FLOW_ENDED::该账号已经开通过ChatGPT订阅套餐,不能重复订阅。(checkout_order',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -962,7 +962,7 @@ test('auto-run does not restart GPC checkout when account already has a ChatGPT
assert.ok(result?.error);
assert.deepStrictEqual(result.events.steps, [6, 7]);
assert.equal(result.events.invalidations.length, 0);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
assert.ok(!result.events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新准备 GPC 页面/.test(message)));
});
test('auto-run does not reroute SUB2API session import failures into OAuth restarts', async () => {
@@ -58,7 +58,6 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneMode'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizePhoneSmsProviderOrder'),
extractFunction('normalizeSignupMethod'),
@@ -186,22 +185,23 @@ const self = {
normalizeGoPayPin(value) {
return String(value || '').trim().replace(/[^\\d]/g, '');
},
normalizeGpcHelperBaseUrl(value) {
return String(value || '')
normalizeGpcBaseUrl(value) {
return String(value || 'https://gpc.qlhazycoder.top')
.trim()
.replace(/\\/+$/g, '')
.replace(/\\/api\\/checkout\\/start$/i, '')
.replace(/\\/api\\/gopay\\/(?:otp|pin)$/i, '')
.replace(/\\/api\\/gp\\/tasks(?:\\/[^/?#]+)?(?:\\/(?:otp|pin|stop))?(?:\\?.*)?$/i, '')
.replace(/\\/api\\/gp\\/balance(?:\\?.*)?$/i, '')
.replace(/\\/api\\/web\\/card\\/balance(?:\\?.*)?$/i, '')
.replace(/\\/api\\/card\\/balance(?:\\?.*)?$/i, '')
.replace(/\\/api\\/card\\/redeem-api-key(?:\\?.*)?$/i, '');
|| 'https://gpc.qlhazycoder.top';
},
normalizeGpcCardKey(value) {
return String(value || '').trim().toUpperCase();
},
},
};
const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null,
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top',
gpcBaseUrl: 'https://gpc.qlhazycoder.top',
mailProvider: '163',
heroSmsMinPrice: '',
fiveSimMinPrice: '',
@@ -249,37 +249,19 @@ return {
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'cpa_codex_session'), 'cpa_codex_session');
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'unknown'), 'oauth');
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/checkout/start '),
api.normalizePersistentSettingValue('gpcBaseUrl', ' https://gpc.qlhazycoder.top/api/checkout/start '),
'https://gpc.qlhazycoder.top'
);
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/gp/tasks/task_1/pin '),
api.normalizePersistentSettingValue('gpcBaseUrl', ' https://gpc.qlhazycoder.top/api/web/card/balance?card_key=old '),
'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('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');
assert.equal(api.normalizePersistentSettingValue('gopayHelperOtpChannel', 'SMS'), 'sms');
assert.equal(api.normalizePersistentSettingValue('gopayHelperOtpChannel', 'unknown'), 'whatsapp');
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsHelperEnabled', 1), true);
assert.equal(
api.normalizePersistentSettingValue('gopayHelperLocalSmsHelperUrl', 'http://127.0.0.1:18767/otp?x=1'),
'http://127.0.0.1:18767'
);
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsTimeoutSeconds', '999'), 300);
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsPollIntervalSeconds', '0'), 1);
assert.equal(api.normalizePersistentSettingValue('gpcBaseUrl', ''), 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizePersistentSettingValue('gpcCardKey', ' gpc-6c9f1a32-45734795-914e6f00 '), 'GPC-6C9F1A32-45734795-914E6F00');
assert.equal(api.normalizePersistentSettingValue('gpcRemainingUses', '998'), 998);
assert.equal(api.normalizePersistentSettingValue('gpcCardStatus', ' active '), 'active');
assert.equal(api.normalizePersistentSettingValue('gpcPageStatus', ' running '), 'running');
assert.equal(api.normalizePersistentSettingValue('gpcPageStatusText', ' 页面启动 '), '页面启动');
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '9'), 9);
@@ -48,8 +48,8 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
assert.equal(loggingStatus.getLoginAuthStateLabel('choose_account_page'), 'OpenAI choose account page');
assert.equal(
loggingStatus.getErrorMessage(new Error('GPC_TASK_ENDED::GPC OTP 超时,请重新创建任务')),
'GPC OTP 超时,请重新创建任务'
loggingStatus.getErrorMessage(new Error('GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面等待超时,未检测到订阅完成。')),
'步骤 7:GPC 页面等待超时,未检测到订阅完成。'
);
assert.equal(
loggingStatus.isKiroProxyFailure('Kiro 注册页出现 AWS 请求异常,通常是当前代理 IP 或出口区域异常,请先切换代理后再重试。'),
@@ -233,7 +233,7 @@ function createRouter(overrides = {}) {
verifyHotmailAccount: async () => {},
refreshGpcCardBalance: overrides.refreshGpcCardBalance || (async (state, options) => {
events.balanceRefreshes.push({ state, options });
return { balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' };
return { balance: '余额 3', remainingUses: 3, cardStatus: 'active' };
}),
});
@@ -868,39 +868,11 @@ test('message router delegates Kiro manual step 4 without OpenAI auth-tab prereq
assert.deepStrictEqual(events.executedSteps, [4]);
});
test('message router resolves GPC OTP manual confirmation without completing step early', async () => {
const state = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'otp-request-1',
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
};
const { router, events } = createRouter({ state });
const response = await router.handleMessage({
type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION',
source: 'sidepanel',
payload: {
step: 7,
requestId: 'otp-request-1',
confirmed: true,
otp: ' 12-34 56 ',
},
}, {});
assert.deepStrictEqual(response, { ok: true });
assert.equal(events.notifyCompletions.length, 0);
assert.equal(events.stepStatuses.length, 0);
assert.equal(events.stateUpdates[0].gopayHelperResolvedOtp, '123456');
assert.equal(events.stateUpdates[0].plusManualConfirmationPending, false);
assert.deepStrictEqual(events.broadcasts[0], events.stateUpdates[0]);
});
test('message router refreshes GPC balance through explicit sidepanel message', async () => {
const state = {
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'http://localhost:18473/',
gopayHelperApiKey: 'state_api_key',
gpcBaseUrl: 'http://localhost:18473/',
gpcCardKey: 'GPC-11111111-22222222-33333333',
};
const { router, events } = createRouter({ state });
@@ -908,15 +880,15 @@ test('message router refreshes GPC balance through explicit sidepanel message',
type: 'REFRESH_GPC_CARD_BALANCE',
source: 'sidepanel',
payload: {
gopayHelperApiKey: 'payload_api_key',
gpcCardKey: 'GPC-6C9F1A32-45734795-914E6F00',
reason: 'manual',
},
}, {});
assert.deepStrictEqual(response, { ok: true, balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' });
assert.deepStrictEqual(response, { ok: true, balance: '余额 3', remainingUses: 3, cardStatus: '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');
assert.equal(events.balanceRefreshes[0].state.gpcBaseUrl, 'http://localhost:18473/');
assert.equal(events.balanceRefreshes[0].state.gpcCardKey, 'GPC-6C9F1A32-45734795-914E6F00');
assert.deepStrictEqual(events.balanceRefreshes[0].options, { reason: 'manual' });
});
@@ -410,7 +410,7 @@ return {
cloudflareTempEmailBaseUrl: 'https://mail.example.com',
cloudflareTempEmailAdminAuth: 'admin-secret',
cloudMailToken: 'cloud-token',
gopayHelperApiKey: 'gpc-key',
gpcCardKey: 'GPC-6C9F1A32-45734795-914E6F00',
heroSmsApiKey: 'hero-key',
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
@@ -443,7 +443,7 @@ return {
assert.equal(api.getPayloadInput().cloudflareTempEmailBaseUrl, 'https://mail.example.com');
assert.equal(api.getPayloadInput().cloudflareTempEmailAdminAuth, 'admin-secret');
assert.equal(api.getPayloadInput().cloudMailToken, 'cloud-token');
assert.equal(api.getPayloadInput().gopayHelperApiKey, 'gpc-key');
assert.equal(api.getPayloadInput().gpcCardKey, 'GPC-6C9F1A32-45734795-914E6F00');
assert.equal(api.getPayloadInput().heroSmsApiKey, 'hero-key');
assert.equal(api.getPayloadInput().phoneSmsProvider, 'madao');
assert.equal(api.getPayloadInput().madaoBaseUrl, 'http://127.0.0.1:7822');
+18 -51
View File
@@ -12,16 +12,9 @@ test('GoPay utils normalize manual OTP input', () => {
const api = loadGoPayUtils();
assert.equal(api.normalizeGoPayOtp(' 12-34 56 '), '123456');
assert.equal(api.normalizeGoPayOtp('abc'), '');
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', () => {
test('GoPay utils keeps GPC payment method distinct', () => {
const api = loadGoPayUtils();
assert.equal(api.normalizePlusPaymentMethod('paypal-hosted'), 'paypal-hosted');
assert.equal(api.normalizePlusPaymentMethod('paypal_direct'), 'paypal-hosted');
@@ -32,51 +25,26 @@ test('GoPay utils keeps GPC helper payment method distinct', () => {
assert.equal(api.normalizePlusPaymentMethod('unknown'), 'paypal');
});
test('GoPay utils builds GPC queue task and balance URLs from helper endpoints', () => {
test('GoPay utils builds GPC card balance URLs from portal endpoints', () => {
const api = loadGoPayUtils();
assert.equal(api.DEFAULT_GPC_HELPER_API_URL, 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcHelperBaseUrl('https://example.com/api/gp/tasks'), 'https://gpc.qlhazycoder.top');
assert.equal(api.DEFAULT_GPC_BASE_URL, 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcBaseUrl(''), 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcBaseUrl('https://example.com/api/web/card/balance'), 'https://gpc.qlhazycoder.top');
assert.equal(
api.buildGpcHelperApiUrl('', '/api/checkout/start'),
api.buildGpcApiUrl('', '/api/checkout/start'),
'https://gpc.qlhazycoder.top/api/checkout/start'
);
assert.equal(
api.buildGpcApiKeyBalanceUrl('http://localhost:18473/'),
'http://localhost:18473/api/gp/balance'
api.buildGpcCardBalanceUrl('https://gpc.qlhazycoder.top/api/web/card/balance'),
'https://gpc.qlhazycoder.top/api/web/card/balance'
);
assert.equal(
api.buildGpcCardBalanceUrl('https://gpc.qlhazycoder.top/api/gp/balance'),
'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(
api.buildGpcTaskCreateUrl('https://gpc.qlhazycoder.top/api/checkout/start'),
'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 queue OTP/PIN payloads without card_key', () => {
const api = loadGoPayUtils();
assert.deepEqual(
api.buildGpcTaskOtpPayload({ otp: ' 12-34 56 ', card_key: ' card_1 ', reference_id: 'ref_1' }),
{ otp: '123456' }
);
assert.deepEqual(
api.buildGpcTaskPinPayload({ pin: '65-43-21', cardKey: 'card_1', challengeId: 'challenge_1' }),
{ pin: '654321' }
api.buildGpcCardBalanceUrl('https://gpc.qlhazycoder.top/api/web/card/balance?card_key=old', ' gpc-6c9f1a32-45734795-914e6f00 '),
'https://gpc.qlhazycoder.top/api/web/card/balance?card_key=GPC-6C9F1A32-45734795-914E6F00'
);
assert.equal(api.normalizeGpcCardKey(' gpc-6c9f1a32-45734795-914e6f00 '), 'GPC-6C9F1A32-45734795-914E6F00');
assert.equal(api.isGpcCardKeyFormat('GPC-6C9F1A32-45734795-914E6F00'), true);
assert.equal(api.isGpcCardKeyFormat('card-key-1'), false);
});
test('GoPay utils formats balance and maps linked-account errors', () => {
@@ -102,15 +70,14 @@ test('GoPay utils formats balance and maps linked-account errors', () => {
'余额 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.equal(api.getGpcCardStatus({ data: { status: 'active' } }), 'active');
assert.deepEqual(
api.unwrapGpcResponse({ code: 200, message: 'ok', data: { task_id: 'task_1' } }),
{ task_id: 'task_1' }
api.unwrapGpcResponse({ code: 200, message: 'ok', data: { remaining_uses: 1 } }),
{ remaining_uses: 1 }
);
assert.equal(
api.extractGpcResponseErrorDetail({ errors: [{ loc: ['body', 'otp'], msg: 'Field required' }] }, 422),
'body.otp: Field required'
api.extractGpcResponseErrorDetail({ errors: [{ loc: ['query', 'card_key'], msg: 'Field required' }] }, 422),
'query.card_key: Field required'
);
assert.equal(
api.extractGpcResponseErrorDetail({
-201
View File
@@ -1,201 +0,0 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const scriptPath = path.join(process.cwd(), 'scripts/gpc_sms_helper_macos.py');
function runPython(args, options = {}) {
for (const command of ['python3', 'python']) {
const result = spawnSync(command, args, {
encoding: 'utf8',
...options,
env: {
...process.env,
PYTHONIOENCODING: 'utf-8',
...(options.env || {}),
},
});
if (result.error && result.error.code === 'ENOENT') {
continue;
}
if (process.platform === 'win32' && result.status === 9009) {
continue;
}
return result;
}
return null;
}
test('GPC SMS helper shows macOS and iPhone forwarding guidance on non-macOS', () => {
const help = runPython([scriptPath, '--help']);
if (!help) {
return;
}
assert.equal(help.status, 0);
if (process.platform === 'darwin') {
return;
}
const run = runPython([scriptPath, '--db', '/tmp/nonexistent-gpc-chat.db'], {
timeout: 3000,
});
assert.ok(run);
assert.notEqual(run.status, 0);
assert.match(run.stderr, /仅支持 macOS/);
assert.match(run.stderr, /iPhone 短信已转发|短信转发/);
});
test('GPC SMS helper filters cached OTP records by order timestamp', () => {
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)
old = {"otp": "111111", "code": "111111", "message_id": "old", "received_at": "2026-05-05T00:00:00+00:00"}
fresh = {"otp": "222222", "code": "222222", "message_id": "fresh", "received_at": "2026-05-05T00:00:10+00:00"}
state = {"last_otp": fresh, "otps": [fresh, old]}
payload = {
"without_filter": module.select_otp_record(state, 0)["otp"],
"fresh_only": module.select_otp_record(state, module.parse_timestamp_ms("2026-05-05T00:00:05+00:00"))["otp"],
"none_after_fresh": module.select_otp_record(state, module.parse_timestamp_ms("2026-05-05T00:00:11+00:00")) is None,
}
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()), {
without_filter: '222222',
fresh_only: '222222',
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',
});
});
@@ -60,8 +60,6 @@ const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
${extractFunction('normalizePlusAccountAccessStrategy')}
${extractFunction('getRequestedPlusAccountAccessStrategy')}
${extractFunction('updatePlusModeUI')}
@@ -72,15 +70,6 @@ function normalizePlusPaymentMethod(value = '') {
function getSelectedPlusPaymentMethod() {
return normalizePlusPaymentMethod(selectPlusPaymentMethod.value || latestState?.plusPaymentMethod || currentPlusPaymentMethod || 'paypal');
}
function normalizeGpcHelperPhoneModeValue(value = '') {
return String(value || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual';
}
function normalizeGpcOtpChannelValue(value = '') {
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
}
function isGpcAutoModePermissionDenied() {
return false;
}
function getSelectedPanelMode() {
return latestState?.targetId || 'cpa';
}
File diff suppressed because it is too large Load Diff
+151 -464
View File
@@ -156,37 +156,6 @@ function createCheckoutContentHarness() {
return { checkoutEvents, send };
}
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({
@@ -878,435 +847,199 @@ test('Plus checkout content routes same-frame autocomplete query and suggestion
assert.equal(checkoutEvents.some((event) => event.type === 'delay' && event.ms !== 2000), false);
});
test('GPC manual checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
function createFakeGpcPrepareDom() {
const dispatches = [];
function createElement({ tagName = 'DIV', text = '', placeholder = '', className = '', value = '', disabled = false } = {}) {
return {
tagName,
textContent: text,
innerText: text,
placeholder,
className,
value,
disabled,
clicked: false,
style: { display: 'block', visibility: 'visible', opacity: '1' },
name: '',
id: '',
getAttribute(name) {
if (name === 'class') return this.className;
if (name === 'aria-disabled') return this.disabled ? 'true' : '';
if (name === 'placeholder') return this.placeholder;
return '';
},
focus() {},
blur() {},
scrollIntoView() {},
click() {
this.clicked = true;
},
dispatchEvent(event) {
dispatches.push({ element: this, type: event?.type || '' });
return true;
},
getBoundingClientRect() {
return { width: 120, height: 32 };
},
};
}
const freeButton = createElement({ tagName: 'BUTTON', text: '免费充值' });
const cardModeButton = createElement({ tagName: 'BUTTON', text: '卡密充值 使用付费卡密扣次充值' });
const startButton = createElement({ tagName: 'BUTTON', text: '开始 Plus 充值' });
const cardInputs = [
createElement({ tagName: 'INPUT', placeholder: 'XXXXXXXX', className: 'card-key-seg' }),
createElement({ tagName: 'INPUT', placeholder: 'XXXXXXXX', className: 'card-key-seg' }),
createElement({ tagName: 'INPUT', placeholder: 'XXXXXXXX', className: 'card-key-seg' }),
];
const sessionTextarea = createElement({
tagName: 'TEXTAREA',
placeholder: '可直接粘贴完整 session JSON,会自动提取 accessToken',
className: 'design-session-input',
});
return {
cardModeButton,
cardInputs,
dispatches,
sessionTextarea,
document: {
querySelectorAll(selector) {
const text = String(selector || '');
if (text.includes('button') || text.includes('[role="button"]') || text.includes('.design-mode-card')) {
return [freeButton, cardModeButton, startButton];
}
if (text.includes('input.card-key-seg') || text.includes('placeholder*="XXXXXXXX"') || text.includes('maxlength="8"')) {
return cardInputs;
}
if (text === 'textarea') {
return [sessionTextarea];
}
if (text === 'input') {
return cardInputs;
}
return [];
},
querySelector(selector) {
return String(selector || '').includes('textarea') ? sessionTextarea : null;
},
body: { innerText: 'SYSTEM 页面已就绪' },
documentElement: { innerText: 'SYSTEM 页面已就绪' },
},
};
}
test('GPC checkout prepare opens page, fills segmented card key, and writes full ChatGPT session JSON', async () => {
const events = [];
const fetchCalls = [];
const dom = createFakeGpcPrepareDom();
const session = {
user: { email: 'current@example.com' },
accessToken: 'session-access-token',
expires: '2026-06-01T00:00:00.000Z',
};
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }),
chrome: {
tabs: {
create: async (payload) => {
events.push({ type: 'tab-create', payload });
return { id: 77 };
return { id: payload.url === 'https://gpc.qlhazycoder.top/' ? 77 : 88, url: payload.url };
},
get: async () => null,
query: async () => [],
remove: async (tabId) => events.push({ type: 'tab-remove', tabId }),
update: async (tabId, payload) => events.push({ type: 'tab-update', tabId, payload }),
},
scripting: {
executeScript: async (details) => {
events.push({ type: 'execute-script', target: details.target, args: details.args });
const previous = {
document: global.document,
window: global.window,
location: global.location,
Event: global.Event,
};
global.document = dom.document;
global.window = { getComputedStyle: (element) => element?.style || {} };
global.location = { href: 'https://gpc.qlhazycoder.top/' };
global.Event = class TestEvent { constructor(type) { this.type = type; } };
try {
return [{ result: details.func(...(details.args || [])) }];
} finally {
global.document = previous.document;
global.window = previous.window;
global.location = previous.location;
global.Event = previous.Event;
}
},
},
},
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }),
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => url.endsWith('/api/gp/balance')
? createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 })
: createGpcTaskResponse({ otp_channel: 'whatsapp' }),
};
createAutomationTab: async (payload) => {
events.push({ type: 'automation-tab-create', payload });
return { id: payload.url === 'https://gpc.qlhazycoder.top/' ? 77 : 88, url: payload.url };
},
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }),
queryTabsInAutomationWindow: async () => [],
registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }),
sendTabMessageUntilStopped: async (tabId, source, message) => {
events.push({ type: 'tab-message', tabId, source, message });
return { accessToken: 'session-access-token' };
return { session, accessToken: session.accessToken };
},
setState: async (payload) => events.push({ type: 'set-state', payload }),
sleepWithStop: async (ms) => events.push({ type: 'sleep', ms }),
waitForTabCompleteUntilStopped: async () => events.push({ type: 'tab-complete' }),
waitForTabCompleteUntilStopped: async (tabId) => events.push({ type: 'tab-complete', tabId }),
});
await executor.executePlusCheckoutCreate({
email: 'Current.Round+GPC@Example.COM',
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'manual',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperPhoneNumber: '+8613800138000',
gopayPhone: '',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_test_123',
gpcCardKey: 'AAAA1111BBBB2222CCCC3333',
});
const readyIndex = events.findIndex((event) => event.type === 'ready');
const messageIndex = events.findIndex((event) => event.type === 'tab-message');
assert.ok(readyIndex >= 0);
assert.ok(messageIndex > readyIndex);
assert.equal(events[messageIndex].message.type, 'PLUS_CHECKOUT_GET_STATE');
assert.deepEqual(events[messageIndex].message.payload, {
assert.equal(dom.cardModeButton.clicked, true);
assert.deepEqual(dom.cardInputs.map((input) => input.value), ['AAAA1111', 'BBBB2222', 'CCCC3333']);
assert.equal(dom.sessionTextarea.value, JSON.stringify(session));
const sessionMessage = events.find((event) => event.type === 'tab-message');
assert.deepEqual(sessionMessage.message.payload, {
includeSession: true,
includeAccessToken: true,
});
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',
country_code: '86',
phone_number: '13800138000',
otp_channel: 'whatsapp',
});
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);
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?.gopayHelperTaskId, 'task_123');
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);
const executeEvent = events.find((event) => event.type === 'execute-script');
assert.equal(executeEvent.target.tabId, 77);
assert.equal(executeEvent.args[1], JSON.stringify(session));
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
assert.equal(statePayload.plusCheckoutTabId, 77);
assert.equal(statePayload.plusCheckoutSource, 'gpc-helper');
assert.equal(statePayload.gpcPageStatus, 'prepared');
assert.equal(statePayload.gpcPageStatusText, '页面已准备');
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
});
test('GPC auto checkout only sends access token and API Key', async () => {
test('GPC checkout prepare rejects missing card key before opening pages', 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 () => {},
},
},
completeNodeFromBackground: 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(Object.prototype.hasOwnProperty.call(statePayload, 'gopayHelperPhoneMode'), false);
assert.equal(statePayload.gopayHelperTaskStatus, 'queued');
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
});
test('GPC auto checkout keeps running when balance payload omits auto mode permission', async () => {
const events = [];
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');
events.push({ type: 'tab-create' });
throw new Error('should not open tab without card key');
},
remove: async () => {},
},
},
completeNodeFromBackground: 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: undefined, remaining_uses: 998 })
: createGpcTaskResponse({
task_id: 'task_auto_unknown_permission',
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[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
const helperPayload = JSON.parse(fetchCalls[1].options.body);
assert.equal(helperPayload.phone_mode, 'auto');
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
assert.equal(statePayload.gopayHelperTaskId, 'task_auto_unknown_permission');
assert.equal(Object.prototype.hasOwnProperty.call(statePayload, 'gopayHelperPhoneMode'), false);
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
});
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');
scripting: {
executeScript: async () => {
throw new Error('should not inject without card key');
},
remove: async () => {},
},
},
completeNodeFromBackground: async () => {},
createAutomationTab: async () => {
events.push({ type: 'automation-tab-create' });
throw new Error('should not open tab without card key');
},
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 () => {},
},
},
completeNodeFromBackground: 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({
addLog: async () => {},
chrome: {
tabs: {
create: async () => ({ id: 88 }),
remove: async () => {},
},
},
completeNodeFromBackground: async () => {},
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: false, remaining_uses: 998 })
: createGpcTaskResponse({ task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' }),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({ accessToken: 'session-access-token' }),
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await executor.executePlusCheckoutCreate({
email: 'sms@example.com',
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_sms',
gopayHelperOtpChannel: 'sms',
});
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[1].options.headers['X-API-Key'], 'gpc_sms');
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
});
test('GPC checkout surfaces unified queue API errors', 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 () => {},
},
},
completeNodeFromBackground: 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,
json: async () => ({
code: 400,
message: 'invalid_param',
data: { detail: 'access_token 无效' },
}),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
email: 'paid@example.com',
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_paid_456',
}),
/创建 GPC 订单失败:access_token 无效/
);
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 () => {
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 () => {},
},
},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async () => {
throw new Error('should not call helper API without helper phone');
},
queryTabsInAutomationWindow: async () => [],
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
@@ -1317,55 +1050,9 @@ 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',
gopayPhone: '+8613800138000',
gopayCountryCode: '+86',
gopayPin: '123456',
gopayHelperPhoneNumber: '',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_phone_test',
gpcCardKey: '',
}),
/缺少手机号/
);
});
test('GPC checkout rejects missing API Key before calling helper API', async () => {
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 () => {},
},
},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async () => {
throw new Error('should not call helper API without API Key');
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
email: 'missing-card@example.com',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: '',
}),
/缺少 API Key/
/缺少卡密/
);
assert.equal(events.length, 0);
});
@@ -134,7 +134,7 @@ async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
}
async function ensureGpcApiKeyReadyForStart() {
async function ensureGpcCardKeyReadyForStart() {
return true;
}
${bundle}
@@ -323,7 +323,7 @@ async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
return null;
}
async function ensureGpcApiKeyReadyForStart() {
async function ensureGpcCardKeyReadyForStart() {
return true;
}
${bundle}
@@ -204,6 +204,7 @@ const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function normalizeGpcCardKeyInput(value = '') { return String(value || '').trim().toUpperCase(); }
const inputVpsUrl = { value: 'https://panel.example.com' };
const inputVpsPassword = { value: 'panel-secret' };
const inputSub2ApiUrl = { value: 'https://sub.example.com' };
+1
View File
@@ -95,6 +95,7 @@ const selectCfDomain = { value: '' };
const selectTempEmailDomain = { value: '' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function normalizeGpcCardKeyInput(value = '') { return String(value || '').trim().toUpperCase(); }
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
@@ -168,6 +168,7 @@ const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function normalizeGpcCardKeyInput(value = '') { return String(value || '').trim().toUpperCase(); }
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
@@ -1245,6 +1245,7 @@ function getCloudflareTempEmailDomainsFromState() { return { domains: [], active
function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
function getSelectedLocalCpaStep9Mode() { return 'submit'; }
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function normalizeGpcCardKeyInput(value = '') { return String(value || '').trim().toUpperCase(); }
function getSelectedMail2925Mode() { return 'provide'; }
function getSelectedHotmailServiceMode() { return 'local'; }
function buildManagedAliasBaseEmailPayload() { return { gmailBaseEmail: '', mail2925BaseEmail: '', emailPrefix: '' }; }
+36 -485
View File
@@ -151,14 +151,6 @@ test('sidepanel Plus UI hides PayPal account selector while GoPay is selected',
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
@@ -168,8 +160,6 @@ let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = 'oauth';
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 PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
@@ -195,14 +185,6 @@ test('sidepanel Plus UI separates PayPal account mode from PayPal no-card bindin
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
@@ -212,8 +194,6 @@ let currentPlusPaymentMethod = 'paypal-hosted';
let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'paypal-hosted', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
@@ -256,14 +236,6 @@ test('sidepanel Plus UI supports no-payment mode without payment-specific rows',
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
@@ -273,8 +245,6 @@ let currentPlusPaymentMethod = 'none';
let currentPlusAccountAccessStrategy = 'sub2api_codex_session';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'none', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_PAYMENT_METHOD_NONE = 'none';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
@@ -286,7 +256,7 @@ const rowHostedCheckoutVerificationUrl = { style: { display: '' } };
const rowHostedCheckoutPhone = { style: { display: '' } };
const rowPlusHostedCheckoutOauthDelay = { style: { display: '' } };
const rowGoPayPhone = { style: { display: '' } };
const rowGpcHelperApi = { style: { display: '' } };
const rowGpcCardKey = { style: { display: '' } };
${bundle}
return {
updatePlusModeUI,
@@ -297,7 +267,7 @@ return {
rowHostedCheckoutPhone,
rowPlusHostedCheckoutOauthDelay,
rowGoPayPhone,
rowGpcHelperApi,
rowGpcCardKey,
},
};
`)();
@@ -316,14 +286,6 @@ test('sidepanel Plus UI can hide Plus controls when the shared flow capability r
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
@@ -349,8 +311,6 @@ const rowPlusMode = { style: { display: '' } };
const selectPlusPaymentMethod = { value: 'paypal', style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: '' } };
const rowPayPalAccount = { style: { display: '' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
@@ -373,7 +333,7 @@ return {
assert.equal(api.selectPlusPaymentMethod.style.display, 'none');
});
test('sidepanel step definitions keep GPC helper mode distinct', () => {
test('sidepanel step definitions keep GPC payment mode distinct', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusPaymentMethod'),
@@ -436,25 +396,15 @@ test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () =
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperAutoModeEnabled: true };
let latestState = { plusPaymentMethod: 'gpc-helper' };
let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = 'oauth';
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 PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
@@ -463,18 +413,7 @@ 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' } };
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
const inputGpcHelperLocalSmsEnabled = { checked: false };
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
const rowGpcHelperPin = { style: { display: 'none' } };
const rowGpcCardKey = { style: { display: 'none' } };
const rowGoPayCountryCode = { style: { display: 'none' } };
const rowGoPayPhone = { style: { display: 'none' } };
const rowGoPayOtp = { style: { display: 'none' } };
@@ -483,13 +422,10 @@ ${bundle}
return {
updatePlusModeUI,
selectPlusPaymentMethod,
selectGpcHelperPhoneMode,
selectGpcHelperOtpChannel,
inputGpcHelperLocalSmsEnabled,
btnGpcCardKeyPurchase,
rowPayPalAccount,
plusPaymentMethodCaption,
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperPhoneMode, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
rows: { rowGpcCardKey },
};
`)();
@@ -497,374 +433,61 @@ return {
assert.equal(api.rowPayPalAccount.style.display, 'none');
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, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /GPC/);
api.inputGpcHelperLocalSmsEnabled.checked = true;
api.updatePlusModeUI();
assert.equal(api.selectGpcHelperOtpChannel.value, 'whatsapp');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectGpcHelperOtpChannel.value = 'sms';
api.updatePlusModeUI();
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, true);
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, /自动/);
assert.equal(api.rows.rowGpcCardKey.style.display, '');
assert.equal(api.plusPaymentMethodCaption.textContent, 'GPC 网页充值链路');
api.selectPlusPaymentMethod.value = 'gopay';
api.updatePlusModeUI();
assert.equal(api.btnGpcCardKeyPurchase.style.display, 'none');
assert.equal(api.rows.rowGpcHelperApi.style.display, 'none');
assert.equal(api.rows.rowGpcCardKey.style.display, 'none');
assert.equal(api.rowPayPalAccount.style.display, 'none');
});
test('sidepanel keeps selected GPC auto mode when API Key has no auto permission', () => {
test('sidepanel start check only requires a GPC card key', async () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
extractFunction('normalizeGpcCardKeyInput'),
extractFunction('isGpcCardKeyInputFormat'),
extractFunction('setGpcCardKeyStatus'),
extractFunction('ensureGpcCardKeyReadyForStart'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: { auto_mode_enabled: false } };
let currentPlusPaymentMethod = 'gpc-helper';
let currentPlusAccountAccessStrategy = 'oauth';
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 PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
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, '');
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 keeps selected GPC auto mode when persisted permission survives stop refresh', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = {
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'auto',
gopayHelperAutoModeEnabled: true,
gopayHelperBalancePayload: { auto_mode_enabled: true },
};
let currentPlusPaymentMethod = 'gpc-helper';
let currentPlusAccountAccessStrategy = 'oauth';
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 PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
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}
function syncLatestState(nextState) { latestState = { ...latestState, ...nextState }; }
return {
updatePlusModeUI,
selectGpcHelperPhoneMode,
getSelectedPhoneMode() { return selectGpcHelperPhoneMode.value; },
getPayloadPhoneMode() {
return (() => {
return normalizeGpcHelperPhoneModeValue(selectGpcHelperPhoneMode.value);
})();
},
applyDataUpdated(payload) {
syncLatestState(payload);
if (payload.gopayHelperPhoneMode !== undefined) {
selectGpcHelperPhoneMode.value = normalizeGpcHelperPhoneModeValue(payload.gopayHelperPhoneMode);
}
updatePlusModeUI();
},
rows: { rowGpcHelperPhoneMode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperPin },
};
`)();
api.updatePlusModeUI();
assert.equal(api.getSelectedPhoneMode(), 'auto');
assert.equal(api.getPayloadPhoneMode(), 'auto');
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
api.applyDataUpdated({
autoRunning: false,
autoRunPhase: 'stopped',
gopayHelperAutoModeEnabled: false,
});
assert.equal(api.getSelectedPhoneMode(), 'auto');
assert.equal(api.getPayloadPhoneMode(), 'auto');
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
});
test('sidepanel keeps selected GPC auto mode before permission has been queried', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
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';
let currentPlusAccountAccessStrategy = 'oauth';
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 PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
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 start check keeps GPC auto mode when balance payload omits permission field', async () => {
const bundle = [
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcRemainingUsesValue'),
extractFunction('ensureGpcApiKeyReadyForStart'),
].join('\n');
const api = new Function(`
let latestState = { gopayHelperPhoneMode: 'auto' };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const selectGpcHelperPhoneMode = { value: 'auto' };
let latestState = { gpcCardKey: '' };
const inputGpcCardKey = { value: '' };
const displayGpcCardKeyStatus = { textContent: '', dataset: {} };
const dialogs = [];
let saveCalls = 0;
let updateCalls = 0;
const toasts = [];
const window = { GoPayUtils: null };
${bundle}
function isGpcHelperCheckoutSelected() { return true; }
function getSelectedGpcHelperPhoneMode() { return selectGpcHelperPhoneMode.value; }
async function refreshGpcBalanceForStart() {
return {
gopayHelperRemainingUses: 998,
gopayHelperApiKeyStatus: 'active',
gopayHelperAutoModeEnabled: false,
gopayHelperBalancePayload: {
status: 'active',
remaining_uses: 998,
},
};
}
async function showGpcStartBlockedDialog(message) {
dialogs.push(message);
}
function syncLatestState(nextState) {
latestState = { ...latestState, ...nextState };
function showToast(message, type, duration) {
toasts.push({ message, type, duration });
}
function updatePlusModeUI() {
updateCalls += 1;
}
async function saveSettings() {
saveCalls += 1;
}
function showToast() {}
return {
ensureGpcApiKeyReadyForStart,
selectGpcHelperPhoneMode,
ensureGpcCardKeyReadyForStart,
inputGpcCardKey,
displayGpcCardKeyStatus,
setLatestState(nextState) { latestState = { ...latestState, ...nextState }; },
getDialogs: () => dialogs.slice(),
getSaveCalls: () => saveCalls,
getUpdateCalls: () => updateCalls,
getPersistedPhoneMode: () => latestState.gopayHelperPhoneMode,
getToasts: () => toasts.slice(),
};
`)();
const allowed = await api.ensureGpcApiKeyReadyForStart();
assert.equal(await api.ensureGpcCardKeyReadyForStart(), false);
assert.deepEqual(api.getDialogs(), ['请先填写 GPC 卡密。']);
assert.equal(allowed, true);
assert.equal(api.selectGpcHelperPhoneMode.value, 'auto');
assert.equal(api.getPersistedPhoneMode(), 'auto');
assert.equal(api.getSaveCalls(), 0);
assert.equal(api.getUpdateCalls(), 0);
assert.deepEqual(api.getDialogs(), []);
});
api.inputGpcCardKey.value = ' card-key-1 ';
assert.equal(await api.ensureGpcCardKeyReadyForStart(), false);
assert.match(api.getDialogs()[1], /GPC/);
assert.equal(api.displayGpcCardKeyStatus.dataset.tone, 'error');
test('sidepanel start check blocks unsupported GPC auto mode without rewriting selection', async () => {
const bundle = [
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcRemainingUsesValue'),
extractFunction('ensureGpcApiKeyReadyForStart'),
].join('\n');
const api = new Function(`
let latestState = { gopayHelperPhoneMode: 'auto' };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const selectGpcHelperPhoneMode = { value: 'auto' };
const dialogs = [];
let saveCalls = 0;
let updateCalls = 0;
${bundle}
function isGpcHelperCheckoutSelected() { return true; }
function getSelectedGpcHelperPhoneMode() { return selectGpcHelperPhoneMode.value; }
async function refreshGpcBalanceForStart() {
return {
gopayHelperRemainingUses: 998,
gopayHelperApiKeyStatus: 'active',
gopayHelperAutoModeEnabled: false,
gopayHelperBalancePayload: {
status: 'active',
remaining_uses: 998,
auto_mode_enabled: false,
},
};
}
async function showGpcStartBlockedDialog(message) {
dialogs.push(message);
}
function syncLatestState(nextState) {
latestState = { ...latestState, ...nextState };
}
function updatePlusModeUI() {
updateCalls += 1;
}
async function saveSettings() {
saveCalls += 1;
}
function showToast() {}
return {
ensureGpcApiKeyReadyForStart,
selectGpcHelperPhoneMode,
getDialogs: () => dialogs.slice(),
getSaveCalls: () => saveCalls,
getUpdateCalls: () => updateCalls,
getPersistedPhoneMode: () => latestState.gopayHelperPhoneMode,
};
`)();
const allowed = await api.ensureGpcApiKeyReadyForStart();
assert.equal(allowed, false);
assert.equal(api.selectGpcHelperPhoneMode.value, 'auto');
assert.equal(api.getPersistedPhoneMode(), 'auto');
assert.equal(api.getSaveCalls(), 0);
assert.equal(api.getUpdateCalls(), 0);
assert.equal(api.getDialogs().length, 1);
api.inputGpcCardKey.value = ' gpc-6c9f1a32-45734795-914e6f00 ';
assert.equal(await api.ensureGpcCardKeyReadyForStart({ notify: true }), true);
assert.equal(api.inputGpcCardKey.value, 'GPC-6C9F1A32-45734795-914E6F00');
assert.deepEqual(api.getToasts(), [{ message: 'GPC 卡密已填写。', type: 'success', duration: 1800 }]);
});
test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED state', async () => {
@@ -928,75 +551,3 @@ return { events, syncPlusManualConfirmationDialog };
assert.match(api.events[2].message, /GoPay/);
assert.equal(api.events[2].tone, 'info');
});
test('sidepanel resolves pending GPC OTP with typed code', async () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('normalizePlusStrategyTargetId'),
extractFunction('getPlusAccountAccessStrategyContinuationLabel'),
extractFunction('resolvePlusManualContinuationActionLabelFromState'),
extractLastFunction('openPlusManualConfirmationDialog'),
extractLastFunction('syncPlusManualConfirmationDialog'),
].join('\n');
const api = new Function(`
const events = [];
let latestState = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'otp-request-1',
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
plusManualConfirmationTitle: 'GPC OTP 验证',
plusManualConfirmationMessage: '',
};
let activePlusManualConfirmationRequestId = '';
let plusManualConfirmationDialogInFlight = false;
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = 'email';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
const sharedFormDialog = {
async open(options) {
events.push({ type: 'form', options });
return { otp: ' 12-34 56 ' };
},
};
function openActionModal(options) {
events.push({ type: 'modal', options });
return Promise.resolve('confirm');
}
function showToast(message, tone) {
events.push({ type: 'toast', message, tone });
}
const chrome = {
runtime: {
async sendMessage(message) {
events.push({ type: 'send', message });
latestState = { ...latestState, plusManualConfirmationPending: false };
return { ok: true };
},
},
};
${bundle}
return { events, syncPlusManualConfirmationDialog };
`)();
await api.syncPlusManualConfirmationDialog();
assert.equal(api.events[0].type, 'form');
assert.equal(api.events[0].options.message, '请在WhatsApp里面获取验证码(耐心等待三十秒左右)');
assert.equal(api.events[0].options.confirmLabel, '提交 OTP');
const sendEvent = api.events.find((event) => event.type === 'send');
assert.deepEqual(sendEvent.message.payload, {
step: 7,
requestId: 'otp-request-1',
confirmed: true,
otp: '123456',
});
assert.equal(api.events.some((event) => event.type === 'modal'), false);
});
+20 -17
View File
@@ -3,6 +3,16 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');
const { readStepDefinitionsBundle } = require('./helpers/script-bundles.js');
function stripHtmlComments(html) {
return String(html || '').replace(/<!--[\s\S]*?-->/g, '');
}
function getSelectMarkup(html, selectId) {
const match = String(html || '').match(new RegExp(`<select\\b[^>]*id="${selectId}"[^>]*>[\\s\\S]*?<\\/select>`));
assert.ok(match, `Expected select #${selectId} to exist`);
return match[0];
}
test('step definitions module exposes ordered normal and Plus step metadata', () => {
const globalScope = {};
const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope);
@@ -319,8 +329,8 @@ 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, 8, 9, 10, 11, 12, 13]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13);
assert.equal(gpcSteps[6].title, '创建 GPC 订单');
assert.equal(gpcSteps[7].title, '等待 GPC 任务完成');
assert.equal(gpcSteps[6].title, '打开 GPC 页面并准备');
assert.equal(gpcSteps[7].title, '启动并等待 GPC 完成');
});
test('Plus no-payment mode removes only payment chain nodes', () => {
@@ -639,8 +649,12 @@ test('sidepanel html loads shared step definitions before sidepanel bootstrap',
test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const visibleHtml = stripHtmlComments(html);
const plusPaymentSelect = getSelectMarkup(visibleHtml, 'select-plus-payment-method');
assert.match(html, /id="input-plus-mode-enabled"/);
assert.match(html, /id="select-plus-payment-method"/);
assert.match(plusPaymentSelect, /<option value="gpc-helper">GPC<\/option>/);
assert.doesNotMatch(plusPaymentSelect, /<option value="gopay">GoPay<\/option>/);
assert.match(html, /<option value="none">无需支付<\/option>/);
assert.match(html, /id="select-paypal-account"/);
assert.match(html, /id="btn-add-paypal-account"/);
@@ -649,21 +663,10 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
assert.match(html, /id="input-gopay-pin"/);
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-query"/);
assert.match(html, />购买卡密</);
assert.match(html, /GPC 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, /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"/);
assert.match(html, /id="input-gpc-helper-local-sms-enabled"/);
assert.match(html, /id="input-gpc-helper-local-sms-url"/);
assert.match(html, /id="input-gpc-helper-pin"/);
assert.match(html, />查询</);
assert.match(html, /GPC 卡密/);
assert.match(html, /id="input-gpc-card-key"/);
assert.match(html, /id="shared-form-modal"/);
});
+5 -5
View File
@@ -56,7 +56,7 @@
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro``v` 版本误显示为比 `Ultra` 更新
- 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证码页时继续复用同一手机号续跑短信验证
- 侧栏在接码卡内提供一个独立运行态“注册手机号”输入框,位于接码订单运行状态下方;第 2 步自动拿到号码后立即回填,用户手动接管手机号注册时也可以直接改写这一个运行态槽位。这个输入框表达账号身份,不等同于接码订单;后续自动拉短信仍依赖 `signupPhoneActivation / signupPhoneCompletedActivation`
- 展示 `Plus 模式` 开关、`账号接入策略``Plus 支付` 配置;`账号接入策略` 固定放在 `Plus 支付` 上方,只提供 `OAuth / 使用会话 JSON 导入` 两种接入方式,会话导入的 CPA / SUB2API 目标由上方 `来源` 自动决定,不支持会话导入的来源会直接禁用为仅 OAuth;支付方式支持 PayPal / GoPay / GPCPayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN,GPC 展示只读 API 地址、API Key、专用手机号、OTP 渠道、本地 OTP helper 开关与 URL、PINGPC 的 `购买卡密` 按钮打开卡密购买页,`转换 API Key` 按钮打开 `https://gpc.qlhazycoder.top/`;步骤列表会按支付方式和账号接入策略动态切换,普通模式的注册成功等待步骤不再显示或执行;Plus 模式下手机号注册不可用,只允许邮箱注册
- 展示 `Plus 模式` 开关、`账号接入策略``Plus 支付` 配置;`账号接入策略` 固定放在 `Plus 支付` 上方,只提供 `OAuth / 使用会话 JSON 导入` 两种接入方式,会话导入的 CPA / SUB2API 目标由上方 `来源` 自动决定,不支持会话导入的来源会直接禁用为仅 OAuth;支付方式支持 PayPal / GoPay / GPCPayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN,GPC 展示卡密输入、购买卡密入口和卡密状态提示;步骤列表会按支付方式和账号接入策略动态切换,普通模式的注册成功等待步骤不再显示或执行;Plus 模式下手机号注册不可用,只允许邮箱注册
- 展示 `执行范围` 配置;当前 UI 只在 codex/openai flow 显示,保存为通用的 `stepExecutionRangeByFlow`,用于指定允许执行的起止步骤。开启后,范围外节点在侧栏中显示为禁用态,手动按钮、手动跳过入口与进度计数只按允许节点计算。
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
- `Cloudflare Temp Email` 在侧栏中有独立区域,支持“按注册邮箱查信 / 按接收邮箱查信”切换、可编辑域名列表、随机子域名开关、固定子域名开关与子域前缀输入,以及教程/部署入口;域名选择由 `sidepanel/editable-list-picker.js` 统一承接
@@ -263,7 +263,7 @@
- IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值
- Plus 模式开关 `plusModeEnabled`
- Plus 账号接入策略 `plusAccountAccessStrategy`
- Plus 支付方式 `plusPaymentMethod`GoPay 配置 `gopayPhone / gopayPin`GPC helper 配置 `gopayHelperApiUrl / gopayHelperApiKey / gopayHelperPhoneMode / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin`;其中 GPC API 地址固定归一为 `https://gpc.qlhazycoder.top`
- Plus 支付方式 `plusPaymentMethod`GoPay 配置 `gopayPhone / gopayPin`GPC 页面充值配置 `gpcBaseUrl / gpcCardKey / gpcBalance / gpcBalancePayload / gpcBalanceUpdatedAt / gpcBalanceError / gpcRemainingUses / gpcCardStatus / gpcPageStatus / gpcPageStatusText`;其中 GPC 页面地址固定归一为 `https://gpc.qlhazycoder.top`
- PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword`
- 邮箱 provider 配置
- Cloud Mail / SkyMail API 配置:`cloudMailBaseUrl / cloudMailAdminEmail / cloudMailAdminPassword / cloudMailToken / cloudMailReceiveMailbox / cloudMailDomain / cloudMailDomains`
@@ -678,8 +678,8 @@ Plus 模式通过 `plusModeEnabled` 开启,核心目标是在普通注册资
- 打开 GoPay 订阅页
- 等待当前页面完成 GoPay 订阅确认
3. `GPC`
- 创建 GPC 订单
- 轮询远端任务,按需提交 OTP / PIN,直到任务完成
- 打开 GPC 页面并准备:覆盖卡密输入框,读取完整 ChatGPT session 并写入页面
- 启动并等待 GPC 完成:点击 `开始plus充值`,按页面日志判断 `订阅完成`、无试用资格、失败或需要再次启动
支付前缀完成后,尾链有两种模式:
@@ -711,7 +711,7 @@ Plus 模式通过 `plusModeEnabled` 开启,核心目标是在普通注册资
等待模型:
- Plus Checkout、PayPal、GoPay 手动确认与 GPC 任务轮询,都使用“可停止的后台等待”模型;等待过程中仍保持 Stop 可打断。
- Plus Checkout、PayPal、GoPay 手动确认与 GPC 页面流程等待,都使用“可停止的后台等待”模型;等待过程中仍保持 Stop 可打断。
## 6.2 Kiro Flow 链路
+9 -11
View File
@@ -24,7 +24,7 @@
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,OAuth 后链 5 分钟总预算只由 `openai + cpa` target capability 自动启用,其他来源与 flow 不启用该预算;`openai` flow 继续承接 `stepExecutionRangeByFlow``kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路,`grok` flow 走独立的“注册页 1-4 步 -> SSO Cookie 提取 5 步 -> `webchat2api` 上传 6 步”链路。
- `cloudmail-utils.js`Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
- `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 与余额响应解析。
- `gopay-utils.js`Plus 支付相关纯工具函数GoPay 部分负责手机号OTPPIN 归一化GPC 部分负责页面地址归一化、卡密格式校验、卡密余额查询 URL 与余额响应解析。
- `hotmail-utils.js`:Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。
- `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。
- `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。
@@ -86,12 +86,12 @@
- `flows/openai/background/steps/wait-registration-success.js`:步骤 6 实现,负责注册资料提交后等待 20 秒,让注册成功状态和页面跳转稳定;默认不清理 cookies,只有侧栏第六步 `清 Cookies` 开关开启时才会在等待结束后清理 ChatGPT / OpenAI 相关 cookies。
- `flows/openai/background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算,只有 `openai + cpa` 来源启用 5 分钟后链预算,其他来源和 flow 仅保留本地回调等待超时。
- `flows/openai/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 运行态。
- `flows/openai/background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 PayPal / GoPay checkout session,或在 GPC 模式下打开页面、覆盖卡密输入框、读取完整 ChatGPT session 并填入页面,记录页面准备运行态。
- `flows/openai/background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用 OpenAI 专属手机号验证码流程;手机号注册登录后若进入 `add-email`,会先按共享邮箱状态持久化规则生成/解析邮箱并提交绑定,在不丢失当前手机号身份的前提下再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录;命中 `email_in_use` 时会仅清空当前邮箱并保留上一轮比较基线,再在当前 Step 8 内重开 `add-email` 获取新邮箱,`max_check_attempts` 也只重启当前步骤恢复链。
- `flows/openai/background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
- `flows/openai/background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 PayPal / GoPay checkout 页面选择付款方式、填写账单地址、提交订阅并等待跳转;GPC 模式则轮询队列任务,按远端 `api_waiting_for` 提交 OTP / PIN,并在任务失败、过期或取消时清理运行态
- `flows/openai/background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 PayPal / GoPay checkout 页面选择付款方式、填写账单地址、提交订阅并等待跳转;GPC 模式则接管 GPC 页面,点击 `开始plus充值`,按右侧日志和按钮文案等待订阅完成、无试用资格、失败或再次启动
- `flows/openai/background/steps/gopay-manual-confirm.js`:GoPay 手动确认节点,负责在需要人工完成订阅时发布待确认运行态,并按当前 Plus 尾链动态生成“继续 OAuth / 继续导入 SUB2API / 继续导入 CPA”的提示文案。
- `flows/openai/background/steps/gopay-approve.js`:GoPay 自动审批节点,负责接管 GoPay / GPC 相关标签页、识别 OTP / PIN / Pay now / Hubungkan 等页面状态,并在支付页失效时回退到重新创建 checkout。
- `flows/openai/background/steps/gopay-approve.js`:GoPay 自动审批节点,负责接管 GoPay 相关标签页、识别 OTP / PIN / Pay now / Hubungkan 等页面状态,并在支付页失效时回退到重新创建 checkout。
- `flows/openai/background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。
- `flows/openai/background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
- `flows/openai/background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;当前会根据 `accountIdentifierType / accountIdentifier` 选择邮箱或手机号登录,手机号账号会先探测并切换手机号登录入口;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
@@ -182,7 +182,6 @@
## `scripts/`
- `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/custom_mail_helper.py`:自定义邮箱可选本地 IMAP helper,读取 `.env` 中的 `FLOWPILOT_CUSTOM_IMAP_*` 配置并提供 `/code``/messages` 等本地接口;默认自定义邮箱仍走手动确认,只有用户显式选择本地 helper 模式才会调用。
@@ -219,7 +218,7 @@
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱`
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;当邮箱服务或邮箱生成器为 Cloud Mail 时,额外显示 API 地址、管理员账号、接收邮箱和生成域名配置行;当邮箱服务为 YYDS Mail 时,额外显示 API Key 与 Base URL 配置行,并隐藏普通邮箱生成/转发邮箱相关配置;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关,并在 `Plus 支付` 之上新增 `账号接入策略` 下拉;该下拉只显示 `OAuth / 使用会话 JSON 导入` 两种接入方式,会话导入目标由当前来源自动决定,不支持的来源会直接禁用并显示“当前来源仅支持 OAuth”;PayPal 账号下拉框继续使用公共表单弹窗添加账号;GPC Plus 配置额外提供只读 API 地址、API Key、专用手机号、OTP 渠道、本地 OTP helper 开关、helper URL 与 PIN,并提供 `购买卡密` `转换 API Key` 入口;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;注册邮箱下方新增 `执行范围` 行,目前只在 codex/openai flow 显示,用于配置允许执行的起止步骤。
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关,并在 `Plus 支付` 之上新增 `账号接入策略` 下拉;该下拉只显示 `OAuth / 使用会话 JSON 导入` 两种接入方式,会话导入目标由当前来源自动决定,不支持的来源会直接禁用并显示“当前来源仅支持 OAuth”;PayPal 账号下拉框继续使用公共表单弹窗添加账号;GPC Plus 配置提供 `GPC 卡密` 输入、卡密状态提示与 `购买卡密` 入口;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;注册邮箱下方新增 `执行范围` 行,目前只在 codex/openai flow 显示,用于配置允许执行的起止步骤。
- `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
@@ -328,14 +327,13 @@
- `tests/phone-verification-flow.test.js`:测试 OpenAI 专属手机号验证流程对 HeroSMS / 5sim / NexSMS 的取号、复用、重发、换号与 Step 7 重开错误分支。
- `tests/paypal-approve-detection.test.js`:测试 Plus 第 8 步后台执行器对 PayPal 标签页发现、分离式账号/密码页识别、联合登录页识别,以及登录后直接离开 PayPal 页的分支判断。
- `tests/paypal-flow-content.test.js`:测试 PayPal 内容脚本对可见邮箱/密码输入框的识别,并覆盖邮箱页即使已预填相同账号也会先清空再重填后继续下一步。
- `tests/gpc-sms-helper-script.test.js`:测试 GPC macOS 本地短信 helper 在非 macOS 环境下会提示平台与 iPhone 短信转发要求。
- `tests/gopay-approve-source.test.js`:测试 GoPay 自动审批执行器对 OTP/PIN/Pay now/iframe/重建 checkout sentinel 的页面识别与回退。
- `tests/gopay-flow-content.test.js`:测试 GoPay 内容脚本的人类点击、表单提交、operation delay 包装与 Continue/Pay now 控件识别。
- `tests/gopay-manual-confirm.test.js`:测试 GoPay 手动确认节点会按当前 Plus 尾链生成 OAuth、SUB2API 或 CPA 的继续提示文案。
- `tests/gopay-utils.test.js`:测试 GoPay/GPC 工具层的 OTP/PIN 输入归一化、helper URL、payload 和余额解析。
- `tests/gopay-utils.test.js`:测试 GoPay 工具层输入归一化,以及 GPC 页面地址、卡密格式、卡密余额 URL 和余额解析。
- `tests/plus-checkout-address-input.test.js`:测试 Plus checkout 内容脚本的国家/商户路径、结构化地址识别、付款方式判定和 GoPay 控件查找。
- `tests/plus-checkout-create-wait.test.js`:测试 Plus checkout 创建节点的等待策略、GoPay 参数透传 GPC helper 队列权限/余额校验
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC 队列任务轮询、本地 OTP 自动读取、手动 OTP 输入、PIN 提交与任务终止分支。
- `tests/plus-checkout-create-wait.test.js`:测试 Plus checkout 创建节点的等待策略、GoPay 参数透传,以及 GPC 页面准备、卡密填入和 ChatGPT session 写入
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC 页面启动、订阅完成、无试用资格、重复启动与超时分支。
- `tests/plus-return-confirm-wait.test.js`:测试 Plus return confirm 在检测到回跳页后会固定等待 20 秒再结束。
- `tests/provider-operation-delay.test.js`:测试 Duck 地址生成与 2925 会话准备接入操作间延迟,而清理路径保持免延迟。
- `tests/qq-mail-content.test.js`:测试 QQ 邮箱内容脚本会把 runtime mail rule 的验证码模式转发到新邮件匹配链路。
@@ -399,7 +397,7 @@
- `tests/sidepanel-password-visibility.test.js`:测试侧边栏与共享表单弹窗中的密码字段都支持显示/隐藏切换。
- `tests/sidepanel-phone-verification-settings.test.js`:测试接码平台多 provider UI、免费复用号码、运行态注册手机号、CPA 风险提醒与国家/价格联动。
- `tests/sidepanel-paypal-manager.test.js`:测试侧边栏公共表单弹窗脚本与 PayPal manager 的加载顺序,以及 PayPal 账号保存后会立即选中当前账号。
- `tests/sidepanel-plus-payment-method.test.js`:测试 Plus 支付方式切换、GPC helper UI、共享步骤刷新与手动确认状态解析。
- `tests/sidepanel-plus-payment-method.test.js`:测试 Plus 支付方式切换、GPC 卡密 UI、共享步骤刷新与手动确认状态解析。
- `tests/sidepanel-sub2api-priority-settings.test.js`:测试 SUB2API 账号优先级输入行的位置、持久化、锁定态与 capability 显隐接线。
- `tests/sidepanel-window-lock.test.js`:测试侧边栏启动自动流程时会附带当前窗口 ID,普通消息不受影响。
- `tests/signup-entry-diagnostics.test.js`:测试注册入口与密码页诊断快照输出,包括密码页错误文案字段。