Compare commits
18 Commits
db4af3e49f
...
5f871c3c40
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f871c3c40 | |||
| 8cee2611ca | |||
| a33073436c | |||
| 932d1e965b | |||
| 9b7a6dcfbb | |||
| 3b751e8ba3 | |||
| 8c2cb2f8e0 | |||
| 0afa951814 | |||
| c64ff5152c | |||
| 3c0bc41794 | |||
| e441c5e7b8 | |||
| ed1d62687c | |||
| 0938f5dfba | |||
| f6a9f42089 | |||
| 26b1b15cd6 | |||
| e50b0fbfeb | |||
| f26f7e949e | |||
| a38fb250ea |
+88
-13
@@ -21,7 +21,9 @@ importScripts(
|
|||||||
'gopay-utils.js',
|
'gopay-utils.js',
|
||||||
'phone-sms/providers/hero-sms.js',
|
'phone-sms/providers/hero-sms.js',
|
||||||
'phone-sms/providers/five-sim.js',
|
'phone-sms/providers/five-sim.js',
|
||||||
|
'phone-sms/providers/nexsms.js',
|
||||||
'phone-sms/providers/madao.js',
|
'phone-sms/providers/madao.js',
|
||||||
|
'phone-sms/providers/sms-bower.js',
|
||||||
'phone-sms/providers/registry.js',
|
'phone-sms/providers/registry.js',
|
||||||
'background/phone-verification-flow.js',
|
'background/phone-verification-flow.js',
|
||||||
'background/account-run-history.js',
|
'background/account-run-history.js',
|
||||||
@@ -701,12 +703,14 @@ const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO;
|
|||||||
const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM;
|
const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM;
|
||||||
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
|
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
|
||||||
const PHONE_SMS_PROVIDER_MADAO = 'madao';
|
const PHONE_SMS_PROVIDER_MADAO = 'madao';
|
||||||
|
const PHONE_SMS_PROVIDER_SMS_BOWER = 'sms-bower';
|
||||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO;
|
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO;
|
||||||
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([
|
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([
|
||||||
PHONE_SMS_PROVIDER_HERO,
|
PHONE_SMS_PROVIDER_HERO,
|
||||||
PHONE_SMS_PROVIDER_5SIM,
|
PHONE_SMS_PROVIDER_5SIM,
|
||||||
PHONE_SMS_PROVIDER_NEXSMS,
|
PHONE_SMS_PROVIDER_NEXSMS,
|
||||||
PHONE_SMS_PROVIDER_MADAO,
|
PHONE_SMS_PROVIDER_MADAO,
|
||||||
|
PHONE_SMS_PROVIDER_SMS_BOWER,
|
||||||
]);
|
]);
|
||||||
const DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1';
|
const DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1';
|
||||||
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
|
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
|
||||||
@@ -715,6 +719,8 @@ const DEFAULT_FIVE_SIM_COUNTRY_ORDER = Object.freeze(['thailand']);
|
|||||||
const DEFAULT_NEX_SMS_BASE_URL = 'https://api.nexsms.net';
|
const DEFAULT_NEX_SMS_BASE_URL = 'https://api.nexsms.net';
|
||||||
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
|
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
|
||||||
const DEFAULT_NEX_SMS_COUNTRY_ORDER = Object.freeze([1]);
|
const DEFAULT_NEX_SMS_COUNTRY_ORDER = Object.freeze([1]);
|
||||||
|
const DEFAULT_SMS_BOWER_SERVICE_CODE = 'ot';
|
||||||
|
const DEFAULT_SMS_BOWER_COUNTRY_ORDER = Object.freeze([52]);
|
||||||
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
||||||
const DEFAULT_MADAO_MODE = 'routing_plan';
|
const DEFAULT_MADAO_MODE = 'routing_plan';
|
||||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||||
@@ -1446,12 +1452,14 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
nexSmsApiKey: '',
|
nexSmsApiKey: '',
|
||||||
nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER],
|
nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER],
|
||||||
nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE,
|
nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE,
|
||||||
|
smsBowerApiKey: '',
|
||||||
madaoBaseUrl: DEFAULT_MADAO_BASE_URL,
|
madaoBaseUrl: DEFAULT_MADAO_BASE_URL,
|
||||||
madaoHttpSecret: '',
|
madaoHttpSecret: '',
|
||||||
madaoMode: DEFAULT_MADAO_MODE,
|
madaoMode: DEFAULT_MADAO_MODE,
|
||||||
madaoRoutingPlanId: '',
|
madaoRoutingPlanId: '',
|
||||||
madaoProviderId: '',
|
madaoProviderId: '',
|
||||||
madaoCountry: '',
|
madaoCountry: '',
|
||||||
|
madaoOperator: '',
|
||||||
madaoAutoPickCountry: true,
|
madaoAutoPickCountry: true,
|
||||||
madaoReusePhone: true,
|
madaoReusePhone: true,
|
||||||
madaoMinPrice: '',
|
madaoMinPrice: '',
|
||||||
@@ -1846,6 +1854,9 @@ function normalizePhoneSmsProvider(value = '') {
|
|||||||
if (normalized === PHONE_SMS_PROVIDER_MADAO) {
|
if (normalized === PHONE_SMS_PROVIDER_MADAO) {
|
||||||
return PHONE_SMS_PROVIDER_MADAO;
|
return PHONE_SMS_PROVIDER_MADAO;
|
||||||
}
|
}
|
||||||
|
if (normalized === PHONE_SMS_PROVIDER_SMS_BOWER) {
|
||||||
|
return PHONE_SMS_PROVIDER_SMS_BOWER;
|
||||||
|
}
|
||||||
return PHONE_SMS_PROVIDER_HERO_SMS;
|
return PHONE_SMS_PROVIDER_HERO_SMS;
|
||||||
}
|
}
|
||||||
function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) {
|
function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) {
|
||||||
@@ -2244,6 +2255,13 @@ function normalizeMaDaoPrice(value = '') {
|
|||||||
return normalizeHeroSmsMaxPrice(value);
|
return normalizeHeroSmsMaxPrice(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMaDaoOperator(value = '') {
|
||||||
|
return String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9_-]+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePhonePreferredActivation(value) {
|
function normalizePhonePreferredActivation(value) {
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -3594,6 +3612,8 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
return normalizeMaDaoProviderId(value);
|
return normalizeMaDaoProviderId(value);
|
||||||
case 'madaoCountry':
|
case 'madaoCountry':
|
||||||
return normalizeMaDaoCountry(value);
|
return normalizeMaDaoCountry(value);
|
||||||
|
case 'madaoOperator':
|
||||||
|
return normalizeMaDaoOperator(value);
|
||||||
case 'madaoAutoPickCountry':
|
case 'madaoAutoPickCountry':
|
||||||
case 'madaoReusePhone':
|
case 'madaoReusePhone':
|
||||||
return Boolean(value);
|
return Boolean(value);
|
||||||
@@ -9971,10 +9991,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
gpcPageStatus: '',
|
gpcPageStatus: '',
|
||||||
gpcPageStatusText: '',
|
gpcPageStatusText: '',
|
||||||
};
|
};
|
||||||
|
const oauthRuntimeResets = {
|
||||||
if (step <= 1) {
|
|
||||||
return {
|
|
||||||
...plusRuntimeResets,
|
|
||||||
oauthUrl: null,
|
oauthUrl: null,
|
||||||
cpaOAuthState: null,
|
cpaOAuthState: null,
|
||||||
cpaManagementOrigin: null,
|
cpaManagementOrigin: null,
|
||||||
@@ -9986,6 +10003,55 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
sub2apiProxyId: null,
|
sub2apiProxyId: null,
|
||||||
codex2apiSessionId: null,
|
codex2apiSessionId: null,
|
||||||
codex2apiOAuthState: null,
|
codex2apiOAuthState: null,
|
||||||
|
};
|
||||||
|
const getNextStepKey = () => {
|
||||||
|
const numericStep = Number(step);
|
||||||
|
const currentNodeId = typeof getNodeIdByStepForState === 'function'
|
||||||
|
? getNodeIdByStepForState(numericStep, state)
|
||||||
|
: '';
|
||||||
|
if (
|
||||||
|
currentNodeId
|
||||||
|
&& typeof getNodeIdsForState === 'function'
|
||||||
|
&& typeof getStepIdByNodeIdForState === 'function'
|
||||||
|
) {
|
||||||
|
const nodeIds = getNodeIdsForState(state);
|
||||||
|
const currentIndex = nodeIds.indexOf(currentNodeId);
|
||||||
|
const nextNodeId = currentIndex >= 0 ? nodeIds[currentIndex + 1] : '';
|
||||||
|
const nextStep = nextNodeId ? getStepIdByNodeIdForState(nextNodeId, state) : null;
|
||||||
|
if (Number.isInteger(nextStep) && nextStep > 0) {
|
||||||
|
return String(getStepExecutionKeyForState(nextStep, state) || '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof getStepIdsForState === 'function') {
|
||||||
|
const stepIds = getStepIdsForState(state);
|
||||||
|
const currentIndex = stepIds.indexOf(numericStep);
|
||||||
|
const nextStep = currentIndex >= 0 ? stepIds[currentIndex + 1] : null;
|
||||||
|
if (Number.isInteger(nextStep) && nextStep > 0) {
|
||||||
|
return String(getStepExecutionKeyForState(nextStep, state) || '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
const nextStepKey = getNextStepKey();
|
||||||
|
const isOAuthEntryRestartBoundary = nextStepKey === 'oauth-login' || nextStepKey === 'relogin-bound-email';
|
||||||
|
const oauthEntryRuntimeResets = {
|
||||||
|
...oauthRuntimeResets,
|
||||||
|
lastLoginCode: null,
|
||||||
|
loginVerificationRequestedAt: null,
|
||||||
|
oauthFlowDeadlineAt: null,
|
||||||
|
oauthFlowDeadlineSourceUrl: null,
|
||||||
|
pendingPhoneActivationConfirmation: null,
|
||||||
|
localhostUrl: null,
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
|
currentPhoneVerificationCountdownEndsAt: 0,
|
||||||
|
currentPhoneVerificationCountdownWindowIndex: 0,
|
||||||
|
currentPhoneVerificationCountdownWindowTotal: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step <= 1) {
|
||||||
|
return {
|
||||||
|
...plusRuntimeResets,
|
||||||
|
...oauthRuntimeResets,
|
||||||
flowStartTime: null,
|
flowStartTime: null,
|
||||||
password: null,
|
password: null,
|
||||||
lastEmailTimestamp: null,
|
lastEmailTimestamp: null,
|
||||||
@@ -10056,6 +10122,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
if (isEarlyRegistrationNode || isBillingNode || isApprovalNode) {
|
if (isEarlyRegistrationNode || isBillingNode || isApprovalNode) {
|
||||||
return {
|
return {
|
||||||
...(isEarlyRegistrationNode ? plusRuntimeResets : {}),
|
...(isEarlyRegistrationNode ? plusRuntimeResets : {}),
|
||||||
|
...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}),
|
||||||
...(isBillingNode ? {
|
...(isBillingNode ? {
|
||||||
plusBillingCountryText: '',
|
plusBillingCountryText: '',
|
||||||
plusBillingAddress: null,
|
plusBillingAddress: null,
|
||||||
@@ -10090,6 +10157,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
}
|
}
|
||||||
if (stepKey === 'plus-checkout-return' || stepKey === 'confirm-oauth') {
|
if (stepKey === 'plus-checkout-return' || stepKey === 'confirm-oauth') {
|
||||||
return {
|
return {
|
||||||
|
...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}),
|
||||||
pendingPhoneActivationConfirmation: null,
|
pendingPhoneActivationConfirmation: null,
|
||||||
plusReturnUrl: '',
|
plusReturnUrl: '',
|
||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
@@ -10099,12 +10167,10 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
currentPhoneVerificationCountdownWindowTotal: 0,
|
currentPhoneVerificationCountdownWindowTotal: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (
|
if (stepKey === 'oauth-login' || stepKey === 'relogin-bound-email') {
|
||||||
stepKey === 'oauth-login'
|
return oauthEntryRuntimeResets;
|
||||||
|| stepKey === 'fetch-login-code'
|
}
|
||||||
|| stepKey === 'relogin-bound-email'
|
if (stepKey === 'fetch-login-code' || stepKey === 'fetch-bound-email-login-code') {
|
||||||
|| stepKey === 'fetch-bound-email-login-code'
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
lastLoginCode: null,
|
lastLoginCode: null,
|
||||||
loginVerificationRequestedAt: null,
|
loginVerificationRequestedAt: null,
|
||||||
@@ -10124,6 +10190,9 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (isOAuthEntryRestartBoundary) {
|
||||||
|
return oauthEntryRuntimeResets;
|
||||||
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13827,8 +13896,6 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea
|
|||||||
setState,
|
setState,
|
||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
throwIfStopped,
|
throwIfStopped,
|
||||||
createFiveSimProvider: self.PhoneSmsFiveSimProvider?.createProvider,
|
|
||||||
createMaDaoProvider: self.PhoneSmsMaDaoProvider?.createProvider,
|
|
||||||
});
|
});
|
||||||
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
|
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
|
||||||
addLog,
|
addLog,
|
||||||
@@ -14988,6 +15055,10 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
const hasTransientTokenExchangeSignal = /token_exchange_user_error|invalid request\.?\s*please try again later/i.test(normalizedMessage);
|
const hasTransientTokenExchangeSignal = /token_exchange_user_error|invalid request\.?\s*please try again later/i.test(normalizedMessage);
|
||||||
return mentionsTokenExchange && (hasTransientNetworkSignal || hasTransientTokenExchangeSignal);
|
return mentionsTokenExchange && (hasTransientNetworkSignal || hasTransientTokenExchangeSignal);
|
||||||
};
|
};
|
||||||
|
const isPlatformVerifyOAuthSessionExpiredError = (errorMessage = '') => {
|
||||||
|
const normalizedMessage = String(errorMessage || '');
|
||||||
|
return /OPENAI_OAUTH_SESSION_NOT_FOUND|session\s+not\s+found\s+or\s+expired|oauth\s+session\s+(?:not\s+found|expired)|missing\s+SUB2API\s+session_id|缺少\s*SUB2API\s*(?:session_id|会话信息)|SUB2API[\s\S]*(?:会话|session)[\s\S]*(?:过期|失效|不存在|not\s+found|expired)/i.test(normalizedMessage);
|
||||||
|
};
|
||||||
const isPhoneVerificationLocalFailure = (errorMessage = '') => {
|
const isPhoneVerificationLocalFailure = (errorMessage = '') => {
|
||||||
const normalizedMessage = String(errorMessage || '');
|
const normalizedMessage = String(errorMessage || '');
|
||||||
if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) {
|
if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) {
|
||||||
@@ -15034,11 +15105,15 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
&& confirmOauthStep > 0
|
&& confirmOauthStep > 0
|
||||||
&& confirmOauthStep < normalizedStep
|
&& confirmOauthStep < normalizedStep
|
||||||
&& isPlatformVerifyTransientRetryError(errorMessage);
|
&& isPlatformVerifyTransientRetryError(errorMessage);
|
||||||
|
const shouldRestartFromOAuthLoginStep = currentNodeKey === 'platform-verify'
|
||||||
|
&& isPlatformVerifyOAuthSessionExpiredError(errorMessage);
|
||||||
const restartAnchorStep = shouldRetryFromConfirmStep
|
const restartAnchorStep = shouldRetryFromConfirmStep
|
||||||
? confirmOauthStep
|
? confirmOauthStep
|
||||||
|
: (shouldRestartFromOAuthLoginStep
|
||||||
|
? authChainStartStep
|
||||||
: (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0
|
: (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0
|
||||||
? boundEmailReloginStep
|
? boundEmailReloginStep
|
||||||
: authChainStartStep);
|
: authChainStartStep));
|
||||||
if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
|
if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
|
||||||
return {
|
return {
|
||||||
shouldRestart: false,
|
shouldRestart: false,
|
||||||
|
|||||||
@@ -74,6 +74,31 @@
|
|||||||
});
|
});
|
||||||
const DONE_NODE_STATUSES = new Set(['completed', 'manual_completed', 'skipped']);
|
const DONE_NODE_STATUSES = new Set(['completed', 'manual_completed', 'skipped']);
|
||||||
|
|
||||||
|
function buildFreshAttemptIdentityResetPatch() {
|
||||||
|
return {
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
phoneNumber: '',
|
||||||
|
accountIdentifierType: null,
|
||||||
|
accountIdentifier: '',
|
||||||
|
signupPhoneNumber: '',
|
||||||
|
signupPhoneActivation: null,
|
||||||
|
signupPhoneCompletedActivation: null,
|
||||||
|
signupPhoneVerificationRequestedAt: null,
|
||||||
|
signupPhoneVerificationPurpose: '',
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
|
currentPhoneVerificationCountdownEndsAt: 0,
|
||||||
|
currentPhoneVerificationCountdownWindowIndex: 0,
|
||||||
|
currentPhoneVerificationCountdownWindowTotal: 0,
|
||||||
|
email: null,
|
||||||
|
registrationEmailState: { ...EMPTY_REGISTRATION_EMAIL_STATE },
|
||||||
|
step8VerificationTargetEmail: '',
|
||||||
|
lastEmailTimestamp: null,
|
||||||
|
lastSignupCode: '',
|
||||||
|
lastLoginCode: '',
|
||||||
|
bindEmailSubmitted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function isPhoneSignupFlow(state = {}) {
|
function isPhoneSignupFlow(state = {}) {
|
||||||
const resolvedSignupMethod = String(state?.resolvedSignupMethod || '').trim().toLowerCase();
|
const resolvedSignupMethod = String(state?.resolvedSignupMethod || '').trim().toLowerCase();
|
||||||
if (resolvedSignupMethod === 'phone' || resolvedSignupMethod === 'email') {
|
if (resolvedSignupMethod === 'phone' || resolvedSignupMethod === 'email') {
|
||||||
@@ -703,6 +728,7 @@
|
|||||||
attemptRun,
|
attemptRun,
|
||||||
sessionId,
|
sessionId,
|
||||||
}),
|
}),
|
||||||
|
...buildFreshAttemptIdentityResetPatch(),
|
||||||
currentNodeId: '',
|
currentNodeId: '',
|
||||||
nodeStatuses: buildFreshAttemptNodeStatuses(prevState),
|
nodeStatuses: buildFreshAttemptNodeStatuses(prevState),
|
||||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||||
|
|||||||
+340
-3363
File diff suppressed because it is too large
Load Diff
@@ -225,15 +225,20 @@
|
|||||||
const cardModeButton = modeButtons.find((element) => /卡密充值/.test(textOf(element)));
|
const cardModeButton = modeButtons.find((element) => /卡密充值/.test(textOf(element)));
|
||||||
const freeModeButton = modeButtons.find((element) => /免费充值/.test(textOf(element)));
|
const freeModeButton = modeButtons.find((element) => /免费充值/.test(textOf(element)));
|
||||||
const bodyText = String(document.body?.innerText || document.documentElement?.innerText || '').replace(/\r/g, '');
|
const bodyText = String(document.body?.innerText || document.documentElement?.innerText || '').replace(/\r/g, '');
|
||||||
|
const rawTextOf = (element) => String(element?.innerText || element?.textContent || element?.value || '');
|
||||||
const logNodes = Array.from(document.querySelectorAll('[class*="log"], [id*="log"], .design-log, .logs, pre, code'));
|
const logNodes = Array.from(document.querySelectorAll('[class*="log"], [id*="log"], .design-log, .logs, pre, code'));
|
||||||
const logText = logNodes.map(textOf).filter(Boolean).join('\n') || bodyText;
|
const logText = logNodes.map(rawTextOf).map((text) => text.trim()).filter(Boolean).join('\n') || bodyText;
|
||||||
const extractLastLogLine = (rawText = '') => {
|
const extractLastLogLine = (rawText = '') => {
|
||||||
const lines = String(rawText || '')
|
const normalizeLogLine = (line = '') => String(line || '').replace(/\s+/g, ' ').trim();
|
||||||
.split(/\n+/)
|
const ignoredLinePattern = /^(?:GPC Plus 充值|系统全自动出号|限量\d+份|自助购买卡密通道|实时进度|免费池|付费池|模式:|任务:|未开始|导出|充值模式|填写卡密|Access Token|开始 Plus 充值|停止当前任务|页面已就绪)$/i;
|
||||||
.map((line) => line.replace(/\s+/g, ' ').trim())
|
const timestampEntries = String(rawText || '')
|
||||||
|
.replace(/\r/g, '')
|
||||||
|
.match(/\[\d{2}:\d{2}:\d{2}\][\s\S]*?(?=\s*\[\d{2}:\d{2}:\d{2}\]|$)/g);
|
||||||
|
const entries = (timestampEntries && timestampEntries.length ? timestampEntries : String(rawText || '').split(/\n+/))
|
||||||
|
.map(normalizeLogLine)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((line) => !/^(?:GPC Plus 充值|系统全自动出号|限量\d+份|自助购买卡密通道|实时进度|免费池|付费池|模式:|任务:|未开始|导出|充值模式|填写卡密|Access Token|开始 Plus 充值|停止当前任务|页面已就绪)$/i.test(line));
|
.filter((line) => !ignoredLinePattern.test(line));
|
||||||
return lines[lines.length - 1] || '';
|
return entries[entries.length - 1] || '';
|
||||||
};
|
};
|
||||||
const lastLogLine = extractLastLogLine(logText) || extractLastLogLine(bodyText);
|
const lastLogLine = extractLastLogLine(logText) || extractLastLogLine(bodyText);
|
||||||
const cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]'))
|
const cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]'))
|
||||||
@@ -266,11 +271,13 @@
|
|||||||
|
|
||||||
function getGpcLastLogSummary(pageState = {}) {
|
function getGpcLastLogSummary(pageState = {}) {
|
||||||
const rawText = String(pageState.lastLogLine || pageState.logText || pageState.bodyText || '');
|
const rawText = String(pageState.lastLogLine || pageState.logText || pageState.bodyText || '');
|
||||||
const lines = rawText
|
const timestampEntries = rawText
|
||||||
.split(/\n+/)
|
.replace(/\r/g, '')
|
||||||
|
.match(/\[\d{2}:\d{2}:\d{2}\][\s\S]*?(?=\s*\[\d{2}:\d{2}:\d{2}\]|$)/g);
|
||||||
|
const entries = (timestampEntries && timestampEntries.length ? timestampEntries : rawText.split(/\n+/))
|
||||||
.map((line) => line.replace(/\s+/g, ' ').trim())
|
.map((line) => line.replace(/\s+/g, ' ').trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const line = lines[lines.length - 1] || '';
|
const line = entries[entries.length - 1] || '';
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -282,6 +289,18 @@
|
|||||||
return summary ? ` 最近日志:${summary}` : '';
|
return summary ? ` 最近日志:${summary}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasGpcNoTrialFailure(pageState = {}) {
|
||||||
|
if (pageState.noTrial) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const text = normalizeText([
|
||||||
|
pageState.lastLogLine,
|
||||||
|
pageState.logText,
|
||||||
|
pageState.bodyText,
|
||||||
|
].filter(Boolean).join(' '));
|
||||||
|
return /(?:该|此|当前)?账[户号].{0,12}(?:没有|无|不具备).{0,8}试用资格|(?:没有|无|不具备).{0,8}试用资格/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureGpcCardMode(tabId) {
|
async function ensureGpcCardMode(tabId) {
|
||||||
if (!chrome?.scripting?.executeScript) {
|
if (!chrome?.scripting?.executeScript) {
|
||||||
throw new Error('步骤 7:当前运行环境不支持脚本注入,无法切换 GPC 卡密充值模式。');
|
throw new Error('步骤 7:当前运行环境不支持脚本注入,无法切换 GPC 卡密充值模式。');
|
||||||
@@ -478,6 +497,7 @@
|
|||||||
while (Date.now() <= deadline) {
|
while (Date.now() <= deadline) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const pageState = await inspectGpcPortalPage(tabId);
|
const pageState = await inspectGpcPortalPage(tabId);
|
||||||
|
pageState.noTrial = hasGpcNoTrialFailure(pageState);
|
||||||
const buttonText = normalizeText(pageState.startButtonText);
|
const buttonText = normalizeText(pageState.startButtonText);
|
||||||
await collectStatusLog(pageState, buttonText);
|
await collectStatusLog(pageState, buttonText);
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,7 @@
|
|||||||
try {
|
try {
|
||||||
const rawCurrentState = {
|
const rawCurrentState = {
|
||||||
...(attempt === 1 ? state : await getState()),
|
...(attempt === 1 ? state : await getState()),
|
||||||
|
visibleStep: completionStep,
|
||||||
...(resolvedIdentifierType === 'phone' ? {
|
...(resolvedIdentifierType === 'phone' ? {
|
||||||
forceLoginIdentifierType: 'phone',
|
forceLoginIdentifierType: 'phone',
|
||||||
forceEmailLogin: false,
|
forceEmailLogin: false,
|
||||||
|
|||||||
@@ -231,6 +231,17 @@ const CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN = new RegExp([
|
|||||||
String.raw`\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8`,
|
String.raw`\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8`,
|
||||||
].join('|'), 'i');
|
].join('|'), 'i');
|
||||||
const CHOOSE_ACCOUNT_ACTION_SELECTOR = 'button, a, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])';
|
const CHOOSE_ACCOUNT_ACTION_SELECTOR = 'button, a, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])';
|
||||||
|
const CHOOSE_ACCOUNT_CARD_SELECTOR = [
|
||||||
|
'[data-testid*="account" i]',
|
||||||
|
'[data-test-id*="account" i]',
|
||||||
|
'[class*="account" i]',
|
||||||
|
'[class*="user" i]',
|
||||||
|
'[class*="card" i]',
|
||||||
|
'[class*="option" i]',
|
||||||
|
'[class*="select" i]',
|
||||||
|
'[class*="list" i] > *',
|
||||||
|
'li',
|
||||||
|
].join(', ');
|
||||||
const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::';
|
const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::';
|
||||||
const CONTACT_VERIFICATION_SERVER_ERROR_PATTERN = /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i;
|
const CONTACT_VERIFICATION_SERVER_ERROR_PATTERN = /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i;
|
||||||
|
|
||||||
@@ -3152,6 +3163,47 @@ function resolveChooseAccountClickTarget(element) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveChooseAccountCardTarget(element, normalizedEmail = '') {
|
||||||
|
let current = element;
|
||||||
|
let bestTarget = null;
|
||||||
|
|
||||||
|
while (current && current !== document.body) {
|
||||||
|
if (isChooseAccountRemovalAction(current) || !isVisibleElement(current)) {
|
||||||
|
current = current.parentElement;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTarget = resolveChooseAccountClickTarget(current);
|
||||||
|
if (actionTarget) {
|
||||||
|
return actionTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(current));
|
||||||
|
if (text.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(text)) {
|
||||||
|
bestTarget = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closestCard = current.closest?.(CHOOSE_ACCOUNT_CARD_SELECTOR) || null;
|
||||||
|
if (
|
||||||
|
closestCard
|
||||||
|
&& closestCard !== current
|
||||||
|
&& closestCard !== document.body
|
||||||
|
&& isVisibleElement(closestCard)
|
||||||
|
&& !isChooseAccountRemovalAction(closestCard)
|
||||||
|
) {
|
||||||
|
const cardText = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(closestCard));
|
||||||
|
if (cardText.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(cardText)) {
|
||||||
|
const cardActionTarget = resolveChooseAccountClickTarget(closestCard);
|
||||||
|
return cardActionTarget || closestCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestTarget;
|
||||||
|
}
|
||||||
|
|
||||||
function findChooseAccountButtonForEmail(email) {
|
function findChooseAccountButtonForEmail(email) {
|
||||||
const normalizedEmail = normalizeAuthAccountIdentifier(email);
|
const normalizedEmail = normalizeAuthAccountIdentifier(email);
|
||||||
if (!normalizedEmail || !normalizedEmail.includes('@')) {
|
if (!normalizedEmail || !normalizedEmail.includes('@')) {
|
||||||
@@ -3180,22 +3232,90 @@ function findChooseAccountButtonForEmail(email) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const node of emailNodes) {
|
for (const node of emailNodes) {
|
||||||
let current = node;
|
const target = resolveChooseAccountCardTarget(node, normalizedEmail);
|
||||||
while (current && current !== document.body) {
|
|
||||||
const target = resolveChooseAccountClickTarget(current);
|
|
||||||
if (target) {
|
if (target) {
|
||||||
const targetText = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(target));
|
|
||||||
if (targetText.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(targetText)) {
|
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current = current.parentElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findChooseAccountOtherAccountButton() {
|
||||||
|
const candidates = Array.from(document.querySelectorAll(CHOOSE_ACCOUNT_ACTION_SELECTOR))
|
||||||
|
.filter((element) => isVisibleElement(element) && isActionEnabled(element));
|
||||||
|
|
||||||
|
return candidates.find((candidate) => {
|
||||||
|
if (isChooseAccountRemovalAction(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const text = getChooseAccountCandidateText(candidate);
|
||||||
|
return CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(text);
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChooseAccountListedEmails() {
|
||||||
|
const emailPattern = /[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/ig;
|
||||||
|
const emails = new Set();
|
||||||
|
const nodes = [
|
||||||
|
...Array.from(document.querySelectorAll(CHOOSE_ACCOUNT_ACTION_SELECTOR)),
|
||||||
|
...Array.from(document.querySelectorAll('body *')).filter((element) => isVisibleElement(element)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (isChooseAccountRemovalAction(node)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const text = getChooseAccountCandidateText(node);
|
||||||
|
for (const match of text.matchAll(emailPattern)) {
|
||||||
|
emails.add(normalizeAuthAccountIdentifier(match[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(emails).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveChooseAccountAction(email, maxRounds = 8) {
|
||||||
|
let otherAccountButton = null;
|
||||||
|
let latestSnapshot = normalizeStep6Snapshot(inspectLoginAuthState());
|
||||||
|
|
||||||
|
for (let round = 0; round < maxRounds; round += 1) {
|
||||||
|
throwIfStopped();
|
||||||
|
latestSnapshot = normalizeStep6Snapshot(inspectLoginAuthState());
|
||||||
|
if (latestSnapshot.state !== 'unknown' && latestSnapshot.state !== 'choose_account_page') {
|
||||||
|
return {
|
||||||
|
snapshot: latestSnapshot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = findChooseAccountButtonForEmail(email);
|
||||||
|
if (target) {
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
snapshot: latestSnapshot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
otherAccountButton = findChooseAccountOtherAccountButton() || otherAccountButton;
|
||||||
|
const listedEmails = getChooseAccountListedEmails();
|
||||||
|
if (otherAccountButton && round >= 2 && (listedEmails.length > 0 || round >= 4)) {
|
||||||
|
return {
|
||||||
|
otherAccountButton,
|
||||||
|
snapshot: latestSnapshot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (round < maxRounds - 1) {
|
||||||
|
await sleep(round === 0 ? 300 : 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
otherAccountButton,
|
||||||
|
snapshot: latestSnapshot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getOAuthConsentForm() {
|
function getOAuthConsentForm() {
|
||||||
return document.querySelector(OAUTH_CONSENT_FORM_SELECTOR);
|
return document.querySelector(OAUTH_CONSENT_FORM_SELECTOR);
|
||||||
}
|
}
|
||||||
@@ -5888,8 +6008,65 @@ async function step6ChooseExistingAccount(payload, snapshot) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = findChooseAccountButtonForEmail(email);
|
const chooseAccountAction = await resolveChooseAccountAction(email);
|
||||||
|
if (
|
||||||
|
chooseAccountAction.snapshot
|
||||||
|
&& chooseAccountAction.snapshot.state !== 'unknown'
|
||||||
|
&& chooseAccountAction.snapshot.state !== 'choose_account_page'
|
||||||
|
) {
|
||||||
|
const resolvedSnapshot = chooseAccountAction.snapshot;
|
||||||
|
if (resolvedSnapshot.state === 'oauth_consent_page') {
|
||||||
|
return createStep6OAuthConsentSuccessResult(resolvedSnapshot, {
|
||||||
|
via: 'choose_account_oauth_consent_page',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (resolvedSnapshot.oauthAuthorizationRoute || isPostChooseAccountOAuthRoute(resolvedSnapshot)) {
|
||||||
|
return createStep6OAuthConsentSuccessResult(resolvedSnapshot, {
|
||||||
|
via: 'choose_account_oauth_authorization_route',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (resolvedSnapshot.state === 'email_page') {
|
||||||
|
return step6LoginFromEmailPage(payload, resolvedSnapshot);
|
||||||
|
}
|
||||||
|
if (resolvedSnapshot.state === 'entry_page') {
|
||||||
|
return step6OpenLoginEntry(payload, resolvedSnapshot);
|
||||||
|
}
|
||||||
|
if (resolvedSnapshot.state === 'password_page') {
|
||||||
|
return step6LoginFromPasswordPage(payload, resolvedSnapshot);
|
||||||
|
}
|
||||||
|
if (resolvedSnapshot.state === 'phone_entry_page') {
|
||||||
|
return step6LoginFromPhonePage(payload, resolvedSnapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = chooseAccountAction.target;
|
||||||
if (!target) {
|
if (!target) {
|
||||||
|
const otherAccountButton = chooseAccountAction.otherAccountButton;
|
||||||
|
if (otherAccountButton) {
|
||||||
|
const listedEmails = getChooseAccountListedEmails();
|
||||||
|
const listedLabel = listedEmails.length ? `页面已有账号:${listedEmails.join(', ')}。` : '页面未读取到已有账号邮箱。';
|
||||||
|
log(`OpenAI 选择账号页未列出目标邮箱 ${email},${listedLabel} 将点击“登录至另一个帐户”继续目标邮箱登录。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' });
|
||||||
|
await humanPause(350, 900);
|
||||||
|
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'choose-other-account' }, async () => {
|
||||||
|
simulateClick(otherAccountButton);
|
||||||
|
});
|
||||||
|
const otherAccountSnapshot = normalizeStep6Snapshot(await waitForChooseAccountTransition(15000));
|
||||||
|
if (otherAccountSnapshot.state === 'email_page') {
|
||||||
|
return step6LoginFromEmailPage(payload, otherAccountSnapshot);
|
||||||
|
}
|
||||||
|
if (otherAccountSnapshot.state === 'entry_page') {
|
||||||
|
return step6OpenLoginEntry(payload, otherAccountSnapshot);
|
||||||
|
}
|
||||||
|
if (otherAccountSnapshot.state === 'password_page') {
|
||||||
|
return step6LoginFromPasswordPage(payload, otherAccountSnapshot);
|
||||||
|
}
|
||||||
|
if (otherAccountSnapshot.state === 'phone_entry_page') {
|
||||||
|
return step6LoginFromPhonePage(payload, otherAccountSnapshot);
|
||||||
|
}
|
||||||
|
return createStep6RecoverableResult('choose_account_other_account_transition_stalled', otherAccountSnapshot, {
|
||||||
|
message: `Clicked another-account login because ${email} was not listed, but the page did not enter a supported login state.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
return createStep6RecoverableResult('choose_account_target_not_found', currentSnapshot, {
|
return createStep6RecoverableResult('choose_account_target_not_found', currentSnapshot, {
|
||||||
message: `OpenAI choose-account page does not contain target email ${email}.`,
|
message: `OpenAI choose-account page does not contain target email ${email}.`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -199,6 +199,23 @@
|
|||||||
return String(visibleSpan?.textContent || '').trim();
|
return String(visibleSpan?.textContent || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function phoneNumberMatchesDialCode(phoneNumber = '', dialCode = '') {
|
||||||
|
const digits = normalizePhoneDigits(phoneNumber);
|
||||||
|
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||||
|
if (!digits || !normalizedDialCode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return digits.startsWith(normalizedDialCode) && digits.length > normalizedDialCode.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedCountryMatchesPhoneNumber(phoneNumber = '') {
|
||||||
|
const digits = normalizePhoneDigits(phoneNumber);
|
||||||
|
if (!digits) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return phoneNumberMatchesDialCode(phoneNumber, getDisplayedDialCode());
|
||||||
|
}
|
||||||
|
|
||||||
function toNationalPhoneNumber(value, dialCode) {
|
function toNationalPhoneNumber(value, dialCode) {
|
||||||
const digits = normalizePhoneDigits(value);
|
const digits = normalizePhoneDigits(value);
|
||||||
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||||
@@ -364,15 +381,17 @@
|
|||||||
|
|
||||||
const byLabel = findCountryOptionByLabel(countryLabel);
|
const byLabel = findCountryOptionByLabel(countryLabel);
|
||||||
if (await trySelectCountryOption(select, byLabel)) {
|
if (await trySelectCountryOption(select, byLabel)) {
|
||||||
|
if (selectedCountryMatchesPhoneNumber(phoneNumber)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber);
|
const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber);
|
||||||
if (await trySelectCountryOption(select, byPhoneNumber)) {
|
if (await trySelectCountryOption(select, byPhoneNumber)) {
|
||||||
return true;
|
return selectedCountryMatchesPhoneNumber(phoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Boolean(getSelectedCountryOption());
|
return selectedCountryMatchesPhoneNumber(phoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAddPhoneSubmitButton() {
|
function getAddPhoneSubmitButton() {
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "FlowPilot",
|
"name": "FlowPilot",
|
||||||
"version": "2.5",
|
"version": "2.6",
|
||||||
"version_name": "FlowPilot2.5",
|
"version_name": "FlowPilot2.6",
|
||||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"sidePanel",
|
"sidePanel",
|
||||||
|
|||||||
@@ -150,6 +150,10 @@
|
|||||||
return String(value || '').trim() || fallback;
|
return String(value || '').trim() || fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCountryKey(value) {
|
||||||
|
return normalizeFiveSimCountryId(value, '');
|
||||||
|
}
|
||||||
|
|
||||||
function formatFiveSimCountryLabel(id = '', englishValue = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
function formatFiveSimCountryLabel(id = '', englishValue = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||||||
const countryId = normalizeFiveSimCountryId(id, '');
|
const countryId = normalizeFiveSimCountryId(id, '');
|
||||||
const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback);
|
const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback);
|
||||||
@@ -181,6 +185,42 @@
|
|||||||
return String(Math.round(numeric * 10000) / 10000);
|
return String(Math.round(numeric * 10000) / 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePriceLimit(value = '') {
|
||||||
|
const normalized = normalizeFiveSimMaxPrice(value);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const numeric = Number(normalized);
|
||||||
|
return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePriceRange(state = {}) {
|
||||||
|
const minPriceLimit = normalizePriceLimit(state?.fiveSimMinPrice);
|
||||||
|
const maxPriceLimit = normalizePriceLimit(state?.fiveSimMaxPrice);
|
||||||
|
return {
|
||||||
|
minPriceLimit,
|
||||||
|
maxPriceLimit,
|
||||||
|
hasMinPriceLimit: minPriceLimit !== null,
|
||||||
|
hasMaxPriceLimit: maxPriceLimit !== null,
|
||||||
|
invalidRange: minPriceLimit !== null && maxPriceLimit !== null && minPriceLimit > maxPriceLimit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPriceRangeText(minPriceLimit = null, maxPriceLimit = null) {
|
||||||
|
const minPrice = normalizePriceLimit(minPriceLimit);
|
||||||
|
const maxPrice = normalizePriceLimit(maxPriceLimit);
|
||||||
|
if (minPrice !== null && maxPrice !== null) {
|
||||||
|
return `${minPrice}~${maxPrice}`;
|
||||||
|
}
|
||||||
|
if (minPrice !== null) {
|
||||||
|
return `${minPrice}~`;
|
||||||
|
}
|
||||||
|
if (maxPrice !== null) {
|
||||||
|
return `~${maxPrice}`;
|
||||||
|
}
|
||||||
|
return 'unbounded';
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFiveSimCountryFallback(value = []) {
|
function normalizeFiveSimCountryFallback(value = []) {
|
||||||
const source = Array.isArray(value)
|
const source = Array.isArray(value)
|
||||||
? value
|
? value
|
||||||
@@ -371,6 +411,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveCountryCandidates(state = {}) {
|
function resolveCountryCandidates(state = {}) {
|
||||||
|
const orderedCountries = normalizeFiveSimCountryFallback(state.fiveSimCountryOrder);
|
||||||
|
if (orderedCountries.length) {
|
||||||
|
const primaryCountryId = normalizeFiveSimCountryId(state.fiveSimCountryId, '');
|
||||||
|
return orderedCountries.map((entry) => {
|
||||||
|
const id = normalizeFiveSimCountryId(entry.id, '');
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
label: primaryCountryId && id === primaryCountryId
|
||||||
|
? formatFiveSimCountryLabel(id, state.fiveSimCountryLabel || entry.label || id, id)
|
||||||
|
: formatFiveSimCountryLabel(id, entry.label || id, id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const primary = resolveCountryConfig(state);
|
const primary = resolveCountryConfig(state);
|
||||||
const fallbackList = normalizeFiveSimCountryFallback(state.fiveSimCountryFallback);
|
const fallbackList = normalizeFiveSimCountryFallback(state.fiveSimCountryFallback);
|
||||||
const seen = new Set([primary.id]);
|
const seen = new Set([primary.id]);
|
||||||
@@ -391,6 +445,13 @@
|
|||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveCountryLabel(state = {}, countryId = DEFAULT_COUNTRY_ID) {
|
||||||
|
const countryKey = normalizeCountryKey(countryId);
|
||||||
|
const matched = resolveCountryCandidates(state)
|
||||||
|
.find((entry) => normalizeCountryKey(entry.id) === countryKey);
|
||||||
|
return matched?.label || formatFiveSimCountryLabel(countryKey, countryKey, countryKey || DEFAULT_COUNTRY_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchBalance(state = {}, deps = {}) {
|
async function fetchBalance(state = {}, deps = {}) {
|
||||||
const config = resolveConfig(state, deps);
|
const config = resolveConfig(state, deps);
|
||||||
const payload = await fetchJson(config, '/v1/user/profile', {
|
const payload = await fetchJson(config, '/v1/user/profile', {
|
||||||
@@ -496,6 +557,8 @@
|
|||||||
async function resolvePricePlan(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
async function resolvePricePlan(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||||
const userLimitText = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
const userLimitText = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
||||||
const userLimit = userLimitText ? Number(userLimitText) : null;
|
const userLimit = userLimitText ? Number(userLimitText) : null;
|
||||||
|
const userMinText = normalizeFiveSimMaxPrice(state.fiveSimMinPrice);
|
||||||
|
const userMinLimit = userMinText ? Number(userMinText) : null;
|
||||||
let priceCandidates = [];
|
let priceCandidates = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -526,19 +589,24 @@
|
|||||||
priceCandidates = buildSortedUniquePriceCandidates(priceCandidates);
|
priceCandidates = buildSortedUniquePriceCandidates(priceCandidates);
|
||||||
|
|
||||||
const minCatalogPrice = priceCandidates.length > 0 ? priceCandidates[0] : null;
|
const minCatalogPrice = priceCandidates.length > 0 ? priceCandidates[0] : null;
|
||||||
|
const rangeFilteredPrices = priceCandidates.filter((price) => (
|
||||||
|
(userMinLimit === null || price >= userMinLimit)
|
||||||
|
&& (userLimit === null || price <= userLimit)
|
||||||
|
));
|
||||||
if (userLimit !== null) {
|
if (userLimit !== null) {
|
||||||
const bounded = priceCandidates.filter((price) => price <= userLimit);
|
const bounded = rangeFilteredPrices;
|
||||||
return {
|
return {
|
||||||
prices: bounded.length > 0 ? [userLimit, ...bounded.filter((price) => price !== userLimit)] : [userLimit],
|
prices: bounded.length > 0 ? [userLimit, ...bounded.filter((price) => price !== userLimit)] : [userLimit],
|
||||||
userLimit,
|
userLimit,
|
||||||
|
userMinLimit,
|
||||||
minCatalogPrice,
|
minCatalogPrice,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priceCandidates.length > 0) {
|
if (rangeFilteredPrices.length > 0) {
|
||||||
return { prices: priceCandidates, userLimit: null, minCatalogPrice };
|
return { prices: rangeFilteredPrices, userLimit: null, userMinLimit, minCatalogPrice };
|
||||||
}
|
}
|
||||||
return { prices: [null], userLimit: null, minCatalogPrice: null };
|
return { prices: [null], userLimit: null, userMinLimit, minCatalogPrice };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeActivation(record, fallback = {}) {
|
function normalizeActivation(record, fallback = {}) {
|
||||||
@@ -558,6 +626,7 @@
|
|||||||
provider: PROVIDER_ID,
|
provider: PROVIDER_ID,
|
||||||
serviceCode: DEFAULT_PRODUCT,
|
serviceCode: DEFAULT_PRODUCT,
|
||||||
countryId,
|
countryId,
|
||||||
|
countryCode: countryId,
|
||||||
countryLabel,
|
countryLabel,
|
||||||
successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)),
|
successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)),
|
||||||
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_MAX_USES)),
|
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_MAX_USES)),
|
||||||
@@ -570,6 +639,40 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveActivationCountry(activation = {}, state = {}) {
|
||||||
|
const normalizedActivation = normalizeActivation(activation)
|
||||||
|
|| (activation && typeof activation === 'object' ? activation : {});
|
||||||
|
const countryId = normalizeFiveSimCountryId(
|
||||||
|
normalizedActivation.countryCode ?? normalizedActivation.countryId ?? normalizedActivation.country,
|
||||||
|
DEFAULT_COUNTRY_ID
|
||||||
|
);
|
||||||
|
const matched = resolveCountryCandidates(state)
|
||||||
|
.find((entry) => normalizeCountryKey(entry.id) === countryId);
|
||||||
|
if (matched) {
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: countryId,
|
||||||
|
code: countryId,
|
||||||
|
label: normalizeFiveSimCountryLabel(
|
||||||
|
normalizedActivation.countryLabel,
|
||||||
|
formatFiveSimCountryLabel(countryId, countryId, countryId)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivationCountryKey(activation = {}) {
|
||||||
|
return normalizeCountryKey(activation?.countryCode ?? activation?.countryId ?? activation?.country);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivationPrice(activation = {}) {
|
||||||
|
return normalizePrice(
|
||||||
|
activation?.selectedPrice
|
||||||
|
?? activation?.price
|
||||||
|
?? activation?.maxPrice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isNoNumbersPayload(payloadOrMessage) {
|
function isNoNumbersPayload(payloadOrMessage) {
|
||||||
const text = describePayload(payloadOrMessage);
|
const text = describePayload(payloadOrMessage);
|
||||||
return /no\s+free\s+phones|no\s+numbers|not\s+found/i.test(text);
|
return /no\s+free\s+phones|no\s+numbers|not\s+found/i.test(text);
|
||||||
@@ -594,10 +697,17 @@
|
|||||||
|
|
||||||
function assertMaxPriceCompatibleWithOperator(state = {}) {
|
function assertMaxPriceCompatibleWithOperator(state = {}) {
|
||||||
const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
||||||
|
const minPrice = normalizeFiveSimMaxPrice(state.fiveSimMinPrice);
|
||||||
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||||
if (maxPrice && operator !== DEFAULT_OPERATOR) {
|
if (maxPrice && operator !== DEFAULT_OPERATOR) {
|
||||||
throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。');
|
throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。');
|
||||||
}
|
}
|
||||||
|
if (minPrice && operator !== DEFAULT_OPERATOR) {
|
||||||
|
throw new Error('5sim 最低购买价仅支持运营商为 "any" 时使用;请清空最低购买价,或先把运营商切换为 any。');
|
||||||
|
}
|
||||||
|
if (minPrice && maxPrice && Number(minPrice) > Number(maxPrice)) {
|
||||||
|
throw new Error(`5sim 价格区间无效:最低购买价 ${minPrice} 高于价格上限 ${maxPrice}。`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeReuseEnabled(state = {}) {
|
function normalizeReuseEnabled(state = {}) {
|
||||||
@@ -777,6 +887,22 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function prepareActivationForReuse(state = {}, activation, _options = {}, deps = {}) {
|
||||||
|
try {
|
||||||
|
const retainedActivation = await reuseActivation(state, activation, deps);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
activation: retainedActivation,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'reuse_check_failed',
|
||||||
|
message: error.message || '5sim 复用手机号基线检查失败。',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function finishActivation(state = {}, activation, deps = {}) {
|
async function finishActivation(state = {}, activation, deps = {}) {
|
||||||
const normalizedActivation = normalizeActivation(activation);
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
if (!normalizedActivation) return '';
|
if (!normalizedActivation) return '';
|
||||||
@@ -807,6 +933,25 @@
|
|||||||
return describePayload(payload);
|
return describePayload(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestAdditionalSms() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rotateActivation(state = {}, activation, options = {}, deps = {}) {
|
||||||
|
const releaseAction = String(options?.releaseAction || '').trim().toLowerCase() === 'ban'
|
||||||
|
? 'ban'
|
||||||
|
: 'cancel';
|
||||||
|
if (releaseAction === 'ban') {
|
||||||
|
await banActivation(state, activation, deps);
|
||||||
|
} else {
|
||||||
|
await cancelActivation(state, activation, deps);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
currentTicketId: String(activation?.activationId || activation?.id || ''),
|
||||||
|
nextActivation: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function extractVerificationCode(rawCodeOrText) {
|
function extractVerificationCode(rawCodeOrText) {
|
||||||
const trimmed = String(rawCodeOrText || '').trim();
|
const trimmed = String(rawCodeOrText || '').trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -913,9 +1058,18 @@
|
|||||||
addLog: deps.addLog,
|
addLog: deps.addLog,
|
||||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||||
};
|
};
|
||||||
|
const capabilities = Object.freeze({
|
||||||
|
supportsReusableActivation: true,
|
||||||
|
supportsAutomaticFreeReuse: true,
|
||||||
|
supportsFreeReusePreservation: true,
|
||||||
|
supportsPageResend: false,
|
||||||
|
supportsPageResendProbe: true,
|
||||||
|
requiresCountrySelection: true,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
id: PROVIDER_ID,
|
id: PROVIDER_ID,
|
||||||
label: '5sim',
|
label: '5sim',
|
||||||
|
capabilities,
|
||||||
defaultCountryId: DEFAULT_COUNTRY_ID,
|
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||||||
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||||||
supportedCountries: SUPPORTED_COUNTRY_ITEMS,
|
supportedCountries: SUPPORTED_COUNTRY_ITEMS,
|
||||||
@@ -925,18 +1079,38 @@
|
|||||||
normalizeCountryLabel: normalizeFiveSimCountryLabel,
|
normalizeCountryLabel: normalizeFiveSimCountryLabel,
|
||||||
formatCountryLabel: formatFiveSimCountryLabel,
|
formatCountryLabel: formatFiveSimCountryLabel,
|
||||||
normalizeCountryFallback: normalizeFiveSimCountryFallback,
|
normalizeCountryFallback: normalizeFiveSimCountryFallback,
|
||||||
|
normalizeCountryKey,
|
||||||
normalizeMaxPrice: normalizeFiveSimMaxPrice,
|
normalizeMaxPrice: normalizeFiveSimMaxPrice,
|
||||||
normalizeOperator: normalizeFiveSimOperator,
|
normalizeOperator: normalizeFiveSimOperator,
|
||||||
|
normalizeActivation,
|
||||||
resolveCountryCandidates,
|
resolveCountryCandidates,
|
||||||
|
resolveCountryLabel,
|
||||||
|
resolveActivationCountry,
|
||||||
|
getActivationCountryKey,
|
||||||
|
getActivationPrice,
|
||||||
requestActivation: (state, options) => requestActivation(state, options, providerDeps),
|
requestActivation: (state, options) => requestActivation(state, options, providerDeps),
|
||||||
reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps),
|
reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps),
|
||||||
finishActivation: (state, activation) => finishActivation(state, activation, providerDeps),
|
finishActivation: (state, activation) => finishActivation(state, activation, providerDeps),
|
||||||
cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps),
|
cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps),
|
||||||
banActivation: (state, activation) => banActivation(state, activation, providerDeps),
|
banActivation: (state, activation) => banActivation(state, activation, providerDeps),
|
||||||
|
requestAdditionalSms: (state, activation) => requestAdditionalSms(state, activation, providerDeps),
|
||||||
|
rotateActivation: (state, activation, options) => rotateActivation(state, activation, options, providerDeps),
|
||||||
pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps),
|
pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps),
|
||||||
|
prepareActivationForReuse: (state, activation, options) => prepareActivationForReuse(state, activation, options, providerDeps),
|
||||||
|
canPersistReusableActivation: () => true,
|
||||||
|
canPreserveActivationForFreeReuse: (_state, activation) => Boolean(
|
||||||
|
normalizeActivation(activation)
|
||||||
|
&& activation
|
||||||
|
&& typeof activation === 'object'
|
||||||
|
&& activation.phoneCodeReceived
|
||||||
|
),
|
||||||
|
shouldUsePageResend: () => false,
|
||||||
|
shouldProbePageResend: () => true,
|
||||||
fetchBalance: (state) => fetchBalance(state, providerDeps),
|
fetchBalance: (state) => fetchBalance(state, providerDeps),
|
||||||
fetchCountries: (state) => fetchCountries(state, providerDeps),
|
fetchCountries: (state) => fetchCountries(state, providerDeps),
|
||||||
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
|
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
|
||||||
|
resolvePriceRange,
|
||||||
|
formatPriceRangeText,
|
||||||
collectPriceEntries,
|
collectPriceEntries,
|
||||||
describePayload,
|
describePayload,
|
||||||
};
|
};
|
||||||
@@ -954,8 +1128,13 @@
|
|||||||
normalizeFiveSimCountryFallback,
|
normalizeFiveSimCountryFallback,
|
||||||
normalizeFiveSimCountryId,
|
normalizeFiveSimCountryId,
|
||||||
normalizeFiveSimCountryLabel,
|
normalizeFiveSimCountryLabel,
|
||||||
|
normalizeCountryKey,
|
||||||
formatFiveSimCountryLabel,
|
formatFiveSimCountryLabel,
|
||||||
normalizeFiveSimMaxPrice,
|
normalizeFiveSimMaxPrice,
|
||||||
normalizeFiveSimOperator,
|
normalizeFiveSimOperator,
|
||||||
|
resolvePriceRange,
|
||||||
|
formatPriceRangeText,
|
||||||
|
normalizeActivation,
|
||||||
|
resolveActivationCountry,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+1300
-5
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,19 @@
|
|||||||
const DEFAULT_SERVICE = 'openai';
|
const DEFAULT_SERVICE = 'openai';
|
||||||
const DEFAULT_MODE = 'routing_plan';
|
const DEFAULT_MODE = 'routing_plan';
|
||||||
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
||||||
|
const DEFAULT_POLL_TIMEOUT_MS = 180000;
|
||||||
|
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
||||||
|
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
|
||||||
|
const COUNTRY_BY_PHONE_PREFIX = Object.freeze([
|
||||||
|
{ prefix: '84', id: 'VN', label: 'Vietnam' },
|
||||||
|
{ prefix: '66', id: 'TH', label: 'Thailand' },
|
||||||
|
{ prefix: '62', id: 'ID', label: 'Indonesia' },
|
||||||
|
{ prefix: '44', id: 'GB', label: 'United Kingdom' },
|
||||||
|
{ prefix: '81', id: 'JP', label: 'Japan' },
|
||||||
|
{ prefix: '49', id: 'DE', label: 'Germany' },
|
||||||
|
{ prefix: '33', id: 'FR', label: 'France' },
|
||||||
|
{ prefix: '1', id: 'US', label: 'USA' },
|
||||||
|
]);
|
||||||
|
|
||||||
function normalizeText(value = '', fallback = '') {
|
function normalizeText(value = '', fallback = '') {
|
||||||
return String(value || '').trim() || fallback;
|
return String(value || '').trim() || fallback;
|
||||||
@@ -29,6 +42,15 @@
|
|||||||
return normalizeText(value).toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
return normalizeText(value).toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOperator(value = '') {
|
||||||
|
const rawValue = normalizeText(value);
|
||||||
|
const compactValue = rawValue.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
||||||
|
if (!rawValue || compactValue === 'any' || compactValue === 'anyoperator') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return rawValue.toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeCountry(value = '') {
|
function normalizeCountry(value = '') {
|
||||||
const trimmed = normalizeText(value);
|
const trimmed = normalizeText(value);
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -44,6 +66,52 @@
|
|||||||
return lowered.replace(/[^a-z0-9_-]+/g, '');
|
return lowered.replace(/[^a-z0-9_-]+/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCountryKey(value) {
|
||||||
|
return normalizeCountry(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegionDisplayName(regionCode, locale = 'en') {
|
||||||
|
const normalizedRegionCode = normalizeCountry(regionCode);
|
||||||
|
const normalizedLocale = normalizeText(locale);
|
||||||
|
if (!/^[A-Z]{2}$/.test(normalizedRegionCode) || !normalizedLocale || typeof Intl?.DisplayNames !== 'function') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return String(
|
||||||
|
new Intl.DisplayNames([normalizedLocale], { type: 'region' }).of(normalizedRegionCode) || ''
|
||||||
|
).trim();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCountryLabel(value = '', countryCode = '') {
|
||||||
|
const label = normalizeText(value);
|
||||||
|
if (label) {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
const normalizedCountryCode = normalizeCountry(countryCode);
|
||||||
|
if (!normalizedCountryCode) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return getRegionDisplayName(normalizedCountryCode, 'en') || normalizedCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferCountryFromPhoneNumber(phoneNumber = '') {
|
||||||
|
const digits = String(phoneNumber || '').replace(/\D+/g, '');
|
||||||
|
if (!digits) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix));
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: normalizeCountry(match.id),
|
||||||
|
label: normalizeCountryLabel(match.label, match.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeBoolean(value, fallback = false) {
|
function normalizeBoolean(value, fallback = false) {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
return Boolean(fallback);
|
return Boolean(fallback);
|
||||||
@@ -196,6 +264,7 @@
|
|||||||
service: normalizeText(options?.service || state?.madaoServiceName, DEFAULT_SERVICE),
|
service: normalizeText(options?.service || state?.madaoServiceName, DEFAULT_SERVICE),
|
||||||
};
|
};
|
||||||
const country = normalizeCountry(options?.country || state?.madaoCountry);
|
const country = normalizeCountry(options?.country || state?.madaoCountry);
|
||||||
|
const operator = normalizeOperator(options?.operator || state?.madaoOperator);
|
||||||
const minPrice = normalizePrice(options?.minPrice ?? state?.madaoMinPrice);
|
const minPrice = normalizePrice(options?.minPrice ?? state?.madaoMinPrice);
|
||||||
const maxPrice = normalizePrice(options?.maxPrice ?? state?.madaoMaxPrice);
|
const maxPrice = normalizePrice(options?.maxPrice ?? state?.madaoMaxPrice);
|
||||||
|
|
||||||
@@ -209,6 +278,9 @@
|
|||||||
if (country) {
|
if (country) {
|
||||||
request.country = country;
|
request.country = country;
|
||||||
}
|
}
|
||||||
|
if (operator) {
|
||||||
|
request.metadata = { operator };
|
||||||
|
}
|
||||||
if (minPrice !== null) {
|
if (minPrice !== null) {
|
||||||
request.min_price = minPrice;
|
request.min_price = minPrice;
|
||||||
}
|
}
|
||||||
@@ -245,6 +317,68 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeActivation(record = {}, fallback = {}) {
|
||||||
|
const direct = normalizeActivationFromAcquire(record, fallback);
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
const source = record && typeof record === 'object' && !Array.isArray(record) ? record : {};
|
||||||
|
const ticketId = normalizeText(source.activationId || source.ticketId || source.id || fallback.activationId);
|
||||||
|
const phoneNumber = normalizeText(source.phoneNumber || source.phone || fallback.phoneNumber);
|
||||||
|
if (!ticketId || !phoneNumber) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const inferredCountry = inferCountryFromPhoneNumber(phoneNumber);
|
||||||
|
const countryId = normalizeCountry(source.countryId ?? source.country ?? fallback.countryId ?? inferredCountry?.id);
|
||||||
|
const price = normalizePrice(source.madaoPrice ?? source.price ?? fallback.madaoPrice ?? fallback.price);
|
||||||
|
return {
|
||||||
|
activationId: ticketId,
|
||||||
|
phoneNumber,
|
||||||
|
provider: PROVIDER_ID,
|
||||||
|
serviceCode: normalizeText(source.serviceCode || source.service || fallback.serviceCode, DEFAULT_SERVICE),
|
||||||
|
countryId,
|
||||||
|
countryLabel: normalizeCountryLabel(source.countryLabel || source.country_label || fallback.countryLabel, countryId),
|
||||||
|
maxUses: Math.max(1, Math.floor(Number(source.maxUses ?? fallback.maxUses) || 1)),
|
||||||
|
successfulUses: Math.max(0, Math.floor(Number(source.successfulUses ?? fallback.successfulUses) || 0)),
|
||||||
|
...(source.source ? { source: normalizeText(source.source) } : {}),
|
||||||
|
...(source.phoneCodeReceived ? { phoneCodeReceived: true } : {}),
|
||||||
|
...(source.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(source.phoneCodeReceivedAt) || 0) } : {}),
|
||||||
|
...(source.madaoProviderId ? { madaoProviderId: normalizeProviderId(source.madaoProviderId) } : {}),
|
||||||
|
...(source.madaoRoutingPlanId ? { madaoRoutingPlanId: normalizeText(source.madaoRoutingPlanId) } : {}),
|
||||||
|
...(source.madaoRoutingPlanName ? { madaoRoutingPlanName: normalizeText(source.madaoRoutingPlanName) } : {}),
|
||||||
|
...(source.madaoRoutingItemId ? { madaoRoutingItemId: normalizeText(source.madaoRoutingItemId) } : {}),
|
||||||
|
...(source.madaoAcquirePath ? { madaoAcquirePath: mapAcquirePath(source.madaoAcquirePath) } : {}),
|
||||||
|
...(source.madaoStatus ? { madaoStatus: mapTicketStatus(source.madaoStatus) } : {}),
|
||||||
|
...(price !== null ? { madaoPrice: price } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCountryLabel(_state = {}, countryId = '') {
|
||||||
|
return normalizeCountryLabel('', countryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveActivationCountry(activation = {}) {
|
||||||
|
const normalizedActivation = normalizeActivation(activation)
|
||||||
|
|| (activation && typeof activation === 'object' ? activation : {});
|
||||||
|
const inferredCountry = inferCountryFromPhoneNumber(normalizedActivation.phoneNumber);
|
||||||
|
const countryId = normalizeCountry(normalizedActivation.countryId ?? normalizedActivation.country ?? inferredCountry?.id);
|
||||||
|
return {
|
||||||
|
id: countryId,
|
||||||
|
label: normalizeCountryLabel(normalizedActivation.countryLabel || inferredCountry?.label, countryId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivationCountryKey(activation = {}) {
|
||||||
|
const normalizedActivation = normalizeActivation(activation)
|
||||||
|
|| (activation && typeof activation === 'object' ? activation : {});
|
||||||
|
const inferredCountry = inferCountryFromPhoneNumber(normalizedActivation.phoneNumber);
|
||||||
|
return normalizeCountryKey(normalizedActivation.countryId ?? normalizedActivation.country ?? inferredCountry?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivationPrice(activation = {}) {
|
||||||
|
return normalizePrice(activation?.madaoPrice ?? activation?.selectedPrice ?? activation?.price ?? activation?.maxPrice);
|
||||||
|
}
|
||||||
|
|
||||||
function extractVerificationCode(value = '') {
|
function extractVerificationCode(value = '') {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -315,6 +449,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
|
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
|
||||||
|
const configuredTimeoutMs = Number(options.timeoutMs);
|
||||||
|
const timeoutMs = Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs > 0
|
||||||
|
? Math.max(1000, configuredTimeoutMs)
|
||||||
|
: 0;
|
||||||
|
if (!timeoutMs) {
|
||||||
const payload = await pollActivation(state, activation, deps);
|
const payload = await pollActivation(state, activation, deps);
|
||||||
const code = extractCodeFromPollPayload(payload);
|
const code = extractCodeFromPollPayload(payload);
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -329,6 +468,58 @@
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const intervalMs = Math.max(1000, Number(options.intervalMs) || DEFAULT_POLL_INTERVAL_MS);
|
||||||
|
const maxRoundsRaw = Math.floor(Number(options.maxRounds));
|
||||||
|
const maxRounds = Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? maxRoundsRaw : 0;
|
||||||
|
const start = Date.now();
|
||||||
|
let pollCount = 0;
|
||||||
|
let lastResponse = '';
|
||||||
|
while (Date.now() - start < timeoutMs) {
|
||||||
|
if (maxRounds > 0 && pollCount >= maxRounds) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
deps.throwIfStopped?.();
|
||||||
|
const payload = await pollActivation(state, activation, deps);
|
||||||
|
const code = extractCodeFromPollPayload(payload);
|
||||||
|
const statusText = normalizeText(
|
||||||
|
payload?.status
|
||||||
|
|| payload?.madaoStatus
|
||||||
|
|| payload?.message
|
||||||
|
|| payload?.text
|
||||||
|
|| describePayload(payload),
|
||||||
|
'PENDING'
|
||||||
|
);
|
||||||
|
lastResponse = statusText;
|
||||||
|
pollCount += 1;
|
||||||
|
if (typeof options.onStatus === 'function') {
|
||||||
|
await options.onStatus({
|
||||||
|
activation,
|
||||||
|
elapsedMs: Date.now() - start,
|
||||||
|
pollCount,
|
||||||
|
statusText,
|
||||||
|
timeoutMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (code) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
if (/^(cancelled|canceled|failed|expired|timeout)$/i.test(statusText)) {
|
||||||
|
throw new Error(`MaDao 订单在收到短信前已结束:${statusText}`);
|
||||||
|
}
|
||||||
|
if (typeof options.onWaitingForCode === 'function') {
|
||||||
|
await options.onWaitingForCode({
|
||||||
|
activation,
|
||||||
|
elapsedMs: Date.now() - start,
|
||||||
|
pollCount,
|
||||||
|
statusText,
|
||||||
|
timeoutMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await deps.sleepWithStop?.(intervalMs);
|
||||||
|
}
|
||||||
|
throw new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${lastResponse ? ` MaDao 最后状态:${lastResponse}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function releaseActivation(state = {}, activation, action = 'cancel', deps = {}) {
|
async function releaseActivation(state = {}, activation, action = 'cancel', deps = {}) {
|
||||||
const config = resolveConfig(state, deps);
|
const config = resolveConfig(state, deps);
|
||||||
const ticketId = normalizeText(activation?.activationId || activation?.ticketId);
|
const ticketId = normalizeText(activation?.activationId || activation?.ticketId);
|
||||||
@@ -366,7 +557,6 @@
|
|||||||
routing_plan_id: activation?.madaoRoutingPlanId,
|
routing_plan_id: activation?.madaoRoutingPlanId,
|
||||||
routing_plan_name: activation?.madaoRoutingPlanName,
|
routing_plan_name: activation?.madaoRoutingPlanName,
|
||||||
service: activation?.serviceCode,
|
service: activation?.serviceCode,
|
||||||
country: activation?.countryId,
|
|
||||||
});
|
});
|
||||||
if (!nextTicket) {
|
if (!nextTicket) {
|
||||||
throw new Error('MaDao 返回的下一条路由激活记录无效。');
|
throw new Error('MaDao 返回的下一条路由激活记录无效。');
|
||||||
@@ -399,15 +589,62 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function finishActivation(state = {}, activation, deps = {}) {
|
||||||
|
return releaseActivation(state, activation, 'finish', deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelActivation(state = {}, activation, deps = {}) {
|
||||||
|
return releaseActivation(state, activation, 'cancel', deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function banActivation(state = {}, activation, deps = {}) {
|
||||||
|
return releaseActivation(state, activation, 'ban', deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reuseActivation(_state = {}, activation) {
|
||||||
|
return activation && typeof activation === 'object' ? { ...activation } : activation;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestAdditionalSms() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCountryCandidates() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
function createProvider(deps = {}) {
|
function createProvider(deps = {}) {
|
||||||
|
const capabilities = Object.freeze({
|
||||||
|
supportsReusableActivation: false,
|
||||||
|
supportsAutomaticFreeReuse: false,
|
||||||
|
supportsFreeReusePreservation: false,
|
||||||
|
supportsPageResend: false,
|
||||||
|
supportsPageResendProbe: false,
|
||||||
|
supportsRouteReplace: true,
|
||||||
|
requiresCountrySelection: false,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
id: PROVIDER_ID,
|
id: PROVIDER_ID,
|
||||||
label: 'MaDao',
|
label: 'MaDao',
|
||||||
|
capabilities,
|
||||||
defaultProduct: DEFAULT_SERVICE,
|
defaultProduct: DEFAULT_SERVICE,
|
||||||
|
normalizeCountryId: normalizeCountry,
|
||||||
|
normalizeCountryLabel,
|
||||||
|
normalizeCountryKey,
|
||||||
|
normalizeActivation,
|
||||||
|
resolveCountryLabel,
|
||||||
|
resolveActivationCountry,
|
||||||
|
getActivationCountryKey,
|
||||||
|
getActivationPrice,
|
||||||
|
requestActivation: (state, options = {}, runtimeDeps = {}) => acquireActivation(state, options, {
|
||||||
|
...deps,
|
||||||
|
...runtimeDeps,
|
||||||
|
}),
|
||||||
acquireActivation: (state, options = {}, runtimeDeps = {}) => acquireActivation(state, options, {
|
acquireActivation: (state, options = {}, runtimeDeps = {}) => acquireActivation(state, options, {
|
||||||
...deps,
|
...deps,
|
||||||
...runtimeDeps,
|
...runtimeDeps,
|
||||||
}),
|
}),
|
||||||
|
reuseActivation,
|
||||||
pollActivation: (state, activation, runtimeDeps = {}) => pollActivation(state, activation, {
|
pollActivation: (state, activation, runtimeDeps = {}) => pollActivation(state, activation, {
|
||||||
...deps,
|
...deps,
|
||||||
...runtimeDeps,
|
...runtimeDeps,
|
||||||
@@ -420,10 +657,32 @@
|
|||||||
...deps,
|
...deps,
|
||||||
...runtimeDeps,
|
...runtimeDeps,
|
||||||
}),
|
}),
|
||||||
|
finishActivation: (state, activation, runtimeDeps = {}) => finishActivation(state, activation, {
|
||||||
|
...deps,
|
||||||
|
...runtimeDeps,
|
||||||
|
}),
|
||||||
|
cancelActivation: (state, activation, runtimeDeps = {}) => cancelActivation(state, activation, {
|
||||||
|
...deps,
|
||||||
|
...runtimeDeps,
|
||||||
|
}),
|
||||||
|
banActivation: (state, activation, runtimeDeps = {}) => banActivation(state, activation, {
|
||||||
|
...deps,
|
||||||
|
...runtimeDeps,
|
||||||
|
}),
|
||||||
|
requestAdditionalSms,
|
||||||
rotateActivation: (state, activation, options = {}, runtimeDeps = {}) => rotateActivation(state, activation, options, {
|
rotateActivation: (state, activation, options = {}, runtimeDeps = {}) => rotateActivation(state, activation, options, {
|
||||||
...deps,
|
...deps,
|
||||||
...runtimeDeps,
|
...runtimeDeps,
|
||||||
}),
|
}),
|
||||||
|
prepareActivationForReuse: async () => ({
|
||||||
|
ok: false,
|
||||||
|
reason: 'prepare_unsupported',
|
||||||
|
message: 'MaDao 不支持自动白嫖复用准备。',
|
||||||
|
}),
|
||||||
|
canPersistReusableActivation: () => false,
|
||||||
|
canPreserveActivationForFreeReuse: () => false,
|
||||||
|
shouldUsePageResend: () => false,
|
||||||
|
shouldProbePageResend: () => false,
|
||||||
replaceRoutingActivation: (state, activation, options = {}, runtimeDeps = {}) => replaceRoutingActivation(state, activation, options, {
|
replaceRoutingActivation: (state, activation, options = {}, runtimeDeps = {}) => replaceRoutingActivation(state, activation, options, {
|
||||||
...deps,
|
...deps,
|
||||||
...runtimeDeps,
|
...runtimeDeps,
|
||||||
@@ -432,7 +691,9 @@
|
|||||||
extractCodeFromPollPayload,
|
extractCodeFromPollPayload,
|
||||||
mapAcquirePath,
|
mapAcquirePath,
|
||||||
mapTicketStatus,
|
mapTicketStatus,
|
||||||
|
normalizeActivation,
|
||||||
normalizeActivationFromAcquire,
|
normalizeActivationFromAcquire,
|
||||||
|
resolveCountryCandidates,
|
||||||
resolveConfig: (state = {}, runtimeDeps = {}) => resolveConfig(state, {
|
resolveConfig: (state = {}, runtimeDeps = {}) => resolveConfig(state, {
|
||||||
...deps,
|
...deps,
|
||||||
...runtimeDeps,
|
...runtimeDeps,
|
||||||
@@ -451,12 +712,23 @@
|
|||||||
extractCodeFromPollPayload,
|
extractCodeFromPollPayload,
|
||||||
mapAcquirePath,
|
mapAcquirePath,
|
||||||
mapTicketStatus,
|
mapTicketStatus,
|
||||||
|
normalizeActivation,
|
||||||
normalizeActivationFromAcquire,
|
normalizeActivationFromAcquire,
|
||||||
|
normalizeCountry,
|
||||||
|
normalizeCountryKey,
|
||||||
|
normalizeCountryLabel,
|
||||||
|
normalizeOperator,
|
||||||
|
resolveActivationCountry,
|
||||||
pollActivation,
|
pollActivation,
|
||||||
pollActivationCode,
|
pollActivationCode,
|
||||||
releaseActivation,
|
releaseActivation,
|
||||||
|
finishActivation,
|
||||||
|
cancelActivation,
|
||||||
|
banActivation,
|
||||||
replaceRoutingActivation,
|
replaceRoutingActivation,
|
||||||
|
requestAdditionalSms,
|
||||||
resolveConfig,
|
resolveConfig,
|
||||||
|
resolveCountryCandidates,
|
||||||
rotateActivation,
|
rotateActivation,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,14 @@
|
|||||||
const PROVIDER_FIVE_SIM = '5sim';
|
const PROVIDER_FIVE_SIM = '5sim';
|
||||||
const PROVIDER_NEXSMS = 'nexsms';
|
const PROVIDER_NEXSMS = 'nexsms';
|
||||||
const PROVIDER_MADAO = 'madao';
|
const PROVIDER_MADAO = 'madao';
|
||||||
|
const PROVIDER_SMS_BOWER = 'sms-bower';
|
||||||
const DEFAULT_PROVIDER = PROVIDER_HERO_SMS;
|
const DEFAULT_PROVIDER = PROVIDER_HERO_SMS;
|
||||||
const DEFAULT_PROVIDER_ORDER = Object.freeze([
|
const DEFAULT_PROVIDER_ORDER = Object.freeze([
|
||||||
PROVIDER_HERO_SMS,
|
PROVIDER_HERO_SMS,
|
||||||
PROVIDER_FIVE_SIM,
|
PROVIDER_FIVE_SIM,
|
||||||
PROVIDER_NEXSMS,
|
PROVIDER_NEXSMS,
|
||||||
PROVIDER_MADAO,
|
PROVIDER_MADAO,
|
||||||
|
PROVIDER_SMS_BOWER,
|
||||||
]);
|
]);
|
||||||
const PROVIDER_DEFINITIONS = Object.freeze({
|
const PROVIDER_DEFINITIONS = Object.freeze({
|
||||||
[PROVIDER_HERO_SMS]: Object.freeze({
|
[PROVIDER_HERO_SMS]: Object.freeze({
|
||||||
@@ -34,6 +36,11 @@
|
|||||||
label: 'MaDao',
|
label: 'MaDao',
|
||||||
moduleKey: 'PhoneSmsMaDaoProvider',
|
moduleKey: 'PhoneSmsMaDaoProvider',
|
||||||
}),
|
}),
|
||||||
|
[PROVIDER_SMS_BOWER]: Object.freeze({
|
||||||
|
id: PROVIDER_SMS_BOWER,
|
||||||
|
label: 'SMS Bower',
|
||||||
|
moduleKey: 'PhoneSmsBowerProvider',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
function resolveProviderKey(value = '') {
|
function resolveProviderKey(value = '') {
|
||||||
@@ -133,6 +140,7 @@
|
|||||||
PROVIDER_FIVE_SIM,
|
PROVIDER_FIVE_SIM,
|
||||||
PROVIDER_NEXSMS,
|
PROVIDER_NEXSMS,
|
||||||
PROVIDER_MADAO,
|
PROVIDER_MADAO,
|
||||||
|
PROVIDER_SMS_BOWER,
|
||||||
DEFAULT_PROVIDER,
|
DEFAULT_PROVIDER,
|
||||||
DEFAULT_PROVIDER_ORDER,
|
DEFAULT_PROVIDER_ORDER,
|
||||||
PROVIDER_DEFINITIONS,
|
PROVIDER_DEFINITIONS,
|
||||||
|
|||||||
@@ -0,0 +1,471 @@
|
|||||||
|
// phone-sms/providers/sms-bower.js — SMS Bower 接码平台适配层
|
||||||
|
(function attachSmsBowerProvider(root, factory) {
|
||||||
|
root.PhoneSmsBowerProvider = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createSmsBowerProviderModule() {
|
||||||
|
const PROVIDER_ID = 'sms-bower';
|
||||||
|
const DEFAULT_BASE_URL = 'https://smsbower.page/stubs/handler_api.php';
|
||||||
|
const DEFAULT_SERVICE_CODE = 'ot';
|
||||||
|
const DEFAULT_SERVICE_LABEL = 'OpenAI';
|
||||||
|
const DEFAULT_COUNTRY_ID = 52;
|
||||||
|
const DEFAULT_COUNTRY_LABEL = 'Thailand';
|
||||||
|
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
||||||
|
const DEFAULT_POLL_TIMEOUT_MS = 180000;
|
||||||
|
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
||||||
|
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
|
||||||
|
const MAX_PRICE_CANDIDATES = 8;
|
||||||
|
|
||||||
|
function normalizeText(value = '', fallback = '') {
|
||||||
|
return String(value || '').trim() || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSmsBowerCountryId(value, fallback = DEFAULT_COUNTRY_ID) {
|
||||||
|
const parsed = Math.floor(Number(value));
|
||||||
|
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
||||||
|
const fallbackParsed = Math.floor(Number(fallback));
|
||||||
|
return Number.isFinite(fallbackParsed) && fallbackParsed >= 0 ? fallbackParsed : DEFAULT_COUNTRY_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSmsBowerCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||||||
|
return normalizeText(value, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCountryKey(value) {
|
||||||
|
const countryId = normalizeSmsBowerCountryId(value, -1);
|
||||||
|
return countryId >= 0 ? String(countryId) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSmsBowerServiceCode(value = '', fallback = DEFAULT_SERVICE_CODE) {
|
||||||
|
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||||
|
if (normalized) return normalized;
|
||||||
|
const fallbackNormalized = String(fallback || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||||
|
return fallbackNormalized || DEFAULT_SERVICE_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePriceLimit(value) {
|
||||||
|
if (value === undefined || value === null || String(value).trim() === '') return null;
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
||||||
|
return Math.round(parsed * 10000) / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSmsBowerMaxPrice(value = '') {
|
||||||
|
const normalized = normalizePriceLimit(value);
|
||||||
|
return normalized === null ? '' : String(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCsvIds(value = '') {
|
||||||
|
const source = Array.isArray(value) ? value : String(value || '').split(/[\r\n,,;;]+/);
|
||||||
|
const seen = new Set();
|
||||||
|
return source
|
||||||
|
.map((entry) => String(entry || '').trim().replace(/[^0-9a-zA-Z_-]+/g, ''))
|
||||||
|
.filter((entry) => {
|
||||||
|
if (!entry || seen.has(entry)) return false;
|
||||||
|
seen.add(entry);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCountryOrder(value = []) {
|
||||||
|
const source = Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: String(value || '').split(/[\r\n,,;;]+/).map((entry) => String(entry || '').trim()).filter(Boolean);
|
||||||
|
const seen = new Set();
|
||||||
|
const normalized = [];
|
||||||
|
source.forEach((entry) => {
|
||||||
|
let id = -1;
|
||||||
|
let label = '';
|
||||||
|
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||||
|
id = normalizeSmsBowerCountryId(entry.id ?? entry.countryId, -1);
|
||||||
|
label = normalizeText(entry.label ?? entry.countryLabel, '');
|
||||||
|
} else {
|
||||||
|
const text = String(entry || '').trim();
|
||||||
|
const structured = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/);
|
||||||
|
id = normalizeSmsBowerCountryId(structured?.[1] || text, -1);
|
||||||
|
label = normalizeText(structured?.[2], '');
|
||||||
|
}
|
||||||
|
if (id < 0 || seen.has(id)) return;
|
||||||
|
seen.add(id);
|
||||||
|
normalized.push({ id, label: label || `Country #${id}` });
|
||||||
|
});
|
||||||
|
return normalized.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBaseUrl(value = '') {
|
||||||
|
const trimmed = String(value || '').trim() || DEFAULT_BASE_URL;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(trimmed);
|
||||||
|
return parsed.toString();
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_BASE_URL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePayload(text) {
|
||||||
|
const trimmed = String(text || '').trim();
|
||||||
|
if (!trimmed) return '';
|
||||||
|
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||||
|
try { return JSON.parse(trimmed); } catch { return trimmed; }
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describePayload(raw) {
|
||||||
|
if (typeof raw === 'string') return raw.trim();
|
||||||
|
if (raw && typeof raw === 'object') {
|
||||||
|
const direct = normalizeText(raw.message || raw.msg || raw.error || raw.title || raw.status, '');
|
||||||
|
if (direct) return direct;
|
||||||
|
try { return JSON.stringify(raw); } catch { return String(raw); }
|
||||||
|
}
|
||||||
|
return String(raw || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveConfig(state = {}, deps = {}) {
|
||||||
|
return {
|
||||||
|
apiKey: normalizeText(state.smsBowerApiKey),
|
||||||
|
baseUrl: normalizeBaseUrl(state.smsBowerBaseUrl || DEFAULT_BASE_URL),
|
||||||
|
serviceCode: normalizeSmsBowerServiceCode(state.smsBowerServiceCode, DEFAULT_SERVICE_CODE),
|
||||||
|
fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null),
|
||||||
|
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(config = {}, query = {}) {
|
||||||
|
const url = new URL(config.baseUrl || DEFAULT_BASE_URL);
|
||||||
|
url.searchParams.set('api_key', config.apiKey);
|
||||||
|
Object.entries(query || {}).forEach(([key, value]) => {
|
||||||
|
if (value === undefined || value === null || value === '') return;
|
||||||
|
url.searchParams.set(key, String(value));
|
||||||
|
});
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPayload(config = {}, query = {}, actionLabel = 'SMS Bower 请求') {
|
||||||
|
if (!config.fetchImpl) throw new Error('SMS Bower 网络请求实现不可用。');
|
||||||
|
if (!config.apiKey) throw new Error('SMS Bower API Key 缺失,请先在侧边栏保存接码 API Key。');
|
||||||
|
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||||||
|
const timeoutId = controller ? setTimeout(() => controller.abort(), Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS) : null;
|
||||||
|
try {
|
||||||
|
const response = await config.fetchImpl(buildUrl(config, query), {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller?.signal,
|
||||||
|
headers: { Accept: 'application/json, text/plain, */*' },
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
const payload = parsePayload(text);
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(`${actionLabel}失败:${describePayload(payload) || response.status}`);
|
||||||
|
error.payload = payload;
|
||||||
|
error.status = response.status;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const message = describePayload(payload);
|
||||||
|
if (/^(BAD_KEY|BAD_ACTION|BAD_SERVICE|BAD_STATUS|NO_ACTIVATION|EARLY_CANCEL_DENIED|BAD_COUNTRY)\b/i.test(message)) {
|
||||||
|
const error = new Error(`${actionLabel}失败:${message}`);
|
||||||
|
error.payload = payload;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError') throw new Error(`${actionLabel}超时。`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCountryCandidates(state = {}) {
|
||||||
|
const candidates = normalizeCountryOrder(state.smsBowerCountryOrder);
|
||||||
|
if (candidates.length) return candidates;
|
||||||
|
return [{
|
||||||
|
id: normalizeSmsBowerCountryId(state.smsBowerCountryId, DEFAULT_COUNTRY_ID),
|
||||||
|
label: normalizeSmsBowerCountryLabel(state.smsBowerCountryLabel, DEFAULT_COUNTRY_LABEL),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCountryLabel(state = {}, countryId = DEFAULT_COUNTRY_ID) {
|
||||||
|
const key = normalizeCountryKey(countryId);
|
||||||
|
const matched = resolveCountryCandidates(state).find((entry) => normalizeCountryKey(entry.id) === key);
|
||||||
|
return matched?.label || (key ? `Country #${key}` : DEFAULT_COUNTRY_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePriceRange(state = {}) {
|
||||||
|
const minPriceLimit = normalizePriceLimit(state.smsBowerMinPrice);
|
||||||
|
const maxPriceLimit = normalizePriceLimit(state.smsBowerMaxPrice);
|
||||||
|
return {
|
||||||
|
minPriceLimit,
|
||||||
|
maxPriceLimit,
|
||||||
|
hasMinPriceLimit: minPriceLimit !== null,
|
||||||
|
hasMaxPriceLimit: maxPriceLimit !== null,
|
||||||
|
invalidRange: minPriceLimit !== null && maxPriceLimit !== null && minPriceLimit > maxPriceLimit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPriceRangeText(minPriceLimit = null, maxPriceLimit = null) {
|
||||||
|
const minPrice = normalizePriceLimit(minPriceLimit);
|
||||||
|
const maxPrice = normalizePriceLimit(maxPriceLimit);
|
||||||
|
if (minPrice !== null && maxPrice !== null) return `${minPrice}~${maxPrice}`;
|
||||||
|
if (minPrice !== null) return `${minPrice}~`;
|
||||||
|
if (maxPrice !== null) return `~${maxPrice}`;
|
||||||
|
return 'unbounded';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBalance(state = {}, deps = {}) {
|
||||||
|
const config = resolveConfig(state, deps);
|
||||||
|
const payload = await fetchPayload(config, { action: 'getBalance' }, 'SMS Bower 查询余额');
|
||||||
|
const matched = describePayload(payload).match(/ACCESS_BALANCE\s*:\s*([\d.]+)/i);
|
||||||
|
return { balance: matched ? Number(matched[1]) : null, raw: payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPrices(state = {}, countryConfig = {}, deps = {}) {
|
||||||
|
const config = resolveConfig(state, deps);
|
||||||
|
return fetchPayload(config, {
|
||||||
|
action: 'getPrices',
|
||||||
|
service: config.serviceCode,
|
||||||
|
country: normalizeSmsBowerCountryId(countryConfig?.id ?? state.smsBowerCountryId, DEFAULT_COUNTRY_ID),
|
||||||
|
}, 'SMS Bower 查询价格');
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectPriceEntries(payload, entries = []) {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
payload.forEach((entry) => collectPriceEntries(entry, entries));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
if (!payload || typeof payload !== 'object') return entries;
|
||||||
|
const cost = Number(payload.cost ?? payload.price ?? payload.Price);
|
||||||
|
if (Number.isFinite(cost) && cost >= 0) {
|
||||||
|
const count = Number(payload.count ?? payload.qty ?? payload.Qty);
|
||||||
|
entries.push({ cost, count: Number.isFinite(count) ? count : 0, inStock: !Number.isFinite(count) || count > 0 });
|
||||||
|
}
|
||||||
|
Object.values(payload).forEach((entry) => collectPriceEntries(entry, entries));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeActivation(payload, fallback = {}) {
|
||||||
|
if (typeof payload === 'string') {
|
||||||
|
const matched = payload.match(/^ACCESS_NUMBER\s*:\s*([^:]+)\s*:\s*(.+)$/i);
|
||||||
|
if (matched) {
|
||||||
|
payload = { activationId: matched[1], phoneNumber: matched[2] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const source = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
||||||
|
const activationId = normalizeText(source.activationId ?? source.id ?? fallback.activationId);
|
||||||
|
const phoneNumber = normalizeText(source.phoneNumber ?? source.phone ?? fallback.phoneNumber);
|
||||||
|
if (!activationId || !phoneNumber) return null;
|
||||||
|
const countryId = normalizeSmsBowerCountryId(source.countryId ?? source.country ?? fallback.countryId, DEFAULT_COUNTRY_ID);
|
||||||
|
return {
|
||||||
|
activationId,
|
||||||
|
phoneNumber,
|
||||||
|
provider: PROVIDER_ID,
|
||||||
|
serviceCode: normalizeSmsBowerServiceCode(source.serviceCode || fallback.serviceCode || DEFAULT_SERVICE_CODE),
|
||||||
|
countryId,
|
||||||
|
countryCode: countryId,
|
||||||
|
countryLabel: normalizeSmsBowerCountryLabel(source.countryLabel || fallback.countryLabel, `Country #${countryId}`),
|
||||||
|
successfulUses: Math.max(0, Math.floor(Number(source.successfulUses ?? fallback.successfulUses) || 0)),
|
||||||
|
maxUses: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveActivationCountry(activation = {}, state = {}) {
|
||||||
|
const normalized = normalizeActivation(activation) || (activation && typeof activation === 'object' ? activation : {});
|
||||||
|
const id = normalizeSmsBowerCountryId(normalized.countryId ?? normalized.countryCode ?? normalized.country, DEFAULT_COUNTRY_ID);
|
||||||
|
return { id, label: resolveCountryLabel(state, id) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivationCountryKey(activation = {}) {
|
||||||
|
return normalizeCountryKey(activation?.countryCode ?? activation?.countryId ?? activation?.country);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivationPrice(activation = {}) {
|
||||||
|
const price = Number(activation?.selectedPrice ?? activation?.price ?? activation?.maxPrice);
|
||||||
|
return Number.isFinite(price) && price >= 0 ? price : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAcquireQuery(state = {}, config = resolveConfig(state), countryConfig = {}) {
|
||||||
|
const range = resolvePriceRange(state);
|
||||||
|
if (range.invalidRange) {
|
||||||
|
throw new Error(`SMS Bower 价格区间无效:最低购买价 ${range.minPriceLimit} 高于价格上限 ${range.maxPriceLimit}。`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
action: 'getNumber',
|
||||||
|
service: config.serviceCode,
|
||||||
|
country: normalizeSmsBowerCountryId(countryConfig.id, DEFAULT_COUNTRY_ID),
|
||||||
|
maxPrice: range.maxPriceLimit,
|
||||||
|
minPrice: range.minPriceLimit,
|
||||||
|
providerIds: normalizeCsvIds(state.smsBowerProviderIds),
|
||||||
|
exceptProviderIds: normalizeCsvIds(state.smsBowerExceptProviderIds),
|
||||||
|
phoneException: normalizeCsvIds(state.smsBowerPhoneException),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestActivation(state = {}, options = {}, deps = {}) {
|
||||||
|
const config = resolveConfig(state, deps);
|
||||||
|
const blocked = new Set((Array.isArray(options.blockedCountryIds) ? options.blockedCountryIds : []).map((v) => normalizeCountryKey(v)).filter(Boolean));
|
||||||
|
const candidates = resolveCountryCandidates(state).filter((entry) => !blocked.has(normalizeCountryKey(entry.id)));
|
||||||
|
const countryList = candidates.length ? candidates : resolveCountryCandidates(state);
|
||||||
|
const errors = [];
|
||||||
|
for (const countryConfig of countryList) {
|
||||||
|
try {
|
||||||
|
const payload = await fetchPayload(config, buildAcquireQuery(state, config, countryConfig), `SMS Bower 购买手机号(${countryConfig.label || countryConfig.id})`);
|
||||||
|
const activation = normalizeActivation(payload, {
|
||||||
|
countryId: countryConfig.id,
|
||||||
|
countryLabel: countryConfig.label,
|
||||||
|
serviceCode: config.serviceCode,
|
||||||
|
});
|
||||||
|
if (activation) return activation;
|
||||||
|
throw new Error(`SMS Bower 购买手机号返回不可用响应:${describePayload(payload) || '空响应'}`);
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error.message || String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`SMS Bower 购买手机号失败:${errors.join(' | ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setActivationStatus(state = {}, activation, status, actionLabel, deps = {}) {
|
||||||
|
const normalized = normalizeActivation(activation);
|
||||||
|
if (!normalized) return '';
|
||||||
|
const config = resolveConfig(state, deps);
|
||||||
|
const payload = await fetchPayload(config, {
|
||||||
|
action: 'setStatus',
|
||||||
|
status,
|
||||||
|
id: normalized.activationId,
|
||||||
|
}, actionLabel);
|
||||||
|
return describePayload(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finishActivation(state = {}, activation, deps = {}) {
|
||||||
|
return setActivationStatus(state, activation, 6, 'SMS Bower 完成订单', deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelActivation(state = {}, activation, deps = {}) {
|
||||||
|
return setActivationStatus(state, activation, 8, 'SMS Bower 取消订单', deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function banActivation(state = {}, activation, deps = {}) {
|
||||||
|
return cancelActivation(state, activation, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestAdditionalSms(state = {}, activation, deps = {}) {
|
||||||
|
return setActivationStatus(state, activation, 3, 'SMS Bower 请求下一条短信', deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reuseActivation(state = {}, activation, deps = {}) {
|
||||||
|
await requestAdditionalSms(state, activation, deps);
|
||||||
|
return normalizeActivation(activation);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rotateActivation(state = {}, activation, options = {}, deps = {}) {
|
||||||
|
await (String(options?.releaseAction || '').trim().toLowerCase() === 'ban'
|
||||||
|
? banActivation(state, activation, deps)
|
||||||
|
: cancelActivation(state, activation, deps));
|
||||||
|
return { currentTicketId: String(activation?.activationId || activation?.id || ''), nextActivation: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractVerificationCode(raw = '') {
|
||||||
|
const trimmed = String(raw || '').trim();
|
||||||
|
if (!trimmed) return '';
|
||||||
|
const matched = trimmed.match(/\b(\d{4,8})\b/);
|
||||||
|
return matched?.[1] || trimmed.replace(/^STATUS_OK\s*:\s*/i, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
|
||||||
|
const normalized = normalizeActivation(activation);
|
||||||
|
if (!normalized) throw new Error('缺少手机号接码订单。');
|
||||||
|
const config = resolveConfig(state, deps);
|
||||||
|
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || DEFAULT_POLL_TIMEOUT_MS);
|
||||||
|
const intervalMs = Math.max(1000, Number(options.intervalMs) || DEFAULT_POLL_INTERVAL_MS);
|
||||||
|
const maxRoundsRaw = Math.floor(Number(options.maxRounds));
|
||||||
|
const maxRounds = Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? maxRoundsRaw : 0;
|
||||||
|
const start = Date.now();
|
||||||
|
let pollCount = 0;
|
||||||
|
let lastResponse = '';
|
||||||
|
while (Date.now() - start < timeoutMs) {
|
||||||
|
if (maxRounds > 0 && pollCount >= maxRounds) break;
|
||||||
|
deps.throwIfStopped?.();
|
||||||
|
const payload = await fetchPayload(config, { action: 'getStatus', id: normalized.activationId }, 'SMS Bower 查询验证码');
|
||||||
|
pollCount += 1;
|
||||||
|
lastResponse = describePayload(payload);
|
||||||
|
if (typeof options.onStatus === 'function') {
|
||||||
|
await options.onStatus({ activation: normalized, elapsedMs: Date.now() - start, pollCount, statusText: lastResponse, timeoutMs });
|
||||||
|
}
|
||||||
|
if (/^STATUS_OK\s*:/i.test(lastResponse)) return extractVerificationCode(lastResponse);
|
||||||
|
if (/^STATUS_CANCEL\b/i.test(lastResponse)) throw new Error('SMS Bower 查询验证码失败:订单已取消');
|
||||||
|
if (typeof options.onWaitingForCode === 'function') {
|
||||||
|
await options.onWaitingForCode({ activation: normalized, elapsedMs: Date.now() - start, pollCount, statusText: lastResponse, timeoutMs });
|
||||||
|
}
|
||||||
|
await deps.sleepWithStop?.(intervalMs);
|
||||||
|
}
|
||||||
|
throw new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${lastResponse ? ` SMS Bower 最后状态:${lastResponse}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProvider(deps = {}) {
|
||||||
|
const providerDeps = {
|
||||||
|
fetchImpl: deps.fetchImpl,
|
||||||
|
sleepWithStop: deps.sleepWithStop,
|
||||||
|
throwIfStopped: deps.throwIfStopped,
|
||||||
|
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
const capabilities = Object.freeze({
|
||||||
|
supportsReusableActivation: true,
|
||||||
|
supportsAutomaticFreeReuse: true,
|
||||||
|
supportsFreeReusePreservation: false,
|
||||||
|
supportsPageResend: false,
|
||||||
|
supportsPageResendProbe: true,
|
||||||
|
requiresCountrySelection: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id: PROVIDER_ID,
|
||||||
|
label: 'SMS Bower',
|
||||||
|
capabilities,
|
||||||
|
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||||||
|
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||||||
|
defaultProduct: DEFAULT_SERVICE_LABEL,
|
||||||
|
defaultServiceCode: DEFAULT_SERVICE_CODE,
|
||||||
|
normalizeCountryId: normalizeSmsBowerCountryId,
|
||||||
|
normalizeCountryLabel: normalizeSmsBowerCountryLabel,
|
||||||
|
normalizeCountryKey,
|
||||||
|
normalizeServiceCode: normalizeSmsBowerServiceCode,
|
||||||
|
normalizeMaxPrice: normalizeSmsBowerMaxPrice,
|
||||||
|
normalizeCountryFallback: normalizeCountryOrder,
|
||||||
|
normalizeActivation,
|
||||||
|
resolveCountryCandidates,
|
||||||
|
resolveCountryLabel,
|
||||||
|
resolveActivationCountry,
|
||||||
|
getActivationCountryKey,
|
||||||
|
getActivationPrice,
|
||||||
|
requestActivation: (state, options) => requestActivation(state, options, providerDeps),
|
||||||
|
reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps),
|
||||||
|
finishActivation: (state, activation) => finishActivation(state, activation, providerDeps),
|
||||||
|
cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps),
|
||||||
|
banActivation: (state, activation) => banActivation(state, activation, providerDeps),
|
||||||
|
requestAdditionalSms: (state, activation) => requestAdditionalSms(state, activation, providerDeps),
|
||||||
|
rotateActivation: (state, activation, options) => rotateActivation(state, activation, options, providerDeps),
|
||||||
|
pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps),
|
||||||
|
prepareActivationForReuse: async (_state, activation) => ({ activation: normalizeActivation(activation), ignoredPhoneCodeKeys: [] }),
|
||||||
|
canPersistReusableActivation: () => false,
|
||||||
|
canPreserveActivationForFreeReuse: () => false,
|
||||||
|
shouldUsePageResend: () => false,
|
||||||
|
shouldProbePageResend: () => true,
|
||||||
|
fetchBalance: (state) => fetchBalance(state, providerDeps),
|
||||||
|
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
|
||||||
|
resolvePriceRange,
|
||||||
|
formatPriceRangeText,
|
||||||
|
collectPriceEntries,
|
||||||
|
describePayload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
PROVIDER_ID,
|
||||||
|
DEFAULT_COUNTRY_ID,
|
||||||
|
DEFAULT_COUNTRY_LABEL,
|
||||||
|
DEFAULT_SERVICE_CODE,
|
||||||
|
createProvider,
|
||||||
|
normalizeSmsBowerCountryId,
|
||||||
|
normalizeSmsBowerCountryLabel,
|
||||||
|
normalizeCountryKey,
|
||||||
|
normalizeSmsBowerServiceCode,
|
||||||
|
normalizeSmsBowerMaxPrice,
|
||||||
|
normalizeActivation,
|
||||||
|
};
|
||||||
|
});
|
||||||
+41
-30
@@ -1367,6 +1367,7 @@
|
|||||||
<option value="5sim">5sim</option>
|
<option value="5sim">5sim</option>
|
||||||
<option value="nexsms">NexSMS</option>
|
<option value="nexsms">NexSMS</option>
|
||||||
<option value="madao">MaDao</option>
|
<option value="madao">MaDao</option>
|
||||||
|
<option value="sms-bower">SMS Bower</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-phone-sms-provider-order" style="display:none;">
|
<div class="data-row" id="row-phone-sms-provider-order" style="display:none;">
|
||||||
@@ -1377,6 +1378,7 @@
|
|||||||
<option value="5sim" selected>5sim</option>
|
<option value="5sim" selected>5sim</option>
|
||||||
<option value="nexsms" selected>NexSMS</option>
|
<option value="nexsms" selected>NexSMS</option>
|
||||||
<option value="madao" selected>MaDao</option>
|
<option value="madao" selected>MaDao</option>
|
||||||
|
<option value="sms-bower" selected>SMS Bower</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="hero-sms-country-mainline">
|
<div class="hero-sms-country-mainline">
|
||||||
<div id="phone-sms-provider-order-menu-shell" class="hero-sms-country-menu">
|
<div id="phone-sms-provider-order-menu-shell" class="hero-sms-country-menu">
|
||||||
@@ -1534,6 +1536,16 @@
|
|||||||
<input type="text" id="input-nex-sms-service-code" class="data-input mono"
|
<input type="text" id="input-nex-sms-service-code" class="data-input mono"
|
||||||
placeholder="ot" />
|
placeholder="ot" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row" id="row-sms-bower-api-key" style="display:none;">
|
||||||
|
<span class="data-label">SMS Bower</span>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<input type="password" id="input-sms-bower-api-key" class="data-input data-input-with-icon mono"
|
||||||
|
placeholder="只需填写 SMS Bower API Token" />
|
||||||
|
<button id="btn-toggle-sms-bower-api-key" class="input-icon-btn" type="button"
|
||||||
|
data-password-toggle="input-sms-bower-api-key" data-show-label="显示 SMS Bower Token"
|
||||||
|
data-hide-label="隐藏 SMS Bower Token" aria-label="显示 SMS Bower Token" title="显示 SMS Bower Token"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="data-row" id="row-madao-base-url" style="display:none;">
|
<div class="data-row" id="row-madao-base-url" style="display:none;">
|
||||||
<span class="data-label">MaDao 地址</span>
|
<span class="data-label">MaDao 地址</span>
|
||||||
<input type="text" id="input-madao-base-url" class="data-input mono"
|
<input type="text" id="input-madao-base-url" class="data-input mono"
|
||||||
@@ -1553,46 +1565,43 @@
|
|||||||
<span class="data-label">接入模式</span>
|
<span class="data-label">接入模式</span>
|
||||||
<select id="select-madao-mode" class="data-select mono">
|
<select id="select-madao-mode" class="data-select mono">
|
||||||
<option value="routing_plan">路由计划</option>
|
<option value="routing_plan">路由计划</option>
|
||||||
<option value="direct">直连参数</option>
|
<option value="direct">直连模式</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-madao-routing-plan-id" style="display:none;">
|
<div class="data-row" id="row-madao-routing-plan-id" style="display:none;">
|
||||||
<span class="data-label">路由计划</span>
|
<span class="data-label">路由计划</span>
|
||||||
<input type="text" id="input-madao-routing-plan-id" class="data-input mono"
|
<div class="data-inline data-value-actions">
|
||||||
placeholder="routing_plan_id" />
|
<select id="select-madao-routing-plan-id" class="data-select mono">
|
||||||
|
<option value="">请先刷新路由计划</option>
|
||||||
|
</select>
|
||||||
|
<button id="btn-madao-refresh-routing-plans" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-madao-provider-id" style="display:none;">
|
<div class="data-row" id="row-madao-provider-id" style="display:none;">
|
||||||
<span class="data-label">直连平台</span>
|
<span class="data-label">MaDao 服务商</span>
|
||||||
<input type="text" id="input-madao-provider-id" class="data-input mono"
|
<div class="data-inline data-value-actions">
|
||||||
placeholder="auto" />
|
<select id="select-madao-provider-id" class="data-select mono">
|
||||||
|
<option value="">请先刷新服务商</option>
|
||||||
|
</select>
|
||||||
|
<button id="btn-madao-refresh-providers" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-madao-country" style="display:none;">
|
<div class="data-row" id="row-madao-country" style="display:none;">
|
||||||
<span class="data-label">直连国家</span>
|
<span class="data-label">MaDao 国家</span>
|
||||||
<input type="text" id="input-madao-country" class="data-input mono"
|
<div class="data-inline data-value-actions">
|
||||||
placeholder="local / TH / any" />
|
<select id="select-madao-country" class="data-select mono">
|
||||||
</div>
|
<option value="">请先选择服务商</option>
|
||||||
<div class="data-row" id="row-madao-auto-pick-country" style="display:none;">
|
</select>
|
||||||
<span class="data-label">自动选国家</span>
|
<button id="btn-madao-refresh-countries" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
|
||||||
<div class="data-inline">
|
|
||||||
<label class="toggle-switch" for="input-madao-auto-pick-country">
|
|
||||||
<input type="checkbox" id="input-madao-auto-pick-country" />
|
|
||||||
<span class="toggle-switch-track" aria-hidden="true">
|
|
||||||
<span class="toggle-switch-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<span class="data-value">让 MaDao 根据直连平台自动选择国家</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-madao-reuse-phone" style="display:none;">
|
<div class="data-row" id="row-madao-operator" style="display:none;">
|
||||||
<span class="data-label">MaDao 复用</span>
|
<span class="data-label">MaDao 线路</span>
|
||||||
<div class="data-inline">
|
<div class="data-inline data-value-actions">
|
||||||
<label class="toggle-switch" for="input-madao-reuse-phone">
|
<select id="select-madao-operator" class="data-select mono">
|
||||||
<input type="checkbox" id="input-madao-reuse-phone" />
|
<option value="">任意线路</option>
|
||||||
<span class="toggle-switch-track" aria-hidden="true">
|
</select>
|
||||||
<span class="toggle-switch-thumb"></span>
|
<button id="btn-madao-refresh-operators" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<span class="data-value">允许 MaDao 复用符合条件的号码</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-madao-price-range" style="display:none;">
|
<div class="data-row" id="row-madao-price-range" style="display:none;">
|
||||||
@@ -1896,7 +1905,9 @@
|
|||||||
<script src="../gopay-utils.js"></script>
|
<script src="../gopay-utils.js"></script>
|
||||||
<script src="../phone-sms/providers/hero-sms.js"></script>
|
<script src="../phone-sms/providers/hero-sms.js"></script>
|
||||||
<script src="../phone-sms/providers/five-sim.js"></script>
|
<script src="../phone-sms/providers/five-sim.js"></script>
|
||||||
|
<script src="../phone-sms/providers/nexsms.js"></script>
|
||||||
<script src="../phone-sms/providers/madao.js"></script>
|
<script src="../phone-sms/providers/madao.js"></script>
|
||||||
|
<script src="../phone-sms/providers/sms-bower.js"></script>
|
||||||
<script src="../phone-sms/providers/registry.js"></script>
|
<script src="../phone-sms/providers/registry.js"></script>
|
||||||
<script src="../icloud-utils.js"></script>
|
<script src="../icloud-utils.js"></script>
|
||||||
<script src="../mail-provider-utils.js"></script>
|
<script src="../mail-provider-utils.js"></script>
|
||||||
|
|||||||
+729
-60
File diff suppressed because it is too large
Load Diff
@@ -149,6 +149,23 @@ let currentState = {
|
|||||||
inbucketMailbox: '',
|
inbucketMailbox: '',
|
||||||
cloudflareDomain: '',
|
cloudflareDomain: '',
|
||||||
cloudflareDomains: [],
|
cloudflareDomains: [],
|
||||||
|
email: 'stale-round@example.com',
|
||||||
|
registrationEmailState: {
|
||||||
|
current: 'stale-round@example.com',
|
||||||
|
previous: 'older-round@example.com',
|
||||||
|
source: 'generated:duck',
|
||||||
|
updatedAt: 123456789,
|
||||||
|
},
|
||||||
|
step8VerificationTargetEmail: 'bound-round@example.com',
|
||||||
|
lastEmailTimestamp: 987654321,
|
||||||
|
lastSignupCode: '111111',
|
||||||
|
lastLoginCode: '222222',
|
||||||
|
bindEmailSubmitted: true,
|
||||||
|
currentPhoneActivation: {
|
||||||
|
activationId: 'runtime-activation',
|
||||||
|
phoneNumber: '+661111111',
|
||||||
|
},
|
||||||
|
phoneNumber: '+661111111',
|
||||||
reusablePhoneActivation: {
|
reusablePhoneActivation: {
|
||||||
activationId: '123456',
|
activationId: '123456',
|
||||||
phoneNumber: '66959916439',
|
phoneNumber: '66959916439',
|
||||||
@@ -280,6 +297,52 @@ async function runAutoSequenceFromStep() {
|
|||||||
throw new Error('fresh auto-run attempt reused stale runtime tab context');
|
throw new Error('fresh auto-run attempt reused stale runtime tab context');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runCalls === 2) {
|
||||||
|
if (state.email !== null) {
|
||||||
|
throw new Error('fresh round must not reuse prior registration email');
|
||||||
|
}
|
||||||
|
if (JSON.stringify(state.registrationEmailState) !== JSON.stringify({
|
||||||
|
current: '',
|
||||||
|
previous: '',
|
||||||
|
source: '',
|
||||||
|
updatedAt: 0,
|
||||||
|
})) {
|
||||||
|
throw new Error('fresh round must clear registration email history');
|
||||||
|
}
|
||||||
|
if (state.step8VerificationTargetEmail !== '') {
|
||||||
|
throw new Error('fresh round must clear bound-email target');
|
||||||
|
}
|
||||||
|
if (state.lastEmailTimestamp !== null) {
|
||||||
|
throw new Error('fresh round must clear email timestamp');
|
||||||
|
}
|
||||||
|
if (state.lastSignupCode !== '') {
|
||||||
|
throw new Error('fresh round must clear signup code cache');
|
||||||
|
}
|
||||||
|
if (state.lastLoginCode !== '') {
|
||||||
|
throw new Error('fresh round must clear login code cache');
|
||||||
|
}
|
||||||
|
if (state.bindEmailSubmitted !== false) {
|
||||||
|
throw new Error('fresh round must clear bind-email marker');
|
||||||
|
}
|
||||||
|
if (state.currentPhoneActivation !== null) {
|
||||||
|
throw new Error('fresh round must clear runtime phone activation');
|
||||||
|
}
|
||||||
|
if (state.phoneNumber !== '') {
|
||||||
|
throw new Error('fresh round must clear runtime phone number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
email: 'fresh-round-' + runCalls + '@example.com',
|
||||||
|
registrationEmailState: {
|
||||||
|
current: 'fresh-round-' + runCalls + '@example.com',
|
||||||
|
previous: 'fresh-round-' + runCalls + '@example.com',
|
||||||
|
source: 'generated:duck',
|
||||||
|
updatedAt: 22334455 + runCalls,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
stepStatuses: {
|
stepStatuses: {
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ const bundle = [
|
|||||||
extractFunction('startAutoRunNodeIdleLogWatchdog'),
|
extractFunction('startAutoRunNodeIdleLogWatchdog'),
|
||||||
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
|
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
|
||||||
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
|
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
|
||||||
|
extractFunction('getDownstreamStateResets'),
|
||||||
extractFunction('getPostStep6AutoRestartDecision'),
|
extractFunction('getPostStep6AutoRestartDecision'),
|
||||||
NODE_COMPAT_HELPERS,
|
NODE_COMPAT_HELPERS,
|
||||||
extractFunction('getAutoRunWorkflowNodeIds'),
|
extractFunction('getAutoRunWorkflowNodeIds'),
|
||||||
@@ -279,6 +280,7 @@ const events = {
|
|||||||
steps: [],
|
steps: [],
|
||||||
logs: [],
|
logs: [],
|
||||||
invalidations: [],
|
invalidations: [],
|
||||||
|
stateUpdates: [],
|
||||||
cancellations: [],
|
cancellations: [],
|
||||||
stopBroadcasts: 0,
|
stopBroadcasts: 0,
|
||||||
};
|
};
|
||||||
@@ -330,6 +332,10 @@ async function getTabId() {
|
|||||||
}
|
}
|
||||||
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
|
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
|
||||||
events.invalidations.push({ step, options });
|
events.invalidations.push({ step, options });
|
||||||
|
const resets = getDownstreamStateResets(step, await getState());
|
||||||
|
if (Object.keys(resets).length > 0) {
|
||||||
|
events.stateUpdates.push(resets);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function cancelPendingCommands(reason = '') {
|
function cancelPendingCommands(reason = '') {
|
||||||
events.cancellations.push(reason);
|
events.cancellations.push(reason);
|
||||||
@@ -576,6 +582,47 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
|
|||||||
assert.ok(events.logs.some(({ message }) => /回到节点 confirm-oauth 重新开始授权流程/.test(message)));
|
assert.ok(events.logs.some(({ message }) => /回到节点 confirm-oauth 重新开始授权流程/.test(message)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auto-run restarts SUB2API expired oauth session from oauth-login and clears stale session state', async () => {
|
||||||
|
const harness = createHarness({
|
||||||
|
failureStep: 10,
|
||||||
|
failureBudget: 1,
|
||||||
|
failureMessage: 'session not found or expired',
|
||||||
|
authState: { state: 'oauth_consent_page', url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' },
|
||||||
|
customState: {
|
||||||
|
panelMode: 'sub2api',
|
||||||
|
stepStatuses: { 3: 'completed' },
|
||||||
|
stepsVersion: 'ultra2.0',
|
||||||
|
visibleStep: 10,
|
||||||
|
sub2apiSessionId: 'expired-session',
|
||||||
|
sub2apiOAuthState: 'expired-state',
|
||||||
|
sub2apiGroupId: 5,
|
||||||
|
sub2apiGroupIds: [5],
|
||||||
|
sub2apiDraftName: 'draft',
|
||||||
|
sub2apiProxyId: 7,
|
||||||
|
accountContributionEnabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = await harness.run();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(events.steps, [7, 8, 9, 10, 7, 8, 9, 10]);
|
||||||
|
assert.equal(events.invalidations.length, 1);
|
||||||
|
assert.deepStrictEqual(events.invalidations[0], {
|
||||||
|
step: 6,
|
||||||
|
options: {
|
||||||
|
logLabel: '节点 platform-verify 报错后准备回到 oauth-login 重试(第 1 次重开)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const resetPatch = events.stateUpdates.find((updates) => updates.sub2apiSessionId === null);
|
||||||
|
assert.ok(resetPatch, 'expected oauth-login restart to clear stale SUB2API OAuth runtime');
|
||||||
|
assert.equal(resetPatch.sub2apiOAuthState, null);
|
||||||
|
assert.equal(resetPatch.sub2apiGroupId, null);
|
||||||
|
assert.deepStrictEqual(resetPatch.sub2apiGroupIds, []);
|
||||||
|
assert.equal(resetPatch.sub2apiDraftName, null);
|
||||||
|
assert.equal(resetPatch.sub2apiProxyId, null);
|
||||||
|
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
|
||||||
|
});
|
||||||
|
|
||||||
test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => {
|
test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => {
|
||||||
const plusGpcSteps = {
|
const plusGpcSteps = {
|
||||||
6: { key: 'plus-checkout-create' },
|
6: { key: 'plus-checkout-create' },
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ test('background account history settings are normalized independently from hotm
|
|||||||
extractFunction('normalizeMaDaoIdentifier'),
|
extractFunction('normalizeMaDaoIdentifier'),
|
||||||
extractFunction('normalizeMaDaoProviderId'),
|
extractFunction('normalizeMaDaoProviderId'),
|
||||||
extractFunction('normalizeMaDaoCountry'),
|
extractFunction('normalizeMaDaoCountry'),
|
||||||
|
extractFunction('normalizeMaDaoOperator'),
|
||||||
extractFunction('normalizeMaDaoPrice'),
|
extractFunction('normalizeMaDaoPrice'),
|
||||||
extractFunction('normalizePhonePreferredActivation'),
|
extractFunction('normalizePhonePreferredActivation'),
|
||||||
extractFunction('normalizePhoneVerificationReplacementLimit'),
|
extractFunction('normalizePhoneVerificationReplacementLimit'),
|
||||||
@@ -127,7 +128,8 @@ const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
|||||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||||
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
|
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
|
||||||
const PHONE_SMS_PROVIDER_MADAO = 'madao';
|
const PHONE_SMS_PROVIDER_MADAO = 'madao';
|
||||||
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
|
const PHONE_SMS_PROVIDER_SMS_BOWER = 'sms-bower';
|
||||||
|
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao', 'sms-bower'];
|
||||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||||
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
||||||
const DEFAULT_MADAO_MODE = 'routing_plan';
|
const DEFAULT_MADAO_MODE = 'routing_plan';
|
||||||
@@ -382,6 +384,7 @@ return {
|
|||||||
assert.equal(api.normalizePersistentSettingValue('madaoProviderId', ' Upstream A! '), 'upstreama');
|
assert.equal(api.normalizePersistentSettingValue('madaoProviderId', ' Upstream A! '), 'upstreama');
|
||||||
assert.equal(api.normalizePersistentSettingValue('madaoCountry', ' gb '), 'GB');
|
assert.equal(api.normalizePersistentSettingValue('madaoCountry', ' gb '), 'GB');
|
||||||
assert.equal(api.normalizePersistentSettingValue('madaoCountry', 'ANY'), 'any');
|
assert.equal(api.normalizePersistentSettingValue('madaoCountry', 'ANY'), 'any');
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('madaoOperator', ' Operator A! '), 'operatora');
|
||||||
assert.equal(api.normalizePersistentSettingValue('madaoAutoPickCountry', 1), true);
|
assert.equal(api.normalizePersistentSettingValue('madaoAutoPickCountry', 1), true);
|
||||||
assert.equal(api.normalizePersistentSettingValue('madaoReusePhone', 0), false);
|
assert.equal(api.normalizePersistentSettingValue('madaoReusePhone', 0), false);
|
||||||
assert.equal(api.normalizePersistentSettingValue('madaoMinPrice', '0.123456'), '0.1235');
|
assert.equal(api.normalizePersistentSettingValue('madaoMinPrice', '0.123456'), '0.1235');
|
||||||
|
|||||||
@@ -2,6 +2,54 @@ const test = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
function extractFunction(source, name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => source.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let i = start; i < source.length; i += 1) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (ch === '(') {
|
||||||
|
parenDepth += 1;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
signatureEnded = true;
|
||||||
|
}
|
||||||
|
} else if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (braceStart < 0) {
|
||||||
|
throw new Error(`missing body for function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < source.length; end += 1) {
|
||||||
|
const ch = source[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => {
|
test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => {
|
||||||
const source = fs.readFileSync('background.js', 'utf8');
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
|
|
||||||
@@ -9,7 +57,17 @@ test('background step-1 state plumbing persists and resets cpa oauth runtime key
|
|||||||
assert.match(source, /cpaManagementOrigin:\s*null/);
|
assert.match(source, /cpaManagementOrigin:\s*null/);
|
||||||
assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/);
|
assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/);
|
||||||
assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/);
|
assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/);
|
||||||
assert.match(source, /if \(step <= 1\) \{[\s\S]*cpaOAuthState:\s*null,[\s\S]*cpaManagementOrigin:\s*null,/);
|
|
||||||
|
const harness = new Function(`
|
||||||
|
function getStepExecutionKeyForState() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
${extractFunction(source, 'getDownstreamStateResets')}
|
||||||
|
return { getDownstreamStateResets };
|
||||||
|
`)();
|
||||||
|
const resets = harness.getDownstreamStateResets(1, {});
|
||||||
|
assert.equal(resets.cpaOAuthState, null);
|
||||||
|
assert.equal(resets.cpaManagementOrigin, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('message router step-1 handler stores cpa oauth runtime keys', async () => {
|
test('message router step-1 handler stores cpa oauth runtime keys', async () => {
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ return {
|
|||||||
madaoRoutingPlanId: 'rp-openai',
|
madaoRoutingPlanId: 'rp-openai',
|
||||||
madaoProviderId: 'upstream-a',
|
madaoProviderId: 'upstream-a',
|
||||||
madaoCountry: 'GB',
|
madaoCountry: 'GB',
|
||||||
|
madaoOperator: 'operator-a',
|
||||||
madaoAutoPickCountry: false,
|
madaoAutoPickCountry: false,
|
||||||
madaoReusePhone: true,
|
madaoReusePhone: true,
|
||||||
madaoMinPrice: '0.12',
|
madaoMinPrice: '0.12',
|
||||||
@@ -452,6 +453,7 @@ return {
|
|||||||
assert.equal(api.getPayloadInput().madaoRoutingPlanId, 'rp-openai');
|
assert.equal(api.getPayloadInput().madaoRoutingPlanId, 'rp-openai');
|
||||||
assert.equal(api.getPayloadInput().madaoProviderId, 'upstream-a');
|
assert.equal(api.getPayloadInput().madaoProviderId, 'upstream-a');
|
||||||
assert.equal(api.getPayloadInput().madaoCountry, 'GB');
|
assert.equal(api.getPayloadInput().madaoCountry, 'GB');
|
||||||
|
assert.equal(api.getPayloadInput().madaoOperator, 'operator-a');
|
||||||
assert.equal(api.getPayloadInput().madaoAutoPickCountry, false);
|
assert.equal(api.getPayloadInput().madaoAutoPickCountry, false);
|
||||||
assert.equal(api.getPayloadInput().madaoReusePhone, true);
|
assert.equal(api.getPayloadInput().madaoReusePhone, true);
|
||||||
assert.equal(api.getPayloadInput().madaoMinPrice, '0.12');
|
assert.equal(api.getPayloadInput().madaoMinPrice, '0.12');
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
madaoRoutingPlanId: '',
|
madaoRoutingPlanId: '',
|
||||||
madaoProviderId: '',
|
madaoProviderId: '',
|
||||||
madaoCountry: '',
|
madaoCountry: '',
|
||||||
|
madaoOperator: '',
|
||||||
madaoAutoPickCountry: true,
|
madaoAutoPickCountry: true,
|
||||||
madaoReusePhone: true,
|
madaoReusePhone: true,
|
||||||
madaoMinPrice: '',
|
madaoMinPrice: '',
|
||||||
@@ -219,6 +220,7 @@ function normalizeMaDaoBaseUrl(value = '') {
|
|||||||
function normalizeMaDaoMode(value = '') { return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : DEFAULT_MADAO_MODE; }
|
function normalizeMaDaoMode(value = '') { return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : DEFAULT_MADAO_MODE; }
|
||||||
function normalizeMaDaoIdentifier(value = '') { return String(value || '').trim(); }
|
function normalizeMaDaoIdentifier(value = '') { return String(value || '').trim(); }
|
||||||
function normalizeMaDaoProviderId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); }
|
function normalizeMaDaoProviderId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); }
|
||||||
|
function normalizeMaDaoOperator(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); }
|
||||||
function normalizeMaDaoCountry(value = '') {
|
function normalizeMaDaoCountry(value = '') {
|
||||||
const trimmed = String(value || '').trim();
|
const trimmed = String(value || '').trim();
|
||||||
if (!trimmed) return '';
|
if (!trimmed) return '';
|
||||||
@@ -306,6 +308,7 @@ test('buildPersistentSettingsPayload writes canonical settings schema into persi
|
|||||||
assert.equal(payload.phoneSmsProvider, 'hero-sms');
|
assert.equal(payload.phoneSmsProvider, 'hero-sms');
|
||||||
assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST);
|
assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST);
|
||||||
assert.equal(payload.madaoMode, DEFAULT_MADAO_MODE_FOR_TEST);
|
assert.equal(payload.madaoMode, DEFAULT_MADAO_MODE_FOR_TEST);
|
||||||
|
assert.equal(payload.madaoOperator, '');
|
||||||
assert.equal(payload.madaoAutoPickCountry, true);
|
assert.equal(payload.madaoAutoPickCountry, true);
|
||||||
assert.equal(payload.madaoReusePhone, true);
|
assert.equal(payload.madaoReusePhone, true);
|
||||||
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
|
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
|
||||||
@@ -724,6 +727,7 @@ test('buildPersistentSettingsPayload persists normalized MaDao flat settings out
|
|||||||
madaoRoutingPlanId: ' rp-openai ',
|
madaoRoutingPlanId: ' rp-openai ',
|
||||||
madaoProviderId: ' Upstream A! ',
|
madaoProviderId: ' Upstream A! ',
|
||||||
madaoCountry: ' gb ',
|
madaoCountry: ' gb ',
|
||||||
|
madaoOperator: ' Operator A! ',
|
||||||
madaoAutoPickCountry: 0,
|
madaoAutoPickCountry: 0,
|
||||||
madaoReusePhone: 1,
|
madaoReusePhone: 1,
|
||||||
madaoMinPrice: '0.123456',
|
madaoMinPrice: '0.123456',
|
||||||
@@ -737,6 +741,7 @@ test('buildPersistentSettingsPayload persists normalized MaDao flat settings out
|
|||||||
assert.equal(payload.madaoRoutingPlanId, 'rp-openai');
|
assert.equal(payload.madaoRoutingPlanId, 'rp-openai');
|
||||||
assert.equal(payload.madaoProviderId, 'upstreama');
|
assert.equal(payload.madaoProviderId, 'upstreama');
|
||||||
assert.equal(payload.madaoCountry, 'GB');
|
assert.equal(payload.madaoCountry, 'GB');
|
||||||
|
assert.equal(payload.madaoOperator, 'operatora');
|
||||||
assert.equal(payload.madaoAutoPickCountry, false);
|
assert.equal(payload.madaoAutoPickCountry, false);
|
||||||
assert.equal(payload.madaoReusePhone, true);
|
assert.equal(payload.madaoReusePhone, true);
|
||||||
assert.equal(payload.madaoMinPrice, '0.1235');
|
assert.equal(payload.madaoMinPrice, '0.1235');
|
||||||
|
|||||||
@@ -137,6 +137,45 @@ test('step 7 retries up to configured limit and then fails', async () => {
|
|||||||
assert.equal(events.completed, 0);
|
assert.equal(events.completed, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step 7 preserves visible step when refreshing OAuth after retry', async () => {
|
||||||
|
const source = fs.readFileSync('flows/openai/background/steps/oauth-login.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||||
|
|
||||||
|
const events = {
|
||||||
|
refreshSteps: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const executor = api.createStep7Executor({
|
||||||
|
addLog: async () => {},
|
||||||
|
completeNodeFromBackground: async () => {},
|
||||||
|
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||||
|
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||||
|
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||||
|
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||||
|
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||||
|
refreshOAuthUrlBeforeStep6: async (state) => {
|
||||||
|
events.refreshSteps.push(state.visibleStep);
|
||||||
|
return `https://oauth.example/${events.refreshSteps.length}`;
|
||||||
|
},
|
||||||
|
reuseOrCreateTab: async () => {},
|
||||||
|
sendToContentScriptResilient: async () => ({
|
||||||
|
step6Outcome: 'recoverable',
|
||||||
|
state: 'email_page',
|
||||||
|
message: '当前仍停留在邮箱页。',
|
||||||
|
}),
|
||||||
|
STEP6_MAX_ATTEMPTS: 3,
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => executor.executeStep7({ email: 'user@example.com', password: 'secret', visibleStep: 9 }),
|
||||||
|
/步骤 9:判断失败后已重试 2 次/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(events.refreshSteps, [9, 9, 9]);
|
||||||
|
});
|
||||||
|
|
||||||
test('step 7 hands add-phone to the dedicated post-login phone node without internal retry', async () => {
|
test('step 7 hands add-phone to the dedicated post-login phone node without internal retry', async () => {
|
||||||
const source = fs.readFileSync('flows/openai/background/steps/oauth-login.js', 'utf8');
|
const source = fs.readFileSync('flows/openai/background/steps/oauth-login.js', 'utf8');
|
||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ test('MaDao direct acquire sends only direct fields and normalizes activation da
|
|||||||
madaoRoutingPlanId: 'route-plan-should-not-be-sent',
|
madaoRoutingPlanId: 'route-plan-should-not-be-sent',
|
||||||
madaoProviderId: 'Upstream A!',
|
madaoProviderId: 'Upstream A!',
|
||||||
madaoCountry: 'gb',
|
madaoCountry: 'gb',
|
||||||
|
madaoOperator: 'Operator A!',
|
||||||
madaoAutoPickCountry: 'false',
|
madaoAutoPickCountry: 'false',
|
||||||
madaoReusePhone: '1',
|
madaoReusePhone: '1',
|
||||||
madaoMinPrice: '0.01',
|
madaoMinPrice: '0.01',
|
||||||
@@ -54,6 +55,9 @@ test('MaDao direct acquire sends only direct fields and normalizes activation da
|
|||||||
auto_pick_country: false,
|
auto_pick_country: false,
|
||||||
reuse_phone: true,
|
reuse_phone: true,
|
||||||
country: 'GB',
|
country: 'GB',
|
||||||
|
metadata: {
|
||||||
|
operator: 'operatora',
|
||||||
|
},
|
||||||
min_price: 0.01,
|
min_price: 0.01,
|
||||||
max_price: 0.2,
|
max_price: 0.2,
|
||||||
});
|
});
|
||||||
@@ -100,6 +104,37 @@ test('MaDao routing acquire sends routing plan only', async () => {
|
|||||||
assert.equal(activation.madaoRoutingItemId, 'route-1');
|
assert.equal(activation.madaoRoutingItemId, 'route-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('MaDao direct acquire treats any operator as default route', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const provider = api.createProvider({
|
||||||
|
fetchImpl: async (_url, options = {}) => {
|
||||||
|
requests.push({ body: JSON.parse(options.body) });
|
||||||
|
return createJsonResponse({
|
||||||
|
ticket_id: 'ticket-any',
|
||||||
|
phone_number: '+66111111111',
|
||||||
|
country: 'TH',
|
||||||
|
provider: 'fivesim',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.acquireActivation({
|
||||||
|
madaoMode: 'direct',
|
||||||
|
madaoProviderId: 'fivesim',
|
||||||
|
madaoCountry: 'TH',
|
||||||
|
madaoOperator: 'Any operator',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(requests.length, 1);
|
||||||
|
assert.deepEqual(requests[0].body, {
|
||||||
|
provider: 'fivesim',
|
||||||
|
service: 'openai',
|
||||||
|
auto_pick_country: true,
|
||||||
|
reuse_phone: true,
|
||||||
|
country: 'TH',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('MaDao poll extracts codes from nested messages and reports pending status', async () => {
|
test('MaDao poll extracts codes from nested messages and reports pending status', async () => {
|
||||||
const statusEvents = [];
|
const statusEvents = [];
|
||||||
const provider = api.createProvider({
|
const provider = api.createProvider({
|
||||||
|
|||||||
@@ -356,6 +356,68 @@ test('phone auth can auto-select country by dial code even when number has no pl
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone auth rejects stale selected country when it does not match phone dial code', async () => {
|
||||||
|
const originalDocument = global.document;
|
||||||
|
const originalEvent = global.Event;
|
||||||
|
const originalLocation = global.location;
|
||||||
|
|
||||||
|
const dom = createFakeAddPhoneDom({
|
||||||
|
options: [
|
||||||
|
{ value: 'TH', textContent: 'Thailand (+66)', buttonText: 'Thailand (+66)' },
|
||||||
|
{ value: 'GB', textContent: 'United Kingdom (+44)', buttonText: 'United Kingdom (+44)' },
|
||||||
|
],
|
||||||
|
selectedIndex: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
global.document = dom.document;
|
||||||
|
global.Event = class Event {
|
||||||
|
constructor(type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
global.location = { href: 'https://auth.openai.com/add-phone' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const helpers = api.createPhoneAuthHelpers({
|
||||||
|
fillInput: (element, value) => {
|
||||||
|
element.value = value;
|
||||||
|
},
|
||||||
|
getActionText: () => '',
|
||||||
|
getPageTextSnapshot: () => '',
|
||||||
|
getVerificationErrorText: () => '',
|
||||||
|
humanPause: async () => {},
|
||||||
|
isActionEnabled: () => true,
|
||||||
|
isAddPhonePageReady: () => true,
|
||||||
|
isConsentReady: () => false,
|
||||||
|
isPhoneVerificationPageReady: () => false,
|
||||||
|
isVisibleElement: () => true,
|
||||||
|
simulateClick: (element) => {
|
||||||
|
element.click?.();
|
||||||
|
},
|
||||||
|
sleep: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
waitForElement: async () => null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => helpers.submitPhoneNumber({
|
||||||
|
countryLabel: 'Country #10',
|
||||||
|
phoneNumber: '+84943328460',
|
||||||
|
}),
|
||||||
|
/Failed to select "Country #10" on the add-phone page\./
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(dom.select.value, 'TH');
|
||||||
|
assert.equal(dom.phoneInput.value, '');
|
||||||
|
assert.equal(dom.hiddenPhoneInput.value, '');
|
||||||
|
assert.equal(dom.wasSubmitClicked(), false);
|
||||||
|
} finally {
|
||||||
|
global.document = originalDocument;
|
||||||
|
global.Event = originalEvent;
|
||||||
|
global.location = originalLocation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of endless Try-again loop', async () => {
|
test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of endless Try-again loop', async () => {
|
||||||
const originalDocument = global.document;
|
const originalDocument = global.document;
|
||||||
const originalLocation = global.location;
|
const originalLocation = global.location;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ const assert = require('node:assert/strict');
|
|||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
||||||
const source = fs.readFileSync('phone-sms/providers/registry.js', 'utf8');
|
const source = fs.readFileSync('phone-sms/providers/registry.js', 'utf8');
|
||||||
|
const maDaoSource = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
|
||||||
|
const nexSmsSource = fs.readFileSync('phone-sms/providers/nexsms.js', 'utf8');
|
||||||
|
|
||||||
function loadRegistry(root = {}) {
|
function loadRegistry(root = {}) {
|
||||||
return new Function('self', `${source}; return self.PhoneSmsProviderRegistry;`)(root);
|
return new Function('self', `${source}; return self.PhoneSmsProviderRegistry;`)(root);
|
||||||
@@ -16,33 +18,42 @@ test('phone sms provider registry normalizes ids, order and labels consistently'
|
|||||||
PhoneSmsFiveSimProvider: {
|
PhoneSmsFiveSimProvider: {
|
||||||
createProvider: (deps = {}) => ({ provider: '5sim', deps }),
|
createProvider: (deps = {}) => ({ provider: '5sim', deps }),
|
||||||
},
|
},
|
||||||
|
PhoneSmsNexSmsProvider: {
|
||||||
|
createProvider: (deps = {}) => ({ provider: 'nexsms', deps }),
|
||||||
|
},
|
||||||
PhoneSmsMaDaoProvider: {
|
PhoneSmsMaDaoProvider: {
|
||||||
createProvider: (deps = {}) => ({ provider: 'madao', deps }),
|
createProvider: (deps = {}) => ({ provider: 'madao', deps }),
|
||||||
},
|
},
|
||||||
|
PhoneSmsBowerProvider: {
|
||||||
|
createProvider: (deps = {}) => ({ provider: 'sms-bower', deps }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepStrictEqual(registry.getProviderIds(), ['hero-sms', '5sim', 'nexsms', 'madao']);
|
assert.deepStrictEqual(registry.getProviderIds(), ['hero-sms', '5sim', 'nexsms', 'madao', 'sms-bower']);
|
||||||
assert.equal(registry.normalizeProviderId(' NEXSMS '), 'nexsms');
|
assert.equal(registry.normalizeProviderId(' NEXSMS '), 'nexsms');
|
||||||
assert.equal(registry.normalizeProviderId(' MaDao '), 'madao');
|
assert.equal(registry.normalizeProviderId(' MaDao '), 'madao');
|
||||||
assert.equal(registry.normalizeProviderId('unknown-provider'), 'hero-sms');
|
assert.equal(registry.normalizeProviderId('unknown-provider'), 'hero-sms');
|
||||||
assert.equal(registry.getProviderLabel('nexsms'), 'NexSMS');
|
assert.equal(registry.getProviderLabel('nexsms'), 'NexSMS');
|
||||||
assert.equal(registry.getProviderLabel('madao'), 'MaDao');
|
assert.equal(registry.getProviderLabel('madao'), 'MaDao');
|
||||||
|
assert.equal(registry.getProviderLabel('sms-bower'), 'SMS Bower');
|
||||||
assert.equal(registry.getProviderDefinition('nexsms').moduleKey, 'PhoneSmsNexSmsProvider');
|
assert.equal(registry.getProviderDefinition('nexsms').moduleKey, 'PhoneSmsNexSmsProvider');
|
||||||
assert.equal(registry.getProviderDefinition('madao').moduleKey, 'PhoneSmsMaDaoProvider');
|
assert.equal(registry.getProviderDefinition('madao').moduleKey, 'PhoneSmsMaDaoProvider');
|
||||||
|
assert.equal(registry.getProviderDefinition('sms-bower').moduleKey, 'PhoneSmsBowerProvider');
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
registry.normalizeProviderOrder([
|
registry.normalizeProviderOrder([
|
||||||
{ provider: 'madao' },
|
{ provider: 'madao' },
|
||||||
|
{ provider: 'sms-bower' },
|
||||||
{ provider: 'nexsms' },
|
{ provider: 'nexsms' },
|
||||||
{ id: '5sim' },
|
{ id: '5sim' },
|
||||||
{ value: 'hero-sms' },
|
{ value: 'hero-sms' },
|
||||||
'MADAO',
|
'MADAO',
|
||||||
'NEXSMS',
|
'NEXSMS',
|
||||||
]),
|
]),
|
||||||
['madao', 'nexsms', '5sim', 'hero-sms']
|
['madao', 'sms-bower', 'nexsms', '5sim', 'hero-sms']
|
||||||
);
|
);
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
registry.normalizeProviderOrder([], ['madao', 'nexsms', '5sim', 'nexsms']),
|
registry.normalizeProviderOrder([], ['madao', 'sms-bower', 'nexsms', '5sim', 'nexsms']),
|
||||||
['madao', 'nexsms', '5sim']
|
['madao', 'sms-bower', 'nexsms', '5sim']
|
||||||
);
|
);
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
registry.createProvider('5sim', { foo: 1 }),
|
registry.createProvider('5sim', { foo: 1 }),
|
||||||
@@ -52,5 +63,47 @@ test('phone sms provider registry normalizes ids, order and labels consistently'
|
|||||||
registry.createProvider('madao', { foo: 2 }),
|
registry.createProvider('madao', { foo: 2 }),
|
||||||
{ provider: 'madao', deps: { foo: 2 } }
|
{ provider: 'madao', deps: { foo: 2 } }
|
||||||
);
|
);
|
||||||
assert.throws(() => registry.createProvider('nexsms'), /接码平台模块未加载:nexsms/);
|
assert.deepStrictEqual(
|
||||||
|
registry.createProvider('nexsms', { foo: 3 }),
|
||||||
|
{ provider: 'nexsms', deps: { foo: 3 } }
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
registry.createProvider('sms-bower', { foo: 4 }),
|
||||||
|
{ provider: 'sms-bower', deps: { foo: 4 } }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone sms provider registry can create the real MaDao provider module', () => {
|
||||||
|
const maDaoModule = new Function('self', `${maDaoSource}; return self.PhoneSmsMaDaoProvider;`)({});
|
||||||
|
const registry = loadRegistry({
|
||||||
|
PhoneSmsMaDaoProvider: maDaoModule,
|
||||||
|
});
|
||||||
|
|
||||||
|
const provider = registry.createProvider('madao', { fetchImpl: 'demo-fetch' });
|
||||||
|
|
||||||
|
assert.equal(provider.id, 'madao');
|
||||||
|
assert.equal(provider.label, 'MaDao');
|
||||||
|
assert.equal(provider.defaultProduct, 'openai');
|
||||||
|
assert.equal(typeof provider.acquireActivation, 'function');
|
||||||
|
assert.equal(typeof provider.pollActivation, 'function');
|
||||||
|
assert.equal(typeof provider.releaseActivation, 'function');
|
||||||
|
assert.equal(provider.mapTicketStatus('waiting_code'), 'waiting_code');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone sms provider registry can create the real NexSMS provider module', () => {
|
||||||
|
const nexSmsModule = new Function('self', `${nexSmsSource}; return self.PhoneSmsNexSmsProvider;`)({});
|
||||||
|
const registry = loadRegistry({
|
||||||
|
PhoneSmsNexSmsProvider: nexSmsModule,
|
||||||
|
});
|
||||||
|
|
||||||
|
const provider = registry.createProvider('nexsms', { fetchImpl: 'demo-fetch' });
|
||||||
|
|
||||||
|
assert.equal(provider.id, 'nexsms');
|
||||||
|
assert.equal(provider.label, 'NexSMS');
|
||||||
|
assert.equal(provider.defaultProduct, 'OpenAI');
|
||||||
|
assert.equal(provider.defaultServiceCode, 'ot');
|
||||||
|
assert.equal(typeof provider.fetchBalance, 'function');
|
||||||
|
assert.equal(typeof provider.fetchPrices, 'function');
|
||||||
|
assert.equal(provider.normalizeCountryId('8'), 8);
|
||||||
|
assert.equal(provider.normalizeServiceCode(' OT '), 'ot');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,10 +3,19 @@ const assert = require('node:assert/strict');
|
|||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
||||||
const source = fs.readFileSync('background/phone-verification-flow.js', 'utf8');
|
const source = fs.readFileSync('background/phone-verification-flow.js', 'utf8');
|
||||||
const globalScope = {};
|
const heroSmsSource = fs.readFileSync('phone-sms/providers/hero-sms.js', 'utf8');
|
||||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
|
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
||||||
|
const nexSmsSource = fs.readFileSync('phone-sms/providers/nexsms.js', 'utf8');
|
||||||
const maDaoSource = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
|
const maDaoSource = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
|
||||||
const maDaoModule = new Function('self', `${maDaoSource}; return self.PhoneSmsMaDaoProvider;`)({});
|
const registrySource = fs.readFileSync('phone-sms/providers/registry.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
new Function('self', `${heroSmsSource}; return self.PhoneSmsHeroSmsProvider;`)(globalScope);
|
||||||
|
new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)(globalScope);
|
||||||
|
new Function('self', `${nexSmsSource}; return self.PhoneSmsNexSmsProvider;`)(globalScope);
|
||||||
|
new Function('self', `${maDaoSource}; return self.PhoneSmsMaDaoProvider;`)(globalScope);
|
||||||
|
new Function('self', `${registrySource}; return self.PhoneSmsProviderRegistry;`)(globalScope);
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
|
||||||
|
const maDaoModule = globalScope.PhoneSmsMaDaoProvider;
|
||||||
|
|
||||||
function buildHeroSmsPricesPayload({ country = '52', service = 'dr', cost = 0.08, count = 25370, physicalCount = 14528 } = {}) {
|
function buildHeroSmsPricesPayload({ country = '52', service = 'dr', cost = 0.08, count = 25370, physicalCount = 14528 } = {}) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
@@ -157,6 +166,110 @@ test('phone verification helper reads latest HeroSMS operator from persistent st
|
|||||||
assert.equal(getNumberRequest.searchParams.get('operator'), 'dtac');
|
assert.equal(getNumberRequest.searchParams.get('operator'), 'dtac');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper creates 5sim adapter through provider registry when available', async () => {
|
||||||
|
const createCalls = [];
|
||||||
|
const root = {
|
||||||
|
PhoneSmsProviderRegistry: {
|
||||||
|
createProvider: (providerId, deps = {}) => {
|
||||||
|
createCalls.push({ providerId, deps });
|
||||||
|
return {
|
||||||
|
requestActivation: async () => ({
|
||||||
|
activationId: '5sim-registry-1',
|
||||||
|
phoneNumber: '+447700900001',
|
||||||
|
provider: '5sim',
|
||||||
|
serviceCode: 'openai',
|
||||||
|
countryId: 'england',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const registryApi = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(root);
|
||||||
|
const helpers = registryApi.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async () => {
|
||||||
|
throw new Error('5sim registry adapter should own network access');
|
||||||
|
},
|
||||||
|
getState: async () => ({ phoneSmsProvider: '5sim' }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation({ phoneSmsProvider: '5sim' });
|
||||||
|
|
||||||
|
assert.equal(createCalls.length, 1);
|
||||||
|
assert.equal(createCalls[0].providerId, '5sim');
|
||||||
|
assert.equal(typeof createCalls[0].deps.fetchImpl, 'function');
|
||||||
|
assert.equal(activation.activationId, '5sim-registry-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper creates MaDao adapter through provider registry when available', async () => {
|
||||||
|
const createCalls = [];
|
||||||
|
const requests = [];
|
||||||
|
const root = {
|
||||||
|
PhoneSmsProviderRegistry: {
|
||||||
|
createProvider: (providerId, deps = {}) => {
|
||||||
|
createCalls.push({ providerId, deps });
|
||||||
|
return maDaoModule.createProvider(deps);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const registryApi = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(root);
|
||||||
|
const currentState = {
|
||||||
|
phoneSmsProvider: 'madao',
|
||||||
|
madaoBaseUrl: 'http://127.0.0.1:7822/',
|
||||||
|
madaoHttpSecret: 'secret-token',
|
||||||
|
madaoMode: 'routing_plan',
|
||||||
|
madaoRoutingPlanId: 'rp-openai',
|
||||||
|
};
|
||||||
|
const helpers = registryApi.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push({ url: parsedUrl, options, body: options.body ? JSON.parse(options.body) : null });
|
||||||
|
if (parsedUrl.pathname === '/api/acquire') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
ticket_id: 'madao-registry-1',
|
||||||
|
phone_number: '+14155550123',
|
||||||
|
service: 'openai',
|
||||||
|
country: 'CA',
|
||||||
|
provider: 'upstream-a',
|
||||||
|
routing_plan_id: 'rp-openai',
|
||||||
|
routing_item_id: 'route-1',
|
||||||
|
status: 'waiting_code',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation(currentState);
|
||||||
|
|
||||||
|
assert.equal(createCalls.length, 1);
|
||||||
|
assert.equal(createCalls[0].providerId, 'madao');
|
||||||
|
assert.equal(typeof createCalls[0].deps.fetchImpl, 'function');
|
||||||
|
assert.equal(activation.activationId, 'madao-registry-1');
|
||||||
|
assert.equal(activation.countryId, 'CA');
|
||||||
|
assert.deepStrictEqual(requests[0].body, {
|
||||||
|
provider: 'auto',
|
||||||
|
service: 'openai',
|
||||||
|
routing_plan_id: 'rp-openai',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('phone verification helper acquires, polls and releases MaDao activation through provider adapter', async () => {
|
test('phone verification helper acquires, polls and releases MaDao activation through provider adapter', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
let currentState = {
|
let currentState = {
|
||||||
@@ -172,7 +285,6 @@ test('phone verification helper acquires, polls and releases MaDao activation th
|
|||||||
};
|
};
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
createMaDaoProvider: maDaoModule.createProvider,
|
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
fetchImpl: async (url, options = {}) => {
|
fetchImpl: async (url, options = {}) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
@@ -252,6 +364,161 @@ test('phone verification helper acquires, polls and releases MaDao activation th
|
|||||||
assert.deepStrictEqual(requests[2].body, { ticket_id: 'madao-1', action: 'finish' });
|
assert.deepStrictEqual(requests[2].body, { ticket_id: 'madao-1', action: 'finish' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper keeps MaDao routing replacement country as ISO code on resubmit', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const messages = [];
|
||||||
|
let currentState = {
|
||||||
|
phoneSmsProvider: 'madao',
|
||||||
|
madaoBaseUrl: 'http://127.0.0.1:7822/',
|
||||||
|
madaoHttpSecret: 'secret-token',
|
||||||
|
madaoMode: 'routing_plan',
|
||||||
|
madaoRoutingPlanId: 'rp-openai',
|
||||||
|
verificationResendCount: 0,
|
||||||
|
phoneVerificationReplacementLimit: 2,
|
||||||
|
phoneCodeWaitSeconds: 15,
|
||||||
|
phoneCodeTimeoutWindows: 1,
|
||||||
|
phoneCodePollIntervalSeconds: 1,
|
||||||
|
phoneCodePollMaxRounds: 1,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
reusablePhoneActivation: null,
|
||||||
|
};
|
||||||
|
let acquireCalls = 0;
|
||||||
|
let replaceCalls = 0;
|
||||||
|
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
const body = options.body ? JSON.parse(options.body) : null;
|
||||||
|
requests.push({ url: parsedUrl, options, body });
|
||||||
|
if (parsedUrl.pathname === '/api/acquire') {
|
||||||
|
acquireCalls += 1;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
ticket_id: 'madao-ticket-routing-1',
|
||||||
|
provider: 'herosms',
|
||||||
|
service: 'openai',
|
||||||
|
country: 'TH',
|
||||||
|
phone_number: '+66950002222',
|
||||||
|
routing_plan_id: 'rp-openai',
|
||||||
|
routing_plan_name: 'OpenAI Plan',
|
||||||
|
routing_item_id: 'route-1',
|
||||||
|
status: 'waiting_code',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsedUrl.pathname === '/api/poll') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
ticket_id: body.ticket_id,
|
||||||
|
provider: body.ticket_id === 'madao-ticket-routing-1' ? 'herosms' : 'smsbower',
|
||||||
|
status: body.ticket_id === 'madao-ticket-routing-1' ? 'waiting_code' : 'code_received',
|
||||||
|
code: body.ticket_id === 'madao-ticket-routing-1' ? null : '654321',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsedUrl.pathname === '/api/routing/replace') {
|
||||||
|
replaceCalls += 1;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
current_ticket_id: 'madao-ticket-routing-1',
|
||||||
|
current_ticket_release: {
|
||||||
|
ticket_id: 'madao-ticket-routing-1',
|
||||||
|
provider: 'herosms',
|
||||||
|
status: 'cancelled',
|
||||||
|
},
|
||||||
|
next_ticket: {
|
||||||
|
ticket_id: 'madao-ticket-routing-2',
|
||||||
|
provider: 'smsbower',
|
||||||
|
service: 'openai',
|
||||||
|
country: 'VN',
|
||||||
|
phone_number: '+84943328460',
|
||||||
|
routing_plan_id: 'rp-openai',
|
||||||
|
routing_plan_name: 'OpenAI Plan',
|
||||||
|
routing_item_id: 'route-2',
|
||||||
|
status: 'waiting_code',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsedUrl.pathname === '/api/release') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({ ok: true }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
messages.push(message);
|
||||||
|
if (message.type === 'STEP8_GET_STATE') {
|
||||||
|
return {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
return {
|
||||||
|
phoneVerificationPage: true,
|
||||||
|
url: 'https://auth.openai.com/phone-verification',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.type === 'RETURN_TO_ADD_PHONE') {
|
||||||
|
return {
|
||||||
|
addPhonePage: true,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
});
|
||||||
|
assert.equal(acquireCalls, 1);
|
||||||
|
assert.equal(replaceCalls, 1);
|
||||||
|
const phoneSubmissions = messages.filter((message) => message.type === 'SUBMIT_PHONE_NUMBER');
|
||||||
|
assert.equal(phoneSubmissions.length, 2);
|
||||||
|
assert.equal(phoneSubmissions[0].payload.phoneNumber, '+66950002222');
|
||||||
|
assert.equal(phoneSubmissions[0].payload.countryId, 'TH');
|
||||||
|
assert.equal(phoneSubmissions[0].payload.countryLabel, 'Thailand');
|
||||||
|
assert.equal(phoneSubmissions[1].payload.phoneNumber, '+84943328460');
|
||||||
|
assert.equal(phoneSubmissions[1].payload.countryId, 'VN');
|
||||||
|
assert.equal(phoneSubmissions[1].payload.countryLabel, 'Vietnam');
|
||||||
|
});
|
||||||
|
|
||||||
test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
|
test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
|
||||||
const setStateCalls = [];
|
const setStateCalls = [];
|
||||||
let currentState = {
|
let currentState = {
|
||||||
@@ -974,8 +1241,6 @@ test('signup phone helper does not let a hung page-state probe stall HeroSMS pol
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('signup phone helper fails stale email-verification on 5sim RECEIVED without code during SMS polling', async () => {
|
test('signup phone helper fails stale email-verification on 5sim RECEIVED without code during SMS polling', async () => {
|
||||||
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
|
||||||
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
|
|
||||||
let checkCount = 0;
|
let checkCount = 0;
|
||||||
let pageStateReads = 0;
|
let pageStateReads = 0;
|
||||||
const contentMessages = [];
|
const contentMessages = [];
|
||||||
@@ -1005,7 +1270,6 @@ test('signup phone helper fails stale email-verification on 5sim RECEIVED withou
|
|||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
createFiveSimProvider: fiveSimModule.createProvider,
|
|
||||||
fetchImpl: async (url) => {
|
fetchImpl: async (url) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
if (parsedUrl.pathname === '/v1/user/check/5001') {
|
if (parsedUrl.pathname === '/v1/user/check/5001') {
|
||||||
@@ -2352,29 +2616,28 @@ test('phone verification helper acquires a number from 5sim with fallback countr
|
|||||||
heroSmsActivationRetryRounds: 1,
|
heroSmsActivationRetryRounds: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepStrictEqual(activation, {
|
assert.equal(activation.activationId, '9876543');
|
||||||
activationId: '9876543',
|
assert.equal(activation.phoneNumber, '+447911123456');
|
||||||
phoneNumber: '+447911123456',
|
assert.equal(activation.provider, '5sim');
|
||||||
provider: '5sim',
|
assert.equal(activation.serviceCode, 'openai');
|
||||||
serviceCode: 'openai',
|
assert.equal(activation.countryId, 'england');
|
||||||
countryId: 'england',
|
assert.equal(activation.countryCode, 'england');
|
||||||
countryCode: 'england',
|
assert.equal(activation.successfulUses, 0);
|
||||||
countryLabel: 'England',
|
assert.equal(activation.maxUses, 3);
|
||||||
successfulUses: 0,
|
assert.equal(requests.length, 6);
|
||||||
maxUses: 3,
|
assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
|
||||||
});
|
assert.equal(requests[1].pathname, '/v1/guest/prices');
|
||||||
assert.equal(requests.length, 4);
|
assert.equal(requests[1].search.get('country'), 'thailand');
|
||||||
assert.equal(requests[0].pathname, '/v1/guest/prices');
|
assert.equal(requests[1].search.get('product'), 'openai');
|
||||||
assert.equal(requests[0].search.get('country'), 'thailand');
|
assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||||
assert.equal(requests[0].search.get('product'), 'openai');
|
assert.equal(requests[2].search.get('maxPrice'), '0.08');
|
||||||
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
assert.equal(requests[2].search.get('reuse'), '1');
|
||||||
assert.equal(requests[1].search.get('maxPrice'), '0.08');
|
assert.equal(requests[2].headers.Authorization, 'Bearer five-token');
|
||||||
assert.equal(requests[1].search.get('reuse'), '1');
|
assert.equal(requests[3].pathname, '/v1/guest/products/england/any');
|
||||||
assert.equal(requests[1].headers.Authorization, 'Bearer five-token');
|
assert.equal(requests[4].pathname, '/v1/guest/prices');
|
||||||
assert.equal(requests[2].pathname, '/v1/guest/prices');
|
assert.equal(requests[4].search.get('country'), 'england');
|
||||||
assert.equal(requests[2].search.get('country'), 'england');
|
assert.equal(requests[4].search.get('product'), 'openai');
|
||||||
assert.equal(requests[2].search.get('product'), 'openai');
|
assert.equal(requests[5].pathname, '/v1/user/buy/activation/england/any/openai');
|
||||||
assert.equal(requests[3].pathname, '/v1/user/buy/activation/england/any/openai');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper preserves fallback provider while refreshing latest settings', async () => {
|
test('phone verification helper preserves fallback provider while refreshing latest settings', async () => {
|
||||||
@@ -2514,13 +2777,14 @@ test('phone verification helper preserves fallback provider while refreshing lat
|
|||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
fiveSimRequests.map((entry) => entry.url.pathname),
|
fiveSimRequests.map((entry) => entry.url.pathname),
|
||||||
[
|
[
|
||||||
|
'/v1/guest/products/vietnam/any',
|
||||||
'/v1/guest/prices',
|
'/v1/guest/prices',
|
||||||
'/v1/user/buy/activation/vietnam/any/openai',
|
'/v1/user/buy/activation/vietnam/any/openai',
|
||||||
'/v1/user/check/5020',
|
'/v1/user/check/5020',
|
||||||
'/v1/user/finish/5020',
|
'/v1/user/finish/5020',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert.equal(fiveSimRequests[1].options.headers.Authorization, 'Bearer five-token');
|
assert.equal(fiveSimRequests[2].options.headers.Authorization, 'Bearer five-token');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSmsReuseEnabled for 5sim acquisition', async () => {
|
test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSmsReuseEnabled for 5sim acquisition', async () => {
|
||||||
@@ -2593,20 +2857,18 @@ test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSms
|
|||||||
heroSmsActivationRetryRounds: 1,
|
heroSmsActivationRetryRounds: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepStrictEqual(activation, {
|
assert.equal(activation.activationId, '1234567');
|
||||||
activationId: '1234567',
|
assert.equal(activation.phoneNumber, '+66880000000');
|
||||||
phoneNumber: '+66880000000',
|
assert.equal(activation.provider, '5sim');
|
||||||
provider: '5sim',
|
assert.equal(activation.serviceCode, 'openai');
|
||||||
serviceCode: 'openai',
|
assert.equal(activation.countryId, 'thailand');
|
||||||
countryId: 'thailand',
|
assert.equal(activation.countryCode, 'thailand');
|
||||||
countryCode: 'thailand',
|
assert.equal(activation.successfulUses, 0);
|
||||||
countryLabel: 'Thailand',
|
assert.equal(activation.maxUses, 3);
|
||||||
successfulUses: 0,
|
assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
|
||||||
maxUses: 3,
|
assert.equal(requests[1].pathname, '/v1/guest/prices');
|
||||||
});
|
assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||||
assert.equal(requests[0].pathname, '/v1/guest/prices');
|
assert.equal(requests[2].search.get('reuse'), null);
|
||||||
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
|
||||||
assert.equal(requests[1].search.get('reuse'), null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper treats fiveSimReuseEnabled as legacy-only when phoneSmsReuseEnabled is absent', async () => {
|
test('phone verification helper treats fiveSimReuseEnabled as legacy-only when phoneSmsReuseEnabled is absent', async () => {
|
||||||
@@ -2679,20 +2941,18 @@ test('phone verification helper treats fiveSimReuseEnabled as legacy-only when p
|
|||||||
heroSmsActivationRetryRounds: 1,
|
heroSmsActivationRetryRounds: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepStrictEqual(activation, {
|
assert.equal(activation.activationId, '1234568');
|
||||||
activationId: '1234568',
|
assert.equal(activation.phoneNumber, '+66880000001');
|
||||||
phoneNumber: '+66880000001',
|
assert.equal(activation.provider, '5sim');
|
||||||
provider: '5sim',
|
assert.equal(activation.serviceCode, 'openai');
|
||||||
serviceCode: 'openai',
|
assert.equal(activation.countryId, 'thailand');
|
||||||
countryId: 'thailand',
|
assert.equal(activation.countryCode, 'thailand');
|
||||||
countryCode: 'thailand',
|
assert.equal(activation.successfulUses, 0);
|
||||||
countryLabel: 'Thailand',
|
assert.equal(activation.maxUses, 3);
|
||||||
successfulUses: 0,
|
assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
|
||||||
maxUses: 3,
|
assert.equal(requests[1].pathname, '/v1/guest/prices');
|
||||||
});
|
assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||||
assert.equal(requests[0].pathname, '/v1/guest/prices');
|
assert.equal(requests[2].search.get('reuse'), '1');
|
||||||
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
|
||||||
assert.equal(requests[1].search.get('reuse'), '1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper rejects 5sim maxPrice with custom operator before buying', async () => {
|
test('phone verification helper rejects 5sim maxPrice with custom operator before buying', async () => {
|
||||||
@@ -3115,7 +3375,7 @@ test('phone verification helper polls and parses 5sim verification codes', async
|
|||||||
|
|
||||||
assert.equal(code, '246810');
|
assert.equal(code, '246810');
|
||||||
assert.equal(checkCount, 2);
|
assert.equal(checkCount, 2);
|
||||||
assert.deepStrictEqual(statusUpdates, ['PENDING']);
|
assert.deepStrictEqual(statusUpdates, ['PENDING', 'RECEIVED']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper treats HeroSMS STATUS_WAIT_RETRY payload status as pending', async () => {
|
test('phone verification helper treats HeroSMS STATUS_WAIT_RETRY payload status as pending', async () => {
|
||||||
@@ -3242,18 +3502,16 @@ test('phone verification helper reuses 5sim by keeping the original activation',
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(nextActivation, {
|
assert.equal(nextActivation.activationId, '600001');
|
||||||
activationId: '600001',
|
assert.equal(nextActivation.phoneNumber, '+44 7911-123-456');
|
||||||
phoneNumber: '+44 7911-123-456',
|
assert.equal(nextActivation.provider, '5sim');
|
||||||
provider: '5sim',
|
assert.equal(nextActivation.serviceCode, 'openai');
|
||||||
serviceCode: 'openai',
|
assert.equal(nextActivation.countryId, 'england');
|
||||||
countryId: 'england',
|
assert.equal(nextActivation.countryCode, 'england');
|
||||||
countryCode: 'england',
|
assert.equal(nextActivation.successfulUses, 0);
|
||||||
successfulUses: 0,
|
assert.equal(nextActivation.maxUses, 1);
|
||||||
maxUses: 1,
|
assert.equal(nextActivation.source, '5sim-retained-reuse');
|
||||||
source: '5sim-retained-reuse',
|
assert.deepStrictEqual(nextActivation.ignoredPhoneCodeKeys, ['old::111111']);
|
||||||
ignoredPhoneCodeKeys: ['old::111111'],
|
|
||||||
});
|
|
||||||
assert.deepStrictEqual(requests, ['/v1/user/check/600001']);
|
assert.deepStrictEqual(requests, ['/v1/user/check/600001']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6448,12 +6706,9 @@ test('phone verification helper auto free-reuses 5sim by polling the retained or
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
|
||||||
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
|
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
createFiveSimProvider: fiveSimModule.createProvider,
|
|
||||||
fetchImpl: async (url) => {
|
fetchImpl: async (url) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
requests.push(parsedUrl);
|
requests.push(parsedUrl);
|
||||||
@@ -6547,12 +6802,9 @@ test('phone verification helper retires failed 5sim free-reuse record instead of
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
|
||||||
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
|
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
createFiveSimProvider: fiveSimModule.createProvider,
|
|
||||||
fetchImpl: async (url) => {
|
fetchImpl: async (url) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
requests.push(parsedUrl);
|
requests.push(parsedUrl);
|
||||||
@@ -8724,12 +8976,9 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
|
|||||||
reusablePhoneActivation: null,
|
reusablePhoneActivation: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
|
||||||
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
|
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
createFiveSimProvider: fiveSimModule.createProvider,
|
|
||||||
fetchImpl: async (url, options = {}) => {
|
fetchImpl: async (url, options = {}) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
requests.push({ url: parsedUrl, options });
|
requests.push({ url: parsedUrl, options });
|
||||||
@@ -8838,7 +9087,6 @@ test('phone verification helper uses MaDao routing replace when add-phone reject
|
|||||||
};
|
};
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
createMaDaoProvider: maDaoModule.createProvider,
|
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
fetchImpl: async (url, options = {}) => {
|
fetchImpl: async (url, options = {}) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
@@ -8966,6 +9214,7 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
|
|||||||
madaoRoutingPlanId: 'rp-stale',
|
madaoRoutingPlanId: 'rp-stale',
|
||||||
madaoProviderId: 'upstream-a',
|
madaoProviderId: 'upstream-a',
|
||||||
madaoCountry: 'gb',
|
madaoCountry: 'gb',
|
||||||
|
madaoOperator: 'operator-a',
|
||||||
madaoAutoPickCountry: false,
|
madaoAutoPickCountry: false,
|
||||||
madaoReusePhone: true,
|
madaoReusePhone: true,
|
||||||
madaoMinPrice: '0.01',
|
madaoMinPrice: '0.01',
|
||||||
@@ -8981,7 +9230,6 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
|
|||||||
};
|
};
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
createMaDaoProvider: maDaoModule.createProvider,
|
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
fetchImpl: async (url, options = {}) => {
|
fetchImpl: async (url, options = {}) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
@@ -9076,6 +9324,9 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
|
|||||||
auto_pick_country: false,
|
auto_pick_country: false,
|
||||||
reuse_phone: true,
|
reuse_phone: true,
|
||||||
country: 'GB',
|
country: 'GB',
|
||||||
|
metadata: {
|
||||||
|
operator: 'operator-a',
|
||||||
|
},
|
||||||
min_price: 0.01,
|
min_price: 0.01,
|
||||||
max_price: 0.2,
|
max_price: 0.2,
|
||||||
});
|
});
|
||||||
@@ -9114,12 +9365,9 @@ test('phone verification helper keeps 5sim reusable activation on the original o
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
|
||||||
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
|
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
createFiveSimProvider: fiveSimModule.createProvider,
|
|
||||||
fetchImpl: async (url) => {
|
fetchImpl: async (url) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
requests.push(parsedUrl);
|
requests.push(parsedUrl);
|
||||||
|
|||||||
@@ -1058,7 +1058,10 @@ test('GPC billing restarts when start button returns without subscription done',
|
|||||||
const { events, executor, pageHarness } = createGpcPageExecutorHarness([
|
const { events, executor, pageHarness } = createGpcPageExecutorHarness([
|
||||||
{ startButtonText: '开始 Plus 充值', logText: 'SYSTEM 页面已就绪' },
|
{ startButtonText: '开始 Plus 充值', logText: 'SYSTEM 页面已就绪' },
|
||||||
{ startButtonText: '任务进行中', logText: '处理中' },
|
{ startButtonText: '任务进行中', logText: '处理中' },
|
||||||
{ startButtonText: '开始 Plus 充值', logText: 'SYSTEM\n卡密次数不足,任务已停止' },
|
{
|
||||||
|
startButtonText: '开始 Plus 充值',
|
||||||
|
logText: '[02:17:28] ACTION 任务已提交 [02:17:28] SYSTEM 排队中,等待 worker 调度... [02:17:31] SUCCESS [02/09] 购买短信号码:+6283******846 [02:17:59] ERROR 卡密次数不足,任务已停止',
|
||||||
|
},
|
||||||
{ startButtonText: '任务进行中', logText: '第二次处理中' },
|
{ startButtonText: '任务进行中', logText: '第二次处理中' },
|
||||||
{ startButtonText: '开始 Plus 充值', logText: '订阅完成', hasSubscriptionDone: true },
|
{ startButtonText: '开始 Plus 充值', logText: '订阅完成', hasSubscriptionDone: true },
|
||||||
]);
|
]);
|
||||||
@@ -1071,7 +1074,8 @@ test('GPC billing restarts when start button returns without subscription done',
|
|||||||
|
|
||||||
assert.equal(pageHarness.clicks.length, 2);
|
assert.equal(pageHarness.clicks.length, 2);
|
||||||
assert.equal(events.logs.some((entry) => /准备再次启动/.test(entry.message)), true);
|
assert.equal(events.logs.some((entry) => /准备再次启动/.test(entry.message)), true);
|
||||||
assert.equal(events.logs.some((entry) => /准备再次启动.*最近日志:卡密次数不足,任务已停止/.test(entry.message)), true);
|
assert.equal(events.logs.some((entry) => /准备再次启动.*最近日志:\[02:17:59\] ERROR 卡密次数不足,任务已停止/.test(entry.message)), true);
|
||||||
|
assert.equal(events.logs.some((entry) => /最近日志:\[02:17:28\] ACTION/.test(entry.message)), false);
|
||||||
assert.equal(events.completed.length, 1);
|
assert.equal(events.completed.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1099,7 +1103,7 @@ test('GPC billing collapses repeated running status logs with a multiplier', asy
|
|||||||
|
|
||||||
test('GPC billing fails current round without restart when account has no trial eligibility', async () => {
|
test('GPC billing fails current round without restart when account has no trial eligibility', async () => {
|
||||||
const { events, executor, pageHarness } = createGpcPageExecutorHarness([
|
const { events, executor, pageHarness } = createGpcPageExecutorHarness([
|
||||||
{ startButtonText: '开始 Plus 充值', logText: 'SYSTEM\n该账户没有试用资格', noTrial: true },
|
{ startButtonText: '开始 Plus 充值', logText: '[02:20:00] ACTION 任务开始执行... [02:20:09] ERROR 该账户没有试用资格', noTrial: true },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
@@ -1108,13 +1112,38 @@ test('GPC billing fails current round without restart when account has no trial
|
|||||||
plusCheckoutSource: 'gpc-helper',
|
plusCheckoutSource: 'gpc-helper',
|
||||||
plusCheckoutTabId: 77,
|
plusCheckoutTabId: 77,
|
||||||
}),
|
}),
|
||||||
/PLUS_CHECKOUT_NON_FREE_TRIAL::.*该账户没有试用资格.*最近日志:该账户没有试用资格/
|
/PLUS_CHECKOUT_NON_FREE_TRIAL::.*该账户没有试用资格.*最近日志:\[02:20:09\] ERROR 该账户没有试用资格/
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(pageHarness.clicks.length, 0);
|
assert.equal(pageHarness.clicks.length, 0);
|
||||||
assert.equal(events.completed.length, 0);
|
assert.equal(events.completed.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('GPC billing treats no-trial log text as terminal even when page flag is missing', async () => {
|
||||||
|
const { events, executor, pageHarness } = createGpcPageExecutorHarness([
|
||||||
|
{ startButtonText: '开始 Plus 充值', logText: 'SYSTEM 页面已就绪' },
|
||||||
|
{ startButtonText: '任务进行中', logText: '处理中' },
|
||||||
|
{
|
||||||
|
startButtonText: '开始 Plus 充值',
|
||||||
|
logText: '[02:30:00] ACTION 任务开始执行... [02:30:18] ERROR 该账号没有试用资格',
|
||||||
|
noTrial: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => executor.executePlusCheckoutBilling({
|
||||||
|
plusPaymentMethod: 'gpc-helper',
|
||||||
|
plusCheckoutSource: 'gpc-helper',
|
||||||
|
plusCheckoutTabId: 77,
|
||||||
|
}),
|
||||||
|
/PLUS_CHECKOUT_NON_FREE_TRIAL::.*该账户没有试用资格.*最近日志:\[02:30:18\] ERROR 该账号没有试用资格/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(pageHarness.clicks.length, 1);
|
||||||
|
assert.equal(events.logs.some((entry) => /准备再次启动/.test(entry.message)), false);
|
||||||
|
assert.equal(events.completed.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('GPC billing times out when page never finishes', async () => {
|
test('GPC billing times out when page never finishes', async () => {
|
||||||
const { executor, pageHarness } = createGpcPageExecutorHarness([
|
const { executor, pageHarness } = createGpcPageExecutorHarness([
|
||||||
{ startButtonText: '任务进行中', logText: '处理中' },
|
{ startButtonText: '任务进行中', logText: '处理中' },
|
||||||
|
|||||||
@@ -156,15 +156,28 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
|
|||||||
assert.match(html, /id="row-madao-mode"/);
|
assert.match(html, /id="row-madao-mode"/);
|
||||||
assert.match(html, /id="select-madao-mode"/);
|
assert.match(html, /id="select-madao-mode"/);
|
||||||
assert.match(html, /id="row-madao-routing-plan-id"/);
|
assert.match(html, /id="row-madao-routing-plan-id"/);
|
||||||
assert.match(html, /id="input-madao-routing-plan-id"/);
|
assert.match(html, /id="select-madao-routing-plan-id"/);
|
||||||
|
assert.match(html, /id="btn-madao-refresh-routing-plans"/);
|
||||||
|
assert.doesNotMatch(html, /id="input-madao-routing-plan-id"/);
|
||||||
assert.match(html, /id="row-madao-provider-id"/);
|
assert.match(html, /id="row-madao-provider-id"/);
|
||||||
assert.match(html, /id="input-madao-provider-id"/);
|
assert.match(html, /id="select-madao-provider-id"/);
|
||||||
|
assert.match(html, /id="btn-madao-refresh-providers"/);
|
||||||
|
assert.doesNotMatch(html, /id="input-madao-provider-id"/);
|
||||||
assert.match(html, /id="row-madao-country"/);
|
assert.match(html, /id="row-madao-country"/);
|
||||||
assert.match(html, /id="input-madao-country"/);
|
assert.match(html, /id="select-madao-country"/);
|
||||||
assert.match(html, /id="row-madao-auto-pick-country"/);
|
assert.match(html, /id="btn-madao-refresh-countries"/);
|
||||||
assert.match(html, /id="input-madao-auto-pick-country"/);
|
assert.doesNotMatch(html, /id="input-madao-country"/);
|
||||||
assert.match(html, /id="row-madao-reuse-phone"/);
|
assert.match(html, /id="row-madao-operator"/);
|
||||||
assert.match(html, /id="input-madao-reuse-phone"/);
|
assert.match(html, /id="select-madao-operator"/);
|
||||||
|
assert.match(html, /id="btn-madao-refresh-operators"/);
|
||||||
|
assert.doesNotMatch(html, /id="row-madao-auto-pick-country"/);
|
||||||
|
assert.doesNotMatch(html, /id="input-madao-auto-pick-country"/);
|
||||||
|
assert.doesNotMatch(html, /id="row-madao-reuse-phone"/);
|
||||||
|
assert.doesNotMatch(html, /id="input-madao-reuse-phone"/);
|
||||||
|
assert.doesNotMatch(html, /直连平台/);
|
||||||
|
assert.doesNotMatch(html, /直连国家/);
|
||||||
|
assert.doesNotMatch(html, /自动选国家/);
|
||||||
|
assert.doesNotMatch(html, /MaDao 复用/);
|
||||||
assert.match(html, /id="row-madao-price-range"/);
|
assert.match(html, /id="row-madao-price-range"/);
|
||||||
assert.match(html, /id="input-madao-min-price"/);
|
assert.match(html, /id="input-madao-min-price"/);
|
||||||
assert.match(html, /id="input-madao-max-price"/);
|
assert.match(html, /id="input-madao-max-price"/);
|
||||||
@@ -190,6 +203,228 @@ test('sidepanel loads live SMS country lists silently during startup', () => {
|
|||||||
assert.doesNotMatch(sidepanelSource, /console\.error\('加载 (?:HeroSMS|5sim|NexSMS) 国家列表失败:'/);
|
assert.doesNotMatch(sidepanelSource, /console\.error\('加载 (?:HeroSMS|5sim|NexSMS) 国家列表失败:'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('MaDao routing plan select loads options from helper API and preserves saved plan', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const fetchImpl = async (url, options = {}) => {
|
||||||
|
requests.push({ url, options });
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
async text() {
|
||||||
|
return JSON.stringify({
|
||||||
|
plans: [
|
||||||
|
{ id: 'openai-plan', name: 'OpenAI Plan', service: 'openai', enabled: true },
|
||||||
|
{ id: 'kiro-plan', name: 'Kiro Plan', service: 'kiro', enabled: true },
|
||||||
|
{ id: 'disabled-plan', name: 'Disabled Plan', service: 'openai', enabled: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = new Function('fetch', `
|
||||||
|
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
||||||
|
let latestState = {
|
||||||
|
madaoBaseUrl: 'http://madao.local/api/acquire',
|
||||||
|
madaoHttpSecret: 'madao-secret',
|
||||||
|
madaoRoutingPlanId: 'stored-plan',
|
||||||
|
};
|
||||||
|
let maDaoRoutingPlanOptions = [];
|
||||||
|
let displayText = '';
|
||||||
|
let toastText = '';
|
||||||
|
const inputMaDaoBaseUrl = { value: 'http://madao.local/api/acquire' };
|
||||||
|
const inputMaDaoHttpSecret = { value: 'madao-secret' };
|
||||||
|
const selectMaDaoRoutingPlanId = {
|
||||||
|
value: 'stored-plan',
|
||||||
|
options: [],
|
||||||
|
replaceChildren(...children) {
|
||||||
|
this.options = children;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
function updateHeroSmsPlatformDisplay() {
|
||||||
|
displayText = getSelectedMaDaoRoutingPlanLabel();
|
||||||
|
}
|
||||||
|
function showToast(message) {
|
||||||
|
toastText = message;
|
||||||
|
}
|
||||||
|
${extractFunction('normalizeMaDaoBaseUrlValue')}
|
||||||
|
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||||
|
${extractFunction('normalizeMaDaoRoutingPlanIdValue')}
|
||||||
|
${extractFunction('createSelectOptionElement')}
|
||||||
|
${extractFunction('setSelectOptions')}
|
||||||
|
${extractFunction('buildMaDaoRoutingPlanOptions')}
|
||||||
|
${extractFunction('setMaDaoRoutingPlanSelectOptions')}
|
||||||
|
${extractFunction('buildMaDaoRequestUrl')}
|
||||||
|
${extractFunction('buildMaDaoRequestHeaders')}
|
||||||
|
${extractFunction('fetchMaDaoJson')}
|
||||||
|
${extractFunction('getMaDaoRoutingPlansFromPayload')}
|
||||||
|
${extractFunction('loadMaDaoRoutingPlans')}
|
||||||
|
${extractFunction('getSelectedMaDaoRoutingPlanLabel')}
|
||||||
|
return {
|
||||||
|
loadMaDaoRoutingPlans,
|
||||||
|
selectMaDaoRoutingPlanId,
|
||||||
|
get options() { return selectMaDaoRoutingPlanId.options.map((option) => ({ value: option.value, label: option.textContent, selected: option.selected })); },
|
||||||
|
get displayText() { return displayText; },
|
||||||
|
get toastText() { return toastText; },
|
||||||
|
};
|
||||||
|
`)(fetchImpl);
|
||||||
|
|
||||||
|
const plans = await api.loadMaDaoRoutingPlans();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(plans.map((plan) => plan.value), ['openai-plan']);
|
||||||
|
assert.equal(api.selectMaDaoRoutingPlanId.value, 'stored-plan');
|
||||||
|
assert.deepStrictEqual(api.options.map((option) => option.value), ['', 'stored-plan', 'openai-plan']);
|
||||||
|
assert.equal(api.options.find((option) => option.value === 'stored-plan').selected, true);
|
||||||
|
assert.equal(api.displayText, 'stored-plan');
|
||||||
|
assert.equal(api.toastText, '已刷新 MaDao 路由计划。');
|
||||||
|
assert.equal(requests[0].url, 'http://madao.local/api/routing-plans');
|
||||||
|
assert.equal(requests[0].options.headers.Authorization, 'Bearer madao-secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MaDao direct selects load provider country and operator options from daemon API', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const fetchImpl = async (url, options = {}) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push({
|
||||||
|
pathname: parsedUrl.pathname,
|
||||||
|
options,
|
||||||
|
body: options.body ? JSON.parse(options.body) : null,
|
||||||
|
});
|
||||||
|
if (parsedUrl.pathname === '/api/providers') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
async text() {
|
||||||
|
return JSON.stringify({
|
||||||
|
providers: [
|
||||||
|
{ id: 'stored-provider', name: 'Stored Provider', enabled: true, protocol_label: 'REST' },
|
||||||
|
{ id: 'disabled-provider', name: 'Disabled Provider', enabled: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsedUrl.pathname === '/api/providers/stored-provider/countries') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
async text() {
|
||||||
|
return JSON.stringify({
|
||||||
|
provider: 'stored-provider',
|
||||||
|
items: [
|
||||||
|
{ value: 'GB', label: 'United Kingdom', label_zh: '英国', provider_value: 'england' },
|
||||||
|
{ value: 'TH', label: 'Thailand', label_zh: '泰国', provider_value: '52' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsedUrl.pathname === '/api/providers/stored-provider/operators') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
async text() {
|
||||||
|
return JSON.stringify({
|
||||||
|
provider: 'stored-provider',
|
||||||
|
items: [
|
||||||
|
{ value: 'any', label: 'Any operator' },
|
||||||
|
{ value: 'operator-a', label: 'Operator A' },
|
||||||
|
{ value: 'operator-b', label: 'Operator B' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = new Function('fetch', `
|
||||||
|
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
||||||
|
let latestState = {
|
||||||
|
madaoBaseUrl: 'http://madao.local/api/acquire',
|
||||||
|
madaoHttpSecret: 'madao-secret',
|
||||||
|
madaoProviderId: 'stored-provider',
|
||||||
|
madaoCountry: 'england',
|
||||||
|
madaoOperator: 'operator-a',
|
||||||
|
};
|
||||||
|
let maDaoProviderOptions = [];
|
||||||
|
let maDaoCountryOptions = [];
|
||||||
|
let maDaoOperatorOptions = [];
|
||||||
|
let displayText = '';
|
||||||
|
let toastText = '';
|
||||||
|
const inputMaDaoBaseUrl = { value: 'http://madao.local/api/acquire' };
|
||||||
|
const inputMaDaoHttpSecret = { value: 'madao-secret' };
|
||||||
|
const selectMaDaoProviderId = { value: 'stored-provider', options: [], replaceChildren(...children) { this.options = children; } };
|
||||||
|
const selectMaDaoCountry = { value: 'england', options: [], replaceChildren(...children) { this.options = children; } };
|
||||||
|
const selectMaDaoOperator = { value: 'operator-a', options: [], replaceChildren(...children) { this.options = children; } };
|
||||||
|
function updateHeroSmsPlatformDisplay() {
|
||||||
|
displayText = [selectMaDaoProviderId.value, selectMaDaoCountry.value, selectMaDaoOperator.value].filter(Boolean).join('/');
|
||||||
|
}
|
||||||
|
function showToast(message) {
|
||||||
|
toastText = message;
|
||||||
|
}
|
||||||
|
${extractFunction('normalizeMaDaoBaseUrlValue')}
|
||||||
|
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||||
|
${extractFunction('normalizeMaDaoProviderIdValue')}
|
||||||
|
${extractFunction('normalizeMaDaoOperatorValue')}
|
||||||
|
${extractFunction('normalizeMaDaoCountry')}
|
||||||
|
${extractFunction('formatMaDaoCountryDisplayLabel')}
|
||||||
|
${extractFunction('createSelectOptionElement')}
|
||||||
|
${extractFunction('setSelectOptions')}
|
||||||
|
${extractFunction('normalizeMaDaoOptionListItems')}
|
||||||
|
${extractFunction('resolveMaDaoOptionSelectedValue')}
|
||||||
|
${extractFunction('setMaDaoProviderSelectOptions')}
|
||||||
|
${extractFunction('setMaDaoCountrySelectOptions')}
|
||||||
|
${extractFunction('setMaDaoOperatorSelectOptions')}
|
||||||
|
${extractFunction('buildMaDaoRequestUrl')}
|
||||||
|
${extractFunction('buildMaDaoRequestHeaders')}
|
||||||
|
${extractFunction('fetchMaDaoJson')}
|
||||||
|
${extractFunction('getMaDaoProvidersFromPayload')}
|
||||||
|
${extractFunction('getMaDaoOptionItemsFromPayload')}
|
||||||
|
${extractFunction('getSelectedMaDaoProviderId')}
|
||||||
|
${extractFunction('getSelectedMaDaoCountry')}
|
||||||
|
${extractFunction('loadMaDaoProviders')}
|
||||||
|
${extractFunction('loadMaDaoCountries')}
|
||||||
|
${extractFunction('loadMaDaoOperators')}
|
||||||
|
return {
|
||||||
|
loadMaDaoProviders,
|
||||||
|
selectMaDaoProviderId,
|
||||||
|
selectMaDaoCountry,
|
||||||
|
selectMaDaoOperator,
|
||||||
|
get providerOptions() { return selectMaDaoProviderId.options.map((option) => ({ value: option.value, label: option.textContent, selected: option.selected })); },
|
||||||
|
get countryOptions() { return selectMaDaoCountry.options.map((option) => ({ value: option.value, label: option.textContent, selected: option.selected })); },
|
||||||
|
get operatorOptions() { return selectMaDaoOperator.options.map((option) => ({ value: option.value, label: option.textContent, selected: option.selected })); },
|
||||||
|
get displayText() { return displayText; },
|
||||||
|
get toastText() { return toastText; },
|
||||||
|
};
|
||||||
|
`)(fetchImpl);
|
||||||
|
|
||||||
|
const providers = await api.loadMaDaoProviders();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(providers.map((provider) => provider.value), ['stored-provider']);
|
||||||
|
assert.equal(api.selectMaDaoProviderId.value, 'stored-provider');
|
||||||
|
assert.equal(api.selectMaDaoCountry.value, 'GB');
|
||||||
|
assert.equal(api.selectMaDaoOperator.value, 'operator-a');
|
||||||
|
assert.deepStrictEqual(api.providerOptions.map((option) => option.value), ['', 'stored-provider']);
|
||||||
|
assert.deepStrictEqual(api.countryOptions.map((option) => option.value), ['', 'GB', 'TH']);
|
||||||
|
assert.deepStrictEqual(api.countryOptions.map((option) => option.label), ['请先选择服务商', '英国', '泰国']);
|
||||||
|
assert.deepStrictEqual(api.operatorOptions.map((option) => option.value), ['', 'operator-a', 'operator-b']);
|
||||||
|
assert.deepStrictEqual(api.operatorOptions.map((option) => option.label), ['任意线路', 'Operator A', 'Operator B']);
|
||||||
|
assert.equal(api.displayText, 'stored-provider/GB/operator-a');
|
||||||
|
assert.equal(api.toastText, '已刷新 MaDao 服务商。');
|
||||||
|
assert.deepStrictEqual(requests.map((request) => request.pathname), [
|
||||||
|
'/api/providers',
|
||||||
|
'/api/providers/stored-provider/countries',
|
||||||
|
'/api/providers/stored-provider/operators',
|
||||||
|
]);
|
||||||
|
assert.deepStrictEqual(requests[2].body, { country: 'GB' });
|
||||||
|
assert.equal(requests[0].options.headers.Authorization, 'Bearer madao-secret');
|
||||||
|
});
|
||||||
|
|
||||||
test('HeroSMS country parser accepts keyed country maps from the live API', () => {
|
test('HeroSMS country parser accepts keyed country maps from the live API', () => {
|
||||||
const api = new Function(`
|
const api = new Function(`
|
||||||
${extractFunction('normalizeHeroSmsCountryPayloadEntries')}
|
${extractFunction('normalizeHeroSmsCountryPayloadEntries')}
|
||||||
@@ -698,12 +933,14 @@ const rowNexSmsApiKey = { style: { display: 'none' } };
|
|||||||
const rowNexSmsCountry = { style: { display: 'none' } };
|
const rowNexSmsCountry = { style: { display: 'none' } };
|
||||||
const rowNexSmsCountryFallback = { style: { display: 'none' } };
|
const rowNexSmsCountryFallback = { style: { display: 'none' } };
|
||||||
const rowNexSmsServiceCode = { style: { display: 'none' } };
|
const rowNexSmsServiceCode = { style: { display: 'none' } };
|
||||||
|
const rowSmsBowerApiKey = { style: { display: 'none' } };
|
||||||
const rowMaDaoBaseUrl = { style: { display: 'none' } };
|
const rowMaDaoBaseUrl = { style: { display: 'none' } };
|
||||||
const rowMaDaoHttpSecret = { style: { display: 'none' } };
|
const rowMaDaoHttpSecret = { style: { display: 'none' } };
|
||||||
const rowMaDaoMode = { style: { display: 'none' } };
|
const rowMaDaoMode = { style: { display: 'none' } };
|
||||||
const rowMaDaoRoutingPlanId = { style: { display: 'none' } };
|
const rowMaDaoRoutingPlanId = { style: { display: 'none' } };
|
||||||
const rowMaDaoProviderId = { style: { display: 'none' } };
|
const rowMaDaoProviderId = { style: { display: 'none' } };
|
||||||
const rowMaDaoCountry = { style: { display: 'none' } };
|
const rowMaDaoCountry = { style: { display: 'none' } };
|
||||||
|
const rowMaDaoOperator = { style: { display: 'none' } };
|
||||||
const rowMaDaoAutoPickCountry = { style: { display: 'none' } };
|
const rowMaDaoAutoPickCountry = { style: { display: 'none' } };
|
||||||
const rowMaDaoReusePhone = { style: { display: 'none' } };
|
const rowMaDaoReusePhone = { style: { display: 'none' } };
|
||||||
const rowMaDaoPriceRange = { style: { display: 'none' } };
|
const rowMaDaoPriceRange = { style: { display: 'none' } };
|
||||||
@@ -773,6 +1010,11 @@ const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = ${JSON.stringify({
|
|||||||
'rowNexSmsServiceCode',
|
'rowNexSmsServiceCode',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
'sms-bower': {
|
||||||
|
rowKeys: [
|
||||||
|
'rowSmsBowerApiKey',
|
||||||
|
],
|
||||||
|
},
|
||||||
madao: {
|
madao: {
|
||||||
rowKeys: [
|
rowKeys: [
|
||||||
'rowMaDaoBaseUrl',
|
'rowMaDaoBaseUrl',
|
||||||
@@ -785,8 +1027,7 @@ const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = ${JSON.stringify({
|
|||||||
directRowKeys: [
|
directRowKeys: [
|
||||||
'rowMaDaoProviderId',
|
'rowMaDaoProviderId',
|
||||||
'rowMaDaoCountry',
|
'rowMaDaoCountry',
|
||||||
'rowMaDaoAutoPickCountry',
|
'rowMaDaoOperator',
|
||||||
'rowMaDaoReusePhone',
|
|
||||||
'rowMaDaoPriceRange',
|
'rowMaDaoPriceRange',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -856,12 +1097,14 @@ return {
|
|||||||
rowNexSmsCountry,
|
rowNexSmsCountry,
|
||||||
rowNexSmsCountryFallback,
|
rowNexSmsCountryFallback,
|
||||||
rowNexSmsServiceCode,
|
rowNexSmsServiceCode,
|
||||||
|
rowSmsBowerApiKey,
|
||||||
rowMaDaoBaseUrl,
|
rowMaDaoBaseUrl,
|
||||||
rowMaDaoHttpSecret,
|
rowMaDaoHttpSecret,
|
||||||
rowMaDaoMode,
|
rowMaDaoMode,
|
||||||
rowMaDaoRoutingPlanId,
|
rowMaDaoRoutingPlanId,
|
||||||
rowMaDaoProviderId,
|
rowMaDaoProviderId,
|
||||||
rowMaDaoCountry,
|
rowMaDaoCountry,
|
||||||
|
rowMaDaoOperator,
|
||||||
rowMaDaoAutoPickCountry,
|
rowMaDaoAutoPickCountry,
|
||||||
rowMaDaoReusePhone,
|
rowMaDaoReusePhone,
|
||||||
rowMaDaoPriceRange,
|
rowMaDaoPriceRange,
|
||||||
@@ -941,6 +1184,7 @@ return {
|
|||||||
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
|
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoProviderId.style.display, 'none');
|
assert.equal(api.rowMaDaoProviderId.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoCountry.style.display, 'none');
|
assert.equal(api.rowMaDaoCountry.style.display, 'none');
|
||||||
|
assert.equal(api.rowMaDaoOperator.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
|
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
|
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
|
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
|
||||||
@@ -1057,6 +1301,7 @@ return {
|
|||||||
assert.equal(api.rowMaDaoRoutingPlanId.style.display, '');
|
assert.equal(api.rowMaDaoRoutingPlanId.style.display, '');
|
||||||
assert.equal(api.rowMaDaoProviderId.style.display, 'none');
|
assert.equal(api.rowMaDaoProviderId.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoCountry.style.display, 'none');
|
assert.equal(api.rowMaDaoCountry.style.display, 'none');
|
||||||
|
assert.equal(api.rowMaDaoOperator.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
|
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
|
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
|
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
|
||||||
@@ -1069,8 +1314,9 @@ return {
|
|||||||
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
|
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoProviderId.style.display, '');
|
assert.equal(api.rowMaDaoProviderId.style.display, '');
|
||||||
assert.equal(api.rowMaDaoCountry.style.display, '');
|
assert.equal(api.rowMaDaoCountry.style.display, '');
|
||||||
assert.equal(api.rowMaDaoAutoPickCountry.style.display, '');
|
assert.equal(api.rowMaDaoOperator.style.display, '');
|
||||||
assert.equal(api.rowMaDaoReusePhone.style.display, '');
|
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
|
||||||
|
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
|
||||||
assert.equal(api.rowMaDaoPriceRange.style.display, '');
|
assert.equal(api.rowMaDaoPriceRange.style.display, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1160,9 +1406,10 @@ const inputNexSmsServiceCode = { value: 'ot' };
|
|||||||
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/acquire' };
|
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/acquire' };
|
||||||
const inputMaDaoHttpSecret = { value: 'madao-secret' };
|
const inputMaDaoHttpSecret = { value: 'madao-secret' };
|
||||||
const selectMaDaoMode = { value: 'direct' };
|
const selectMaDaoMode = { value: 'direct' };
|
||||||
const inputMaDaoRoutingPlanId = { value: 'plan-1' };
|
const selectMaDaoRoutingPlanId = { value: 'plan-1' };
|
||||||
const inputMaDaoProviderId = { value: 'Local Provider!!' };
|
const selectMaDaoProviderId = { value: 'Local Provider!!' };
|
||||||
const inputMaDaoCountry = { value: 'th' };
|
const selectMaDaoCountry = { value: 'th' };
|
||||||
|
const selectMaDaoOperator = { value: 'Operator A!' };
|
||||||
const inputMaDaoAutoPickCountry = { checked: false };
|
const inputMaDaoAutoPickCountry = { checked: false };
|
||||||
const inputMaDaoReusePhone = { checked: true };
|
const inputMaDaoReusePhone = { checked: true };
|
||||||
const inputMaDaoMinPrice = { value: '0.02' };
|
const inputMaDaoMinPrice = { value: '0.02' };
|
||||||
@@ -1266,7 +1513,9 @@ ${extractFunction('normalizeMaDaoBaseUrlValue')}
|
|||||||
${extractFunction('normalizeMaDaoModeValue')}
|
${extractFunction('normalizeMaDaoModeValue')}
|
||||||
${extractFunction('normalizeMaDaoIdentifierValue')}
|
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||||
${extractFunction('normalizeMaDaoProviderIdValue')}
|
${extractFunction('normalizeMaDaoProviderIdValue')}
|
||||||
|
${extractFunction('normalizeMaDaoOperatorValue')}
|
||||||
${extractFunction('normalizeMaDaoCountry')}
|
${extractFunction('normalizeMaDaoCountry')}
|
||||||
|
${extractFunction('formatMaDaoCountryDisplayLabel')}
|
||||||
${extractFunction('normalizeMaDaoPriceValue')}
|
${extractFunction('normalizeMaDaoPriceValue')}
|
||||||
${extractFunction('normalizeFiveSimCountryCode')}
|
${extractFunction('normalizeFiveSimCountryCode')}
|
||||||
${extractFunction('normalizeFiveSimCountryOrderValue')}
|
${extractFunction('normalizeFiveSimCountryOrderValue')}
|
||||||
@@ -1338,6 +1587,7 @@ return { collectSettingsPayload };
|
|||||||
assert.equal(payload.madaoRoutingPlanId, 'plan-1');
|
assert.equal(payload.madaoRoutingPlanId, 'plan-1');
|
||||||
assert.equal(payload.madaoProviderId, 'localprovider');
|
assert.equal(payload.madaoProviderId, 'localprovider');
|
||||||
assert.equal(payload.madaoCountry, 'TH');
|
assert.equal(payload.madaoCountry, 'TH');
|
||||||
|
assert.equal(payload.madaoOperator, 'operatora');
|
||||||
assert.equal(payload.madaoAutoPickCountry, false);
|
assert.equal(payload.madaoAutoPickCountry, false);
|
||||||
assert.equal(payload.madaoReusePhone, true);
|
assert.equal(payload.madaoReusePhone, true);
|
||||||
assert.equal(payload.madaoMinPrice, '0.02');
|
assert.equal(payload.madaoMinPrice, '0.02');
|
||||||
@@ -1389,6 +1639,7 @@ let latestState = {
|
|||||||
madaoRoutingPlanId: 'plan-old',
|
madaoRoutingPlanId: 'plan-old',
|
||||||
madaoProviderId: 'provider-old',
|
madaoProviderId: 'provider-old',
|
||||||
madaoCountry: 'TH',
|
madaoCountry: 'TH',
|
||||||
|
madaoOperator: 'operator-old',
|
||||||
madaoAutoPickCountry: true,
|
madaoAutoPickCountry: true,
|
||||||
madaoReusePhone: true,
|
madaoReusePhone: true,
|
||||||
madaoMinPrice: '0.01',
|
madaoMinPrice: '0.01',
|
||||||
@@ -1434,9 +1685,34 @@ const inputNexSmsServiceCode = { value: 'ot' };
|
|||||||
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/poll' };
|
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/poll' };
|
||||||
const inputMaDaoHttpSecret = { value: 'madao-live-secret' };
|
const inputMaDaoHttpSecret = { value: 'madao-live-secret' };
|
||||||
const selectMaDaoMode = { value: 'direct' };
|
const selectMaDaoMode = { value: 'direct' };
|
||||||
const inputMaDaoRoutingPlanId = { value: 'plan-live' };
|
const selectMaDaoRoutingPlanId = {
|
||||||
const inputMaDaoProviderId = { value: 'Provider Live!' };
|
value: 'plan-live',
|
||||||
const inputMaDaoCountry = { value: 'local' };
|
options: [],
|
||||||
|
replaceChildren(...children) {
|
||||||
|
this.options = children;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const selectMaDaoProviderId = {
|
||||||
|
value: 'Provider Live!',
|
||||||
|
options: [],
|
||||||
|
replaceChildren(...children) {
|
||||||
|
this.options = children;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const selectMaDaoCountry = {
|
||||||
|
value: 'local',
|
||||||
|
options: [],
|
||||||
|
replaceChildren(...children) {
|
||||||
|
this.options = children;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const selectMaDaoOperator = {
|
||||||
|
value: 'Operator Live!',
|
||||||
|
options: [],
|
||||||
|
replaceChildren(...children) {
|
||||||
|
this.options = children;
|
||||||
|
},
|
||||||
|
};
|
||||||
const inputMaDaoAutoPickCountry = { checked: false };
|
const inputMaDaoAutoPickCountry = { checked: false };
|
||||||
const inputMaDaoReusePhone = { checked: false };
|
const inputMaDaoReusePhone = { checked: false };
|
||||||
const inputMaDaoMinPrice = { value: '0.02' };
|
const inputMaDaoMinPrice = { value: '0.02' };
|
||||||
@@ -1454,6 +1730,10 @@ let heroSmsCountrySelectionOrder = [];
|
|||||||
let phoneSmsProviderOrderSelection = ['hero-sms', '5sim'];
|
let phoneSmsProviderOrderSelection = ['hero-sms', '5sim'];
|
||||||
let lastPhoneSmsProviderBeforeChange = null;
|
let lastPhoneSmsProviderBeforeChange = null;
|
||||||
let savedPayload = null;
|
let savedPayload = null;
|
||||||
|
let maDaoRoutingPlanOptions = [];
|
||||||
|
let maDaoProviderOptions = [];
|
||||||
|
let maDaoCountryOptions = [];
|
||||||
|
let maDaoOperatorOptions = [];
|
||||||
|
|
||||||
${extractFunction('normalizePhoneSmsProvider')}
|
${extractFunction('normalizePhoneSmsProvider')}
|
||||||
${extractFunction('normalizePhoneSmsProviderValue')}
|
${extractFunction('normalizePhoneSmsProviderValue')}
|
||||||
@@ -1483,9 +1763,21 @@ ${extractFunction('normalizeNexSmsServiceCodeValue')}
|
|||||||
${extractFunction('normalizeMaDaoBaseUrlValue')}
|
${extractFunction('normalizeMaDaoBaseUrlValue')}
|
||||||
${extractFunction('normalizeMaDaoModeValue')}
|
${extractFunction('normalizeMaDaoModeValue')}
|
||||||
${extractFunction('normalizeMaDaoIdentifierValue')}
|
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||||
|
${extractFunction('normalizeMaDaoRoutingPlanIdValue')}
|
||||||
${extractFunction('normalizeMaDaoProviderIdValue')}
|
${extractFunction('normalizeMaDaoProviderIdValue')}
|
||||||
|
${extractFunction('normalizeMaDaoOperatorValue')}
|
||||||
${extractFunction('normalizeMaDaoCountry')}
|
${extractFunction('normalizeMaDaoCountry')}
|
||||||
${extractFunction('normalizeMaDaoPriceValue')}
|
${extractFunction('normalizeMaDaoPriceValue')}
|
||||||
|
${extractFunction('formatMaDaoCountryDisplayLabel')}
|
||||||
|
${extractFunction('createSelectOptionElement')}
|
||||||
|
${extractFunction('setSelectOptions')}
|
||||||
|
${extractFunction('normalizeMaDaoOptionListItems')}
|
||||||
|
${extractFunction('resolveMaDaoOptionSelectedValue')}
|
||||||
|
${extractFunction('buildMaDaoRoutingPlanOptions')}
|
||||||
|
${extractFunction('setMaDaoRoutingPlanSelectOptions')}
|
||||||
|
${extractFunction('setMaDaoProviderSelectOptions')}
|
||||||
|
${extractFunction('setMaDaoCountrySelectOptions')}
|
||||||
|
${extractFunction('setMaDaoOperatorSelectOptions')}
|
||||||
${extractFunction('normalizePhoneSmsMinPriceValue')}
|
${extractFunction('normalizePhoneSmsMinPriceValue')}
|
||||||
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||||
${extractFunction('normalizeHeroSmsCountryId')}
|
${extractFunction('normalizeHeroSmsCountryId')}
|
||||||
@@ -1508,6 +1800,7 @@ function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
|
|||||||
function loadHeroSmsCountries() { return Promise.resolve(); }
|
function loadHeroSmsCountries() { return Promise.resolve(); }
|
||||||
function loadFiveSimCountries() { return Promise.resolve(); }
|
function loadFiveSimCountries() { return Promise.resolve(); }
|
||||||
function loadNexSmsCountries() { return Promise.resolve(); }
|
function loadNexSmsCountries() { return Promise.resolve(); }
|
||||||
|
function loadMaDaoProviders() { return Promise.resolve(); }
|
||||||
function applyHeroSmsFallbackSelection() {}
|
function applyHeroSmsFallbackSelection() {}
|
||||||
function applyFiveSimCountrySelection() {}
|
function applyFiveSimCountrySelection() {}
|
||||||
function applyNexSmsCountrySelection() {}
|
function applyNexSmsCountrySelection() {}
|
||||||
@@ -1564,6 +1857,71 @@ return {
|
|||||||
assert.equal(api.savedPayload.fiveSimMinPrice, '0.88');
|
assert.equal(api.savedPayload.fiveSimMinPrice, '0.88');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildPhoneSmsProviderStatePatch preserves MaDao hidden reuse defaults when controls are absent', () => {
|
||||||
|
const api = new Function(`
|
||||||
|
let latestState = {
|
||||||
|
phoneSmsProvider: 'madao',
|
||||||
|
madaoBaseUrl: 'http://127.0.0.1:7822',
|
||||||
|
madaoHttpSecret: 'secret-old',
|
||||||
|
madaoMode: 'direct',
|
||||||
|
madaoRoutingPlanId: 'plan-old',
|
||||||
|
madaoProviderId: 'provider-old',
|
||||||
|
madaoCountry: 'TH',
|
||||||
|
madaoOperator: 'any',
|
||||||
|
madaoAutoPickCountry: true,
|
||||||
|
madaoReusePhone: true,
|
||||||
|
madaoMinPrice: '0.01',
|
||||||
|
madaoMaxPrice: '0.09',
|
||||||
|
};
|
||||||
|
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||||
|
const PHONE_SMS_PROVIDER_HERO = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||||
|
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||||
|
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
|
||||||
|
const PHONE_SMS_PROVIDER_MADAO = 'madao';
|
||||||
|
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||||
|
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||||
|
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||||
|
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||||
|
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
|
||||||
|
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
|
||||||
|
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
||||||
|
const MADAO_MODE_ROUTING_PLAN = 'routing_plan';
|
||||||
|
const MADAO_MODE_DIRECT = 'direct';
|
||||||
|
const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN;
|
||||||
|
const selectMaDaoMode = { value: 'direct' };
|
||||||
|
const selectMaDaoRoutingPlanId = { value: 'plan-live' };
|
||||||
|
const selectMaDaoProviderId = { value: 'provider-live' };
|
||||||
|
const selectMaDaoCountry = { value: 'GB' };
|
||||||
|
const selectMaDaoOperator = { value: 'Any operator' };
|
||||||
|
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/acquire' };
|
||||||
|
const inputMaDaoHttpSecret = { value: 'secret-live' };
|
||||||
|
const inputMaDaoMinPrice = { value: '0.02' };
|
||||||
|
const inputMaDaoMaxPrice = { value: '0.20' };
|
||||||
|
|
||||||
|
${extractFunction('normalizePhoneSmsProvider')}
|
||||||
|
${extractFunction('normalizeMaDaoBaseUrlValue')}
|
||||||
|
${extractFunction('normalizeMaDaoModeValue')}
|
||||||
|
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||||
|
${extractFunction('normalizeMaDaoRoutingPlanIdValue')}
|
||||||
|
${extractFunction('normalizeMaDaoProviderIdValue')}
|
||||||
|
${extractFunction('normalizeMaDaoOperatorValue')}
|
||||||
|
${extractFunction('normalizeMaDaoCountry')}
|
||||||
|
${extractFunction('normalizeMaDaoPriceValue')}
|
||||||
|
${extractFunction('normalizePhoneSmsMinPriceValue')}
|
||||||
|
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||||
|
${extractFunction('buildPhoneSmsProviderStatePatch')}
|
||||||
|
return { buildPhoneSmsProviderStatePatch };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const patch = api.buildPhoneSmsProviderStatePatch('madao');
|
||||||
|
|
||||||
|
assert.equal(patch.madaoAutoPickCountry, true);
|
||||||
|
assert.equal(patch.madaoReusePhone, true);
|
||||||
|
assert.equal(patch.madaoOperator, '');
|
||||||
|
assert.equal(patch.madaoProviderId, 'provider-live');
|
||||||
|
assert.equal(patch.madaoCountry, 'GB');
|
||||||
|
});
|
||||||
|
|
||||||
test('HeroSMS operator helpers normalize keyed operator payloads', () => {
|
test('HeroSMS operator helpers normalize keyed operator payloads', () => {
|
||||||
const api = new Function(`
|
const api = new Function(`
|
||||||
const DEFAULT_HERO_SMS_OPERATOR = 'any';
|
const DEFAULT_HERO_SMS_OPERATOR = 'any';
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const source = fs.readFileSync('phone-sms/providers/sms-bower.js', 'utf8');
|
||||||
|
const api = new Function('self', `${source}; return self.PhoneSmsBowerProvider;`)({});
|
||||||
|
|
||||||
|
function createTextResponse(payload, ok = true, status = ok ? 200 : 400, statusText = ok ? 'OK' : 'Bad Request') {
|
||||||
|
return {
|
||||||
|
ok,
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
text: async () => (typeof payload === 'string' ? payload : JSON.stringify(payload)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('SMS Bower provider uses handler API key query for balance and price catalog', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const provider = api.createProvider({
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
requests.push({ url: parsed, options });
|
||||||
|
if (parsed.searchParams.get('action') === 'getBalance') {
|
||||||
|
return createTextResponse('ACCESS_BALANCE:12.34');
|
||||||
|
}
|
||||||
|
if (parsed.searchParams.get('action') === 'getPrices') {
|
||||||
|
return createTextResponse({ 52: { ot: { cost: 0.21, count: 7 } } });
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected ${parsed.toString()}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const balance = await provider.fetchBalance({ smsBowerApiKey: 'demo-key' });
|
||||||
|
const prices = await provider.fetchPrices({ smsBowerApiKey: 'demo-key', smsBowerCountryId: 52 });
|
||||||
|
const entries = provider.collectPriceEntries(prices, []);
|
||||||
|
|
||||||
|
assert.equal(requests[0].url.origin + requests[0].url.pathname, 'https://smsbower.page/stubs/handler_api.php');
|
||||||
|
assert.equal(requests[0].url.searchParams.get('api_key'), 'demo-key');
|
||||||
|
assert.equal(requests[0].url.searchParams.get('action'), 'getBalance');
|
||||||
|
assert.equal(balance.balance, 12.34);
|
||||||
|
assert.equal(requests[1].url.searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[1].url.searchParams.get('service'), 'ot');
|
||||||
|
assert.equal(requests[1].url.searchParams.get('country'), '52');
|
||||||
|
assert.deepStrictEqual(entries, [{ cost: 0.21, count: 7, inStock: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SMS Bower provider acquires number, polls code, and updates activation statuses', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const provider = api.createProvider({
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const action = parsed.searchParams.get('action');
|
||||||
|
requests.push({ url: parsed, options });
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
return createTextResponse('ACCESS_NUMBER:12345:447700900123');
|
||||||
|
}
|
||||||
|
if (action === 'getStatus') {
|
||||||
|
return createTextResponse('STATUS_OK:Your OpenAI code is 654321');
|
||||||
|
}
|
||||||
|
if (action === 'setStatus') {
|
||||||
|
return createTextResponse(parsed.searchParams.get('status') === '6' ? 'ACCESS_ACTIVATION' : 'ACCESS_CANCEL');
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected ${parsed.toString()}`);
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
smsBowerApiKey: 'demo-key',
|
||||||
|
smsBowerServiceCode: 'ot',
|
||||||
|
smsBowerCountryId: 16,
|
||||||
|
smsBowerCountryLabel: 'United Kingdom',
|
||||||
|
smsBowerMaxPrice: '0.3',
|
||||||
|
smsBowerMinPrice: '0.1',
|
||||||
|
smsBowerProviderIds: '3170,3180',
|
||||||
|
};
|
||||||
|
|
||||||
|
const activation = await provider.requestActivation(state);
|
||||||
|
const code = await provider.pollActivationCode(state, activation, { timeoutMs: 1000, intervalMs: 1, maxRounds: 1 });
|
||||||
|
await provider.finishActivation(state, activation);
|
||||||
|
await provider.cancelActivation(state, activation);
|
||||||
|
await provider.banActivation(state, activation);
|
||||||
|
const reused = await provider.reuseActivation(state, activation);
|
||||||
|
|
||||||
|
assert.equal(activation.provider, 'sms-bower');
|
||||||
|
assert.equal(activation.activationId, '12345');
|
||||||
|
assert.equal(activation.phoneNumber, '447700900123');
|
||||||
|
assert.equal(activation.countryId, 16);
|
||||||
|
assert.equal(code, '654321');
|
||||||
|
assert.equal(reused.activationId, '12345');
|
||||||
|
|
||||||
|
const acquireUrl = requests[0].url;
|
||||||
|
assert.equal(acquireUrl.searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(acquireUrl.searchParams.get('service'), 'ot');
|
||||||
|
assert.equal(acquireUrl.searchParams.get('country'), '16');
|
||||||
|
assert.equal(acquireUrl.searchParams.get('maxPrice'), '0.3');
|
||||||
|
assert.equal(acquireUrl.searchParams.get('minPrice'), '0.1');
|
||||||
|
assert.equal(acquireUrl.searchParams.get('providerIds'), '3170,3180');
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
requests.map((entry) => [entry.url.searchParams.get('action'), entry.url.searchParams.get('status')]),
|
||||||
|
[
|
||||||
|
['getNumber', null],
|
||||||
|
['getStatus', null],
|
||||||
|
['setStatus', '6'],
|
||||||
|
['setStatus', '8'],
|
||||||
|
['setStatus', '8'],
|
||||||
|
['setStatus', '3'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
function loadRegistry() {
|
||||||
|
const sandbox = { globalThis: {} };
|
||||||
|
sandbox.self = sandbox.globalThis;
|
||||||
|
vm.runInNewContext(fs.readFileSync('phone-sms/providers/sms-bower.js', 'utf8'), sandbox);
|
||||||
|
vm.runInNewContext(fs.readFileSync('phone-sms/providers/registry.js', 'utf8'), sandbox);
|
||||||
|
return sandbox.globalThis.PhoneSmsProviderRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('SMS Bower is registered as a known phone SMS provider', () => {
|
||||||
|
const registry = loadRegistry();
|
||||||
|
assert.equal(registry.normalizeProviderId('sms-bower'), 'sms-bower');
|
||||||
|
assert.equal(registry.getProviderLabel('sms-bower'), 'SMS Bower');
|
||||||
|
assert.ok(registry.getProviderIds().includes('sms-bower'));
|
||||||
|
assert.deepEqual(
|
||||||
|
Array.from(registry.normalizeProviderOrder(['sms-bower', 'hero-sms'])),
|
||||||
|
['sms-bower', 'hero-sms']
|
||||||
|
);
|
||||||
|
const provider = registry.createProvider('sms-bower', { fetchImpl: async () => ({ ok: true, text: async () => 'ACCESS_BALANCE:1.23' }) });
|
||||||
|
assert.equal(provider.id, 'sms-bower');
|
||||||
|
});
|
||||||
@@ -241,13 +241,18 @@ ${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')}
|
|||||||
${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')}
|
${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')}
|
||||||
${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')}
|
${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')}
|
||||||
${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')}
|
${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')}
|
||||||
${extractFunction('getPageTextSnapshot')}
|
${extractFunction('getPageTextSnapshot')}
|
||||||
${extractFunction('normalizeAuthAccountIdentifier')}
|
${extractFunction('normalizeAuthAccountIdentifier')}
|
||||||
${extractFunction('getChooseAccountCandidateText')}
|
${extractFunction('getChooseAccountCandidateText')}
|
||||||
${extractFunction('isChooseAccountPage')}
|
${extractFunction('isChooseAccountPage')}
|
||||||
${extractFunction('isChooseAccountRemovalAction')}
|
${extractFunction('isChooseAccountRemovalAction')}
|
||||||
${extractFunction('resolveChooseAccountClickTarget')}
|
${extractFunction('resolveChooseAccountClickTarget')}
|
||||||
|
${extractFunction('resolveChooseAccountCardTarget')}
|
||||||
${extractFunction('findChooseAccountButtonForEmail')}
|
${extractFunction('findChooseAccountButtonForEmail')}
|
||||||
|
${extractFunction('findChooseAccountOtherAccountButton')}
|
||||||
|
${extractFunction('getChooseAccountListedEmails')}
|
||||||
|
${extractFunction('resolveChooseAccountAction')}
|
||||||
${extractFunction('createStep6SuccessResult')}
|
${extractFunction('createStep6SuccessResult')}
|
||||||
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
||||||
${extractFunction('createStep6AddEmailSuccessResult')}
|
${extractFunction('createStep6AddEmailSuccessResult')}
|
||||||
@@ -348,13 +353,18 @@ ${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')}
|
|||||||
${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')}
|
${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')}
|
||||||
${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')}
|
${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')}
|
||||||
${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')}
|
${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')}
|
||||||
${extractFunction('getPageTextSnapshot')}
|
${extractFunction('getPageTextSnapshot')}
|
||||||
${extractFunction('normalizeAuthAccountIdentifier')}
|
${extractFunction('normalizeAuthAccountIdentifier')}
|
||||||
${extractFunction('getChooseAccountCandidateText')}
|
${extractFunction('getChooseAccountCandidateText')}
|
||||||
${extractFunction('isChooseAccountPage')}
|
${extractFunction('isChooseAccountPage')}
|
||||||
${extractFunction('isChooseAccountRemovalAction')}
|
${extractFunction('isChooseAccountRemovalAction')}
|
||||||
${extractFunction('resolveChooseAccountClickTarget')}
|
${extractFunction('resolveChooseAccountClickTarget')}
|
||||||
|
${extractFunction('resolveChooseAccountCardTarget')}
|
||||||
${extractFunction('findChooseAccountButtonForEmail')}
|
${extractFunction('findChooseAccountButtonForEmail')}
|
||||||
|
${extractFunction('findChooseAccountOtherAccountButton')}
|
||||||
|
${extractFunction('getChooseAccountListedEmails')}
|
||||||
|
${extractFunction('resolveChooseAccountAction')}
|
||||||
${extractFunction('createStep6SuccessResult')}
|
${extractFunction('createStep6SuccessResult')}
|
||||||
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
||||||
${extractFunction('createStep6AddEmailSuccessResult')}
|
${extractFunction('createStep6AddEmailSuccessResult')}
|
||||||
@@ -386,6 +396,148 @@ return {
|
|||||||
assert.equal(result.via, 'choose_account_oauth_authorization_route');
|
assert.equal(result.via, 'choose_account_oauth_authorization_route');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step 7 clicks matching choose-account card when email is inside a non-action div card', async () => {
|
||||||
|
const api = new Function(`
|
||||||
|
let pageState = 'choose_account_page';
|
||||||
|
const clicked = [];
|
||||||
|
const location = {
|
||||||
|
href: 'https://auth.openai.com/choose-an-account',
|
||||||
|
pathname: '/choose-an-account',
|
||||||
|
};
|
||||||
|
|
||||||
|
function createElement(id, textContent = '', attrs = {}) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
textContent,
|
||||||
|
value: '',
|
||||||
|
parentElement: null,
|
||||||
|
disabled: false,
|
||||||
|
tagName: attrs.tagName || 'DIV',
|
||||||
|
tabIndex: attrs.tabIndex ?? -1,
|
||||||
|
className: attrs.className || '',
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'class') return this.className;
|
||||||
|
if (name === 'aria-disabled') return 'false';
|
||||||
|
if (name === 'role') return attrs.role || '';
|
||||||
|
return attrs[name] || '';
|
||||||
|
},
|
||||||
|
closest(selector) {
|
||||||
|
let current = this;
|
||||||
|
while (current) {
|
||||||
|
if (
|
||||||
|
String(selector).includes('[class*="account" i]')
|
||||||
|
&& String(current.className || '').toLowerCase().includes('account')
|
||||||
|
) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetCard = createElement('target-card', 'Kenneth Wilson sedate-iodize-lisp@duck.com', {
|
||||||
|
className: 'account-card rounded-xl',
|
||||||
|
});
|
||||||
|
const targetEmail = createElement('target-email', 'sedate-iodize-lisp@duck.com');
|
||||||
|
targetEmail.parentElement = targetCard;
|
||||||
|
const removeButton = createElement('remove-button', '', {
|
||||||
|
tagName: 'BUTTON',
|
||||||
|
'aria-label': 'Remove sedate-iodize-lisp@duck.com',
|
||||||
|
});
|
||||||
|
removeButton.parentElement = targetCard;
|
||||||
|
const otherCard = createElement('other-card', 'Other User other@example.com', {
|
||||||
|
className: 'account-card rounded-xl',
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = {
|
||||||
|
body: {
|
||||||
|
innerText: '欢迎回来 选择一个帐户 sedate-iodize-lisp@duck.com other@example.com',
|
||||||
|
textContent: '欢迎回来 选择一个帐户 sedate-iodize-lisp@duck.com other@example.com',
|
||||||
|
},
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (String(selector).includes('body *')) return [removeButton, targetEmail, targetCard, otherCard];
|
||||||
|
return [removeButton];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOperationDelayRunner() {
|
||||||
|
return async (_metadata, operation) => operation();
|
||||||
|
}
|
||||||
|
function isVisibleElement(element) {
|
||||||
|
return Boolean(element);
|
||||||
|
}
|
||||||
|
function isActionEnabled(element) {
|
||||||
|
return Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true';
|
||||||
|
}
|
||||||
|
function simulateClick(element) {
|
||||||
|
clicked.push(element.id);
|
||||||
|
if (element === targetCard) {
|
||||||
|
pageState = 'oauth_consent_page';
|
||||||
|
location.href = 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent';
|
||||||
|
location.pathname = '/sign-in-with-chatgpt/codex/consent';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function inspectLoginAuthState() {
|
||||||
|
return { state: pageState, url: location.href, chooseAccountPage: pageState === 'choose_account_page' };
|
||||||
|
}
|
||||||
|
function throwIfStopped() {}
|
||||||
|
async function sleep() {}
|
||||||
|
async function humanPause() {}
|
||||||
|
function log() {}
|
||||||
|
async function finalizeStep6VerificationReady() { return { routed: 'verification' }; }
|
||||||
|
async function step6LoginFromPasswordPage() { return { routed: 'password' }; }
|
||||||
|
async function step6LoginFromEmailPage() { return { routed: 'email' }; }
|
||||||
|
async function step6LoginFromPhonePage() { return { routed: 'phone' }; }
|
||||||
|
async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'timeout' } }; }
|
||||||
|
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')}
|
||||||
|
${extractFunction('getPageTextSnapshot')}
|
||||||
|
${extractFunction('normalizeAuthAccountIdentifier')}
|
||||||
|
${extractFunction('getChooseAccountCandidateText')}
|
||||||
|
${extractFunction('isChooseAccountPage')}
|
||||||
|
${extractFunction('isChooseAccountRemovalAction')}
|
||||||
|
${extractFunction('resolveChooseAccountClickTarget')}
|
||||||
|
${extractFunction('resolveChooseAccountCardTarget')}
|
||||||
|
${extractFunction('findChooseAccountButtonForEmail')}
|
||||||
|
${extractFunction('findChooseAccountOtherAccountButton')}
|
||||||
|
${extractFunction('getChooseAccountListedEmails')}
|
||||||
|
${extractFunction('resolveChooseAccountAction')}
|
||||||
|
${extractFunction('createStep6SuccessResult')}
|
||||||
|
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
||||||
|
${extractFunction('createStep6AddEmailSuccessResult')}
|
||||||
|
${extractFunction('createStep6RecoverableResult')}
|
||||||
|
${extractFunction('normalizeStep6Snapshot')}
|
||||||
|
${extractFunction('isOpenAiOAuthAuthorizationRoute')}
|
||||||
|
${extractFunction('isPostChooseAccountOAuthRoute')}
|
||||||
|
${extractFunction('waitForChooseAccountTransition')}
|
||||||
|
${extractFunction('step6ChooseExistingAccount')}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clicked,
|
||||||
|
run() {
|
||||||
|
return step6ChooseExistingAccount(
|
||||||
|
{ email: 'sedate-iodize-lisp@duck.com', loginIdentifierType: 'email', visibleStep: 9 },
|
||||||
|
{ state: 'choose_account_page', url: location.href }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.run();
|
||||||
|
|
||||||
|
assert.deepEqual(api.clicked, ['target-card']);
|
||||||
|
assert.equal(result.step6Outcome, 'success');
|
||||||
|
assert.equal(result.state, 'oauth_consent_page');
|
||||||
|
assert.equal(result.skipLoginVerificationStep, true);
|
||||||
|
assert.equal(result.directOAuthConsentPage, true);
|
||||||
|
});
|
||||||
|
|
||||||
test('step 7 does not click choose-account page when target email is missing', async () => {
|
test('step 7 does not click choose-account page when target email is missing', async () => {
|
||||||
const api = new Function(`
|
const api = new Function(`
|
||||||
const clicked = [];
|
const clicked = [];
|
||||||
@@ -447,13 +599,18 @@ ${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')}
|
|||||||
${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')}
|
${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')}
|
||||||
${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')}
|
${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')}
|
||||||
${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')}
|
${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')}
|
||||||
${extractFunction('getPageTextSnapshot')}
|
${extractFunction('getPageTextSnapshot')}
|
||||||
${extractFunction('normalizeAuthAccountIdentifier')}
|
${extractFunction('normalizeAuthAccountIdentifier')}
|
||||||
${extractFunction('getChooseAccountCandidateText')}
|
${extractFunction('getChooseAccountCandidateText')}
|
||||||
${extractFunction('isChooseAccountPage')}
|
${extractFunction('isChooseAccountPage')}
|
||||||
${extractFunction('isChooseAccountRemovalAction')}
|
${extractFunction('isChooseAccountRemovalAction')}
|
||||||
${extractFunction('resolveChooseAccountClickTarget')}
|
${extractFunction('resolveChooseAccountClickTarget')}
|
||||||
|
${extractFunction('resolveChooseAccountCardTarget')}
|
||||||
${extractFunction('findChooseAccountButtonForEmail')}
|
${extractFunction('findChooseAccountButtonForEmail')}
|
||||||
|
${extractFunction('findChooseAccountOtherAccountButton')}
|
||||||
|
${extractFunction('getChooseAccountListedEmails')}
|
||||||
|
${extractFunction('resolveChooseAccountAction')}
|
||||||
${extractFunction('createStep6SuccessResult')}
|
${extractFunction('createStep6SuccessResult')}
|
||||||
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
||||||
${extractFunction('createStep6AddEmailSuccessResult')}
|
${extractFunction('createStep6AddEmailSuccessResult')}
|
||||||
@@ -481,3 +638,167 @@ return {
|
|||||||
assert.equal(result.step6Outcome, 'recoverable');
|
assert.equal(result.step6Outcome, 'recoverable');
|
||||||
assert.equal(result.reason, 'choose_account_target_not_found');
|
assert.equal(result.reason, 'choose_account_target_not_found');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step 7 uses another-account login when choose-account lists a different email', async () => {
|
||||||
|
const api = new Function(`
|
||||||
|
let pageState = 'choose_account_page';
|
||||||
|
const clicked = [];
|
||||||
|
const location = {
|
||||||
|
href: 'https://auth.openai.com/choose-an-account',
|
||||||
|
pathname: '/choose-an-account',
|
||||||
|
};
|
||||||
|
const existingCard = {
|
||||||
|
id: 'existing-card',
|
||||||
|
tagName: 'BUTTON',
|
||||||
|
textContent: 'p 选择帐户 pang-pushing-dealt@duck.com pang-pushing-dealt@duck.com',
|
||||||
|
value: '',
|
||||||
|
parentElement: null,
|
||||||
|
disabled: false,
|
||||||
|
className: '_root_2sicu_62 _leftAlign_2sicu_99 _outline_2sicu_119',
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-disabled') return 'false';
|
||||||
|
if (name === 'class') return this.className;
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
closest() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const removeButton = {
|
||||||
|
id: 'remove-button',
|
||||||
|
tagName: 'BUTTON',
|
||||||
|
textContent: '移除帐户:pang-pushing-dealt@duck.com',
|
||||||
|
value: '',
|
||||||
|
parentElement: null,
|
||||||
|
disabled: false,
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-label') return '移除帐户:pang-pushing-dealt@duck.com';
|
||||||
|
if (name === 'aria-disabled') return 'false';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
closest() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const otherAccount = {
|
||||||
|
id: 'other-account',
|
||||||
|
tagName: 'A',
|
||||||
|
textContent: '登录至另一个帐户',
|
||||||
|
value: '',
|
||||||
|
parentElement: null,
|
||||||
|
disabled: false,
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-disabled') return 'false';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
closest() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const createAccount = {
|
||||||
|
id: 'create-account',
|
||||||
|
tagName: 'A',
|
||||||
|
textContent: '创建帐户',
|
||||||
|
value: '',
|
||||||
|
parentElement: null,
|
||||||
|
disabled: false,
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-disabled') return 'false';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
closest() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const document = {
|
||||||
|
body: {
|
||||||
|
innerText: '欢迎回来 选择一个帐户 pang-pushing-dealt@duck.com 登录至另一个帐户 创建帐户',
|
||||||
|
textContent: '欢迎回来 选择一个帐户 pang-pushing-dealt@duck.com 登录至另一个帐户 创建帐户',
|
||||||
|
},
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (String(selector).includes('body *')) return [existingCard, removeButton, otherAccount, createAccount];
|
||||||
|
return [existingCard, removeButton, otherAccount, createAccount];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOperationDelayRunner() {
|
||||||
|
return async (_metadata, operation) => operation();
|
||||||
|
}
|
||||||
|
function isVisibleElement(element) {
|
||||||
|
return Boolean(element);
|
||||||
|
}
|
||||||
|
function isActionEnabled(element) {
|
||||||
|
return Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true';
|
||||||
|
}
|
||||||
|
function simulateClick(element) {
|
||||||
|
clicked.push(element.id);
|
||||||
|
if (element === otherAccount) {
|
||||||
|
pageState = 'email_page';
|
||||||
|
location.href = 'https://auth.openai.com/log-in';
|
||||||
|
location.pathname = '/log-in';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function inspectLoginAuthState() {
|
||||||
|
return { state: pageState, url: location.href, chooseAccountPage: pageState === 'choose_account_page' };
|
||||||
|
}
|
||||||
|
function throwIfStopped() {}
|
||||||
|
async function sleep() {}
|
||||||
|
async function humanPause() {}
|
||||||
|
function log() {}
|
||||||
|
async function finalizeStep6VerificationReady() { return { routed: 'verification' }; }
|
||||||
|
async function step6LoginFromPasswordPage() { return { routed: 'password' }; }
|
||||||
|
async function step6LoginFromEmailPage(payload, snapshot) {
|
||||||
|
return {
|
||||||
|
routed: 'email',
|
||||||
|
email: payload.email,
|
||||||
|
state: snapshot.state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function step6LoginFromPhonePage() { return { routed: 'phone' }; }
|
||||||
|
async function step6OpenLoginEntry() { return { routed: 'entry' }; }
|
||||||
|
async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'timeout' } }; }
|
||||||
|
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')}
|
||||||
|
${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')}
|
||||||
|
${extractFunction('getPageTextSnapshot')}
|
||||||
|
${extractFunction('normalizeAuthAccountIdentifier')}
|
||||||
|
${extractFunction('getChooseAccountCandidateText')}
|
||||||
|
${extractFunction('isChooseAccountPage')}
|
||||||
|
${extractFunction('isChooseAccountRemovalAction')}
|
||||||
|
${extractFunction('resolveChooseAccountClickTarget')}
|
||||||
|
${extractFunction('resolveChooseAccountCardTarget')}
|
||||||
|
${extractFunction('findChooseAccountButtonForEmail')}
|
||||||
|
${extractFunction('findChooseAccountOtherAccountButton')}
|
||||||
|
${extractFunction('getChooseAccountListedEmails')}
|
||||||
|
${extractFunction('resolveChooseAccountAction')}
|
||||||
|
${extractFunction('createStep6SuccessResult')}
|
||||||
|
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
||||||
|
${extractFunction('createStep6AddEmailSuccessResult')}
|
||||||
|
${extractFunction('createStep6RecoverableResult')}
|
||||||
|
${extractFunction('normalizeStep6Snapshot')}
|
||||||
|
${extractFunction('isOpenAiOAuthAuthorizationRoute')}
|
||||||
|
${extractFunction('isPostChooseAccountOAuthRoute')}
|
||||||
|
${extractFunction('waitForChooseAccountTransition')}
|
||||||
|
${extractFunction('step6ChooseExistingAccount')}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clicked,
|
||||||
|
run() {
|
||||||
|
return step6ChooseExistingAccount(
|
||||||
|
{ email: 'sedate-iodize-lisp@duck.com', loginIdentifierType: 'email', visibleStep: 9 },
|
||||||
|
{ state: 'choose_account_page', url: location.href }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.run();
|
||||||
|
|
||||||
|
assert.deepEqual(api.clicked, ['other-account']);
|
||||||
|
assert.equal(result.routed, 'email');
|
||||||
|
assert.equal(result.email, 'sedate-iodize-lisp@duck.com');
|
||||||
|
assert.equal(result.state, 'email_page');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user