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 |
+96
-21
@@ -21,7 +21,9 @@ importScripts(
|
||||
'gopay-utils.js',
|
||||
'phone-sms/providers/hero-sms.js',
|
||||
'phone-sms/providers/five-sim.js',
|
||||
'phone-sms/providers/nexsms.js',
|
||||
'phone-sms/providers/madao.js',
|
||||
'phone-sms/providers/sms-bower.js',
|
||||
'phone-sms/providers/registry.js',
|
||||
'background/phone-verification-flow.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_NEXSMS = 'nexsms';
|
||||
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_ORDER = Object.freeze([
|
||||
PHONE_SMS_PROVIDER_HERO,
|
||||
PHONE_SMS_PROVIDER_5SIM,
|
||||
PHONE_SMS_PROVIDER_NEXSMS,
|
||||
PHONE_SMS_PROVIDER_MADAO,
|
||||
PHONE_SMS_PROVIDER_SMS_BOWER,
|
||||
]);
|
||||
const DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1';
|
||||
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_SERVICE_CODE = 'ot';
|
||||
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_MODE = 'routing_plan';
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
@@ -1446,12 +1452,14 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
nexSmsApiKey: '',
|
||||
nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER],
|
||||
nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE,
|
||||
smsBowerApiKey: '',
|
||||
madaoBaseUrl: DEFAULT_MADAO_BASE_URL,
|
||||
madaoHttpSecret: '',
|
||||
madaoMode: DEFAULT_MADAO_MODE,
|
||||
madaoRoutingPlanId: '',
|
||||
madaoProviderId: '',
|
||||
madaoCountry: '',
|
||||
madaoOperator: '',
|
||||
madaoAutoPickCountry: true,
|
||||
madaoReusePhone: true,
|
||||
madaoMinPrice: '',
|
||||
@@ -1846,6 +1854,9 @@ function normalizePhoneSmsProvider(value = '') {
|
||||
if (normalized === 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;
|
||||
}
|
||||
function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) {
|
||||
@@ -2244,6 +2255,13 @@ function normalizeMaDaoPrice(value = '') {
|
||||
return normalizeHeroSmsMaxPrice(value);
|
||||
}
|
||||
|
||||
function normalizeMaDaoOperator(value = '') {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/g, '');
|
||||
}
|
||||
|
||||
function normalizePhonePreferredActivation(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null;
|
||||
@@ -3594,6 +3612,8 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeMaDaoProviderId(value);
|
||||
case 'madaoCountry':
|
||||
return normalizeMaDaoCountry(value);
|
||||
case 'madaoOperator':
|
||||
return normalizeMaDaoOperator(value);
|
||||
case 'madaoAutoPickCountry':
|
||||
case 'madaoReusePhone':
|
||||
return Boolean(value);
|
||||
@@ -9971,21 +9991,67 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
gpcPageStatus: '',
|
||||
gpcPageStatusText: '',
|
||||
};
|
||||
const oauthRuntimeResets = {
|
||||
oauthUrl: null,
|
||||
cpaOAuthState: null,
|
||||
cpaManagementOrigin: null,
|
||||
sub2apiSessionId: null,
|
||||
sub2apiOAuthState: null,
|
||||
sub2apiGroupId: null,
|
||||
sub2apiGroupIds: [],
|
||||
sub2apiDraftName: null,
|
||||
sub2apiProxyId: null,
|
||||
codex2apiSessionId: 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,
|
||||
oauthUrl: null,
|
||||
cpaOAuthState: null,
|
||||
cpaManagementOrigin: null,
|
||||
sub2apiSessionId: null,
|
||||
sub2apiOAuthState: null,
|
||||
sub2apiGroupId: null,
|
||||
sub2apiGroupIds: [],
|
||||
sub2apiDraftName: null,
|
||||
sub2apiProxyId: null,
|
||||
codex2apiSessionId: null,
|
||||
codex2apiOAuthState: null,
|
||||
...oauthRuntimeResets,
|
||||
flowStartTime: null,
|
||||
password: null,
|
||||
lastEmailTimestamp: null,
|
||||
@@ -10056,6 +10122,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
if (isEarlyRegistrationNode || isBillingNode || isApprovalNode) {
|
||||
return {
|
||||
...(isEarlyRegistrationNode ? plusRuntimeResets : {}),
|
||||
...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}),
|
||||
...(isBillingNode ? {
|
||||
plusBillingCountryText: '',
|
||||
plusBillingAddress: null,
|
||||
@@ -10090,6 +10157,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
}
|
||||
if (stepKey === 'plus-checkout-return' || stepKey === 'confirm-oauth') {
|
||||
return {
|
||||
...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}),
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
plusReturnUrl: '',
|
||||
localhostUrl: null,
|
||||
@@ -10099,12 +10167,10 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
currentPhoneVerificationCountdownWindowTotal: 0,
|
||||
};
|
||||
}
|
||||
if (
|
||||
stepKey === 'oauth-login'
|
||||
|| stepKey === 'fetch-login-code'
|
||||
|| stepKey === 'relogin-bound-email'
|
||||
|| stepKey === 'fetch-bound-email-login-code'
|
||||
) {
|
||||
if (stepKey === 'oauth-login' || stepKey === 'relogin-bound-email') {
|
||||
return oauthEntryRuntimeResets;
|
||||
}
|
||||
if (stepKey === 'fetch-login-code' || stepKey === 'fetch-bound-email-login-code') {
|
||||
return {
|
||||
lastLoginCode: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
@@ -10124,6 +10190,9 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
localhostUrl: null,
|
||||
};
|
||||
}
|
||||
if (isOAuthEntryRestartBoundary) {
|
||||
return oauthEntryRuntimeResets;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -13827,8 +13896,6 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
createFiveSimProvider: self.PhoneSmsFiveSimProvider?.createProvider,
|
||||
createMaDaoProvider: self.PhoneSmsMaDaoProvider?.createProvider,
|
||||
});
|
||||
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
|
||||
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);
|
||||
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 normalizedMessage = String(errorMessage || '');
|
||||
if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) {
|
||||
@@ -15034,11 +15105,15 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
&& confirmOauthStep > 0
|
||||
&& confirmOauthStep < normalizedStep
|
||||
&& isPlatformVerifyTransientRetryError(errorMessage);
|
||||
const shouldRestartFromOAuthLoginStep = currentNodeKey === 'platform-verify'
|
||||
&& isPlatformVerifyOAuthSessionExpiredError(errorMessage);
|
||||
const restartAnchorStep = shouldRetryFromConfirmStep
|
||||
? confirmOauthStep
|
||||
: (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0
|
||||
: (shouldRestartFromOAuthLoginStep
|
||||
? authChainStartStep
|
||||
: (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0
|
||||
? boundEmailReloginStep
|
||||
: authChainStartStep);
|
||||
: authChainStartStep));
|
||||
if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
|
||||
return {
|
||||
shouldRestart: false,
|
||||
|
||||
@@ -74,6 +74,31 @@
|
||||
});
|
||||
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 = {}) {
|
||||
const resolvedSignupMethod = String(state?.resolvedSignupMethod || '').trim().toLowerCase();
|
||||
if (resolvedSignupMethod === 'phone' || resolvedSignupMethod === 'email') {
|
||||
@@ -703,6 +728,7 @@
|
||||
attemptRun,
|
||||
sessionId,
|
||||
}),
|
||||
...buildFreshAttemptIdentityResetPatch(),
|
||||
currentNodeId: '',
|
||||
nodeStatuses: buildFreshAttemptNodeStatuses(prevState),
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
|
||||
+350
-3373
File diff suppressed because it is too large
Load Diff
@@ -225,15 +225,20 @@
|
||||
const cardModeButton = 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 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 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 lines = String(rawText || '')
|
||||
.split(/\n+/)
|
||||
.map((line) => line.replace(/\s+/g, ' ').trim())
|
||||
const normalizeLogLine = (line = '') => String(line || '').replace(/\s+/g, ' ').trim();
|
||||
const ignoredLinePattern = /^(?:GPC Plus 充值|系统全自动出号|限量\d+份|自助购买卡密通道|实时进度|免费池|付费池|模式:|任务:|未开始|导出|充值模式|填写卡密|Access Token|开始 Plus 充值|停止当前任务|页面已就绪)$/i;
|
||||
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((line) => !/^(?:GPC Plus 充值|系统全自动出号|限量\d+份|自助购买卡密通道|实时进度|免费池|付费池|模式:|任务:|未开始|导出|充值模式|填写卡密|Access Token|开始 Plus 充值|停止当前任务|页面已就绪)$/i.test(line));
|
||||
return lines[lines.length - 1] || '';
|
||||
.filter((line) => !ignoredLinePattern.test(line));
|
||||
return entries[entries.length - 1] || '';
|
||||
};
|
||||
const lastLogLine = extractLastLogLine(logText) || extractLastLogLine(bodyText);
|
||||
const cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]'))
|
||||
@@ -266,11 +271,13 @@
|
||||
|
||||
function getGpcLastLogSummary(pageState = {}) {
|
||||
const rawText = String(pageState.lastLogLine || pageState.logText || pageState.bodyText || '');
|
||||
const lines = rawText
|
||||
.split(/\n+/)
|
||||
const timestampEntries = 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 : rawText.split(/\n+/))
|
||||
.map((line) => line.replace(/\s+/g, ' ').trim())
|
||||
.filter(Boolean);
|
||||
const line = lines[lines.length - 1] || '';
|
||||
const line = entries[entries.length - 1] || '';
|
||||
if (!line) {
|
||||
return '';
|
||||
}
|
||||
@@ -282,6 +289,18 @@
|
||||
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) {
|
||||
if (!chrome?.scripting?.executeScript) {
|
||||
throw new Error('步骤 7:当前运行环境不支持脚本注入,无法切换 GPC 卡密充值模式。');
|
||||
@@ -478,6 +497,7 @@
|
||||
while (Date.now() <= deadline) {
|
||||
throwIfStopped();
|
||||
const pageState = await inspectGpcPortalPage(tabId);
|
||||
pageState.noTrial = hasGpcNoTrialFailure(pageState);
|
||||
const buttonText = normalizeText(pageState.startButtonText);
|
||||
await collectStatusLog(pageState, buttonText);
|
||||
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
try {
|
||||
const rawCurrentState = {
|
||||
...(attempt === 1 ? state : await getState()),
|
||||
visibleStep: completionStep,
|
||||
...(resolvedIdentifierType === 'phone' ? {
|
||||
forceLoginIdentifierType: 'phone',
|
||||
forceEmailLogin: false,
|
||||
|
||||
@@ -231,6 +231,17 @@ const CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN = new RegExp([
|
||||
String.raw`\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8`,
|
||||
].join('|'), 'i');
|
||||
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 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const normalizedEmail = normalizeAuthAccountIdentifier(email);
|
||||
if (!normalizedEmail || !normalizedEmail.includes('@')) {
|
||||
@@ -3180,22 +3232,90 @@ function findChooseAccountButtonForEmail(email) {
|
||||
});
|
||||
|
||||
for (const node of emailNodes) {
|
||||
let current = node;
|
||||
while (current && current !== document.body) {
|
||||
const target = resolveChooseAccountClickTarget(current);
|
||||
if (target) {
|
||||
const targetText = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(target));
|
||||
if (targetText.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(targetText)) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
current = current.parentElement;
|
||||
const target = resolveChooseAccountCardTarget(node, normalizedEmail);
|
||||
if (target) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
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) {
|
||||
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, {
|
||||
message: `OpenAI choose-account page does not contain target email ${email}.`,
|
||||
});
|
||||
|
||||
@@ -199,6 +199,23 @@
|
||||
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) {
|
||||
const digits = normalizePhoneDigits(value);
|
||||
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||
@@ -364,15 +381,17 @@
|
||||
|
||||
const byLabel = findCountryOptionByLabel(countryLabel);
|
||||
if (await trySelectCountryOption(select, byLabel)) {
|
||||
return true;
|
||||
if (selectedCountryMatchesPhoneNumber(phoneNumber)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber);
|
||||
if (await trySelectCountryOption(select, byPhoneNumber)) {
|
||||
return true;
|
||||
return selectedCountryMatchesPhoneNumber(phoneNumber);
|
||||
}
|
||||
|
||||
return Boolean(getSelectedCountryOption());
|
||||
return selectedCountryMatchesPhoneNumber(phoneNumber);
|
||||
}
|
||||
|
||||
function getAddPhoneSubmitButton() {
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "FlowPilot",
|
||||
"version": "2.5",
|
||||
"version_name": "FlowPilot2.5",
|
||||
"version": "2.6",
|
||||
"version_name": "FlowPilot2.6",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
|
||||
@@ -150,6 +150,10 @@
|
||||
return String(value || '').trim() || fallback;
|
||||
}
|
||||
|
||||
function normalizeCountryKey(value) {
|
||||
return normalizeFiveSimCountryId(value, '');
|
||||
}
|
||||
|
||||
function formatFiveSimCountryLabel(id = '', englishValue = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||||
const countryId = normalizeFiveSimCountryId(id, '');
|
||||
const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback);
|
||||
@@ -181,6 +185,42 @@
|
||||
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 = []) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
@@ -371,6 +411,20 @@
|
||||
}
|
||||
|
||||
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 fallbackList = normalizeFiveSimCountryFallback(state.fiveSimCountryFallback);
|
||||
const seen = new Set([primary.id]);
|
||||
@@ -391,6 +445,13 @@
|
||||
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 = {}) {
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, '/v1/user/profile', {
|
||||
@@ -496,6 +557,8 @@
|
||||
async function resolvePricePlan(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||
const userLimitText = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
||||
const userLimit = userLimitText ? Number(userLimitText) : null;
|
||||
const userMinText = normalizeFiveSimMaxPrice(state.fiveSimMinPrice);
|
||||
const userMinLimit = userMinText ? Number(userMinText) : null;
|
||||
let priceCandidates = [];
|
||||
|
||||
try {
|
||||
@@ -526,19 +589,24 @@
|
||||
priceCandidates = buildSortedUniquePriceCandidates(priceCandidates);
|
||||
|
||||
const minCatalogPrice = priceCandidates.length > 0 ? priceCandidates[0] : null;
|
||||
const rangeFilteredPrices = priceCandidates.filter((price) => (
|
||||
(userMinLimit === null || price >= userMinLimit)
|
||||
&& (userLimit === null || price <= userLimit)
|
||||
));
|
||||
if (userLimit !== null) {
|
||||
const bounded = priceCandidates.filter((price) => price <= userLimit);
|
||||
const bounded = rangeFilteredPrices;
|
||||
return {
|
||||
prices: bounded.length > 0 ? [userLimit, ...bounded.filter((price) => price !== userLimit)] : [userLimit],
|
||||
userLimit,
|
||||
userMinLimit,
|
||||
minCatalogPrice,
|
||||
};
|
||||
}
|
||||
|
||||
if (priceCandidates.length > 0) {
|
||||
return { prices: priceCandidates, userLimit: null, minCatalogPrice };
|
||||
if (rangeFilteredPrices.length > 0) {
|
||||
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 = {}) {
|
||||
@@ -558,6 +626,7 @@
|
||||
provider: PROVIDER_ID,
|
||||
serviceCode: DEFAULT_PRODUCT,
|
||||
countryId,
|
||||
countryCode: countryId,
|
||||
countryLabel,
|
||||
successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)),
|
||||
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) {
|
||||
const text = describePayload(payloadOrMessage);
|
||||
return /no\s+free\s+phones|no\s+numbers|not\s+found/i.test(text);
|
||||
@@ -594,10 +697,17 @@
|
||||
|
||||
function assertMaxPriceCompatibleWithOperator(state = {}) {
|
||||
const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
||||
const minPrice = normalizeFiveSimMaxPrice(state.fiveSimMinPrice);
|
||||
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||
if (maxPrice && operator !== DEFAULT_OPERATOR) {
|
||||
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 = {}) {
|
||||
@@ -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 = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) return '';
|
||||
@@ -807,6 +933,25 @@
|
||||
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) {
|
||||
const trimmed = String(rawCodeOrText || '').trim();
|
||||
if (!trimmed) {
|
||||
@@ -913,9 +1058,18 @@
|
||||
addLog: deps.addLog,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
const capabilities = Object.freeze({
|
||||
supportsReusableActivation: true,
|
||||
supportsAutomaticFreeReuse: true,
|
||||
supportsFreeReusePreservation: true,
|
||||
supportsPageResend: false,
|
||||
supportsPageResendProbe: true,
|
||||
requiresCountrySelection: true,
|
||||
});
|
||||
return {
|
||||
id: PROVIDER_ID,
|
||||
label: '5sim',
|
||||
capabilities,
|
||||
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||||
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||||
supportedCountries: SUPPORTED_COUNTRY_ITEMS,
|
||||
@@ -925,18 +1079,38 @@
|
||||
normalizeCountryLabel: normalizeFiveSimCountryLabel,
|
||||
formatCountryLabel: formatFiveSimCountryLabel,
|
||||
normalizeCountryFallback: normalizeFiveSimCountryFallback,
|
||||
normalizeCountryKey,
|
||||
normalizeMaxPrice: normalizeFiveSimMaxPrice,
|
||||
normalizeOperator: normalizeFiveSimOperator,
|
||||
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: (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),
|
||||
fetchCountries: (state) => fetchCountries(state, providerDeps),
|
||||
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
|
||||
resolvePriceRange,
|
||||
formatPriceRangeText,
|
||||
collectPriceEntries,
|
||||
describePayload,
|
||||
};
|
||||
@@ -954,8 +1128,13 @@
|
||||
normalizeFiveSimCountryFallback,
|
||||
normalizeFiveSimCountryId,
|
||||
normalizeFiveSimCountryLabel,
|
||||
normalizeCountryKey,
|
||||
formatFiveSimCountryLabel,
|
||||
normalizeFiveSimMaxPrice,
|
||||
normalizeFiveSimOperator,
|
||||
resolvePriceRange,
|
||||
formatPriceRangeText,
|
||||
normalizeActivation,
|
||||
resolveActivationCountry,
|
||||
};
|
||||
});
|
||||
|
||||
+1300
-5
File diff suppressed because it is too large
Load Diff
+283
-11
@@ -7,6 +7,19 @@
|
||||
const DEFAULT_SERVICE = 'openai';
|
||||
const DEFAULT_MODE = 'routing_plan';
|
||||
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 = '') {
|
||||
return String(value || '').trim() || fallback;
|
||||
@@ -29,6 +42,15 @@
|
||||
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 = '') {
|
||||
const trimmed = normalizeText(value);
|
||||
if (!trimmed) {
|
||||
@@ -44,6 +66,52 @@
|
||||
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) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return Boolean(fallback);
|
||||
@@ -196,6 +264,7 @@
|
||||
service: normalizeText(options?.service || state?.madaoServiceName, DEFAULT_SERVICE),
|
||||
};
|
||||
const country = normalizeCountry(options?.country || state?.madaoCountry);
|
||||
const operator = normalizeOperator(options?.operator || state?.madaoOperator);
|
||||
const minPrice = normalizePrice(options?.minPrice ?? state?.madaoMinPrice);
|
||||
const maxPrice = normalizePrice(options?.maxPrice ?? state?.madaoMaxPrice);
|
||||
|
||||
@@ -209,6 +278,9 @@
|
||||
if (country) {
|
||||
request.country = country;
|
||||
}
|
||||
if (operator) {
|
||||
request.metadata = { operator };
|
||||
}
|
||||
if (minPrice !== null) {
|
||||
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 = '') {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) {
|
||||
@@ -315,18 +449,75 @@
|
||||
}
|
||||
|
||||
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
|
||||
const payload = await pollActivation(state, activation, deps);
|
||||
const code = extractCodeFromPollPayload(payload);
|
||||
if (code) {
|
||||
return code;
|
||||
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 code = extractCodeFromPollPayload(payload);
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
if (typeof options.onStatus === 'function') {
|
||||
await options.onStatus({
|
||||
activation,
|
||||
statusText: describePayload(payload) || 'PENDING',
|
||||
});
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (typeof options.onStatus === 'function') {
|
||||
await options.onStatus({
|
||||
activation,
|
||||
statusText: describePayload(payload) || 'PENDING',
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
return '';
|
||||
throw new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${lastResponse ? ` MaDao 最后状态:${lastResponse}` : ''}`);
|
||||
}
|
||||
|
||||
async function releaseActivation(state = {}, activation, action = 'cancel', deps = {}) {
|
||||
@@ -366,7 +557,6 @@
|
||||
routing_plan_id: activation?.madaoRoutingPlanId,
|
||||
routing_plan_name: activation?.madaoRoutingPlanName,
|
||||
service: activation?.serviceCode,
|
||||
country: activation?.countryId,
|
||||
});
|
||||
if (!nextTicket) {
|
||||
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 = {}) {
|
||||
const capabilities = Object.freeze({
|
||||
supportsReusableActivation: false,
|
||||
supportsAutomaticFreeReuse: false,
|
||||
supportsFreeReusePreservation: false,
|
||||
supportsPageResend: false,
|
||||
supportsPageResendProbe: false,
|
||||
supportsRouteReplace: true,
|
||||
requiresCountrySelection: false,
|
||||
});
|
||||
return {
|
||||
id: PROVIDER_ID,
|
||||
label: 'MaDao',
|
||||
capabilities,
|
||||
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, {
|
||||
...deps,
|
||||
...runtimeDeps,
|
||||
}),
|
||||
reuseActivation,
|
||||
pollActivation: (state, activation, runtimeDeps = {}) => pollActivation(state, activation, {
|
||||
...deps,
|
||||
...runtimeDeps,
|
||||
@@ -420,10 +657,32 @@
|
||||
...deps,
|
||||
...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, {
|
||||
...deps,
|
||||
...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, {
|
||||
...deps,
|
||||
...runtimeDeps,
|
||||
@@ -432,7 +691,9 @@
|
||||
extractCodeFromPollPayload,
|
||||
mapAcquirePath,
|
||||
mapTicketStatus,
|
||||
normalizeActivation,
|
||||
normalizeActivationFromAcquire,
|
||||
resolveCountryCandidates,
|
||||
resolveConfig: (state = {}, runtimeDeps = {}) => resolveConfig(state, {
|
||||
...deps,
|
||||
...runtimeDeps,
|
||||
@@ -451,12 +712,23 @@
|
||||
extractCodeFromPollPayload,
|
||||
mapAcquirePath,
|
||||
mapTicketStatus,
|
||||
normalizeActivation,
|
||||
normalizeActivationFromAcquire,
|
||||
normalizeCountry,
|
||||
normalizeCountryKey,
|
||||
normalizeCountryLabel,
|
||||
normalizeOperator,
|
||||
resolveActivationCountry,
|
||||
pollActivation,
|
||||
pollActivationCode,
|
||||
releaseActivation,
|
||||
finishActivation,
|
||||
cancelActivation,
|
||||
banActivation,
|
||||
replaceRoutingActivation,
|
||||
requestAdditionalSms,
|
||||
resolveConfig,
|
||||
resolveCountryCandidates,
|
||||
rotateActivation,
|
||||
};
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,14 @@
|
||||
const PROVIDER_FIVE_SIM = '5sim';
|
||||
const PROVIDER_NEXSMS = 'nexsms';
|
||||
const PROVIDER_MADAO = 'madao';
|
||||
const PROVIDER_SMS_BOWER = 'sms-bower';
|
||||
const DEFAULT_PROVIDER = PROVIDER_HERO_SMS;
|
||||
const DEFAULT_PROVIDER_ORDER = Object.freeze([
|
||||
PROVIDER_HERO_SMS,
|
||||
PROVIDER_FIVE_SIM,
|
||||
PROVIDER_NEXSMS,
|
||||
PROVIDER_MADAO,
|
||||
PROVIDER_SMS_BOWER,
|
||||
]);
|
||||
const PROVIDER_DEFINITIONS = Object.freeze({
|
||||
[PROVIDER_HERO_SMS]: Object.freeze({
|
||||
@@ -34,6 +36,11 @@
|
||||
label: 'MaDao',
|
||||
moduleKey: 'PhoneSmsMaDaoProvider',
|
||||
}),
|
||||
[PROVIDER_SMS_BOWER]: Object.freeze({
|
||||
id: PROVIDER_SMS_BOWER,
|
||||
label: 'SMS Bower',
|
||||
moduleKey: 'PhoneSmsBowerProvider',
|
||||
}),
|
||||
});
|
||||
|
||||
function resolveProviderKey(value = '') {
|
||||
@@ -133,6 +140,7 @@
|
||||
PROVIDER_FIVE_SIM,
|
||||
PROVIDER_NEXSMS,
|
||||
PROVIDER_MADAO,
|
||||
PROVIDER_SMS_BOWER,
|
||||
DEFAULT_PROVIDER,
|
||||
DEFAULT_PROVIDER_ORDER,
|
||||
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,
|
||||
};
|
||||
});
|
||||
+45
-34
@@ -1367,6 +1367,7 @@
|
||||
<option value="5sim">5sim</option>
|
||||
<option value="nexsms">NexSMS</option>
|
||||
<option value="madao">MaDao</option>
|
||||
<option value="sms-bower">SMS Bower</option>
|
||||
</select>
|
||||
</div>
|
||||
<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="nexsms" selected>NexSMS</option>
|
||||
<option value="madao" selected>MaDao</option>
|
||||
<option value="sms-bower" selected>SMS Bower</option>
|
||||
</select>
|
||||
<div class="hero-sms-country-mainline">
|
||||
<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"
|
||||
placeholder="ot" />
|
||||
</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;">
|
||||
<span class="data-label">MaDao 地址</span>
|
||||
<input type="text" id="input-madao-base-url" class="data-input mono"
|
||||
@@ -1553,46 +1565,43 @@
|
||||
<span class="data-label">接入模式</span>
|
||||
<select id="select-madao-mode" class="data-select mono">
|
||||
<option value="routing_plan">路由计划</option>
|
||||
<option value="direct">直连参数</option>
|
||||
<option value="direct">直连模式</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-madao-routing-plan-id" style="display:none;">
|
||||
<span class="data-label">路由计划</span>
|
||||
<input type="text" id="input-madao-routing-plan-id" class="data-input mono"
|
||||
placeholder="routing_plan_id" />
|
||||
</div>
|
||||
<div class="data-row" id="row-madao-provider-id" style="display:none;">
|
||||
<span class="data-label">直连平台</span>
|
||||
<input type="text" id="input-madao-provider-id" class="data-input mono"
|
||||
placeholder="auto" />
|
||||
</div>
|
||||
<div class="data-row" id="row-madao-country" style="display:none;">
|
||||
<span class="data-label">直连国家</span>
|
||||
<input type="text" id="input-madao-country" class="data-input mono"
|
||||
placeholder="local / TH / any" />
|
||||
</div>
|
||||
<div class="data-row" id="row-madao-auto-pick-country" style="display:none;">
|
||||
<span class="data-label">自动选国家</span>
|
||||
<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 class="data-inline data-value-actions">
|
||||
<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 class="data-row" id="row-madao-reuse-phone" style="display:none;">
|
||||
<span class="data-label">MaDao 复用</span>
|
||||
<div class="data-inline">
|
||||
<label class="toggle-switch" for="input-madao-reuse-phone">
|
||||
<input type="checkbox" id="input-madao-reuse-phone" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
<span class="data-value">允许 MaDao 复用符合条件的号码</span>
|
||||
<div class="data-row" id="row-madao-provider-id" style="display:none;">
|
||||
<span class="data-label">MaDao 服务商</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<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 class="data-row" id="row-madao-country" style="display:none;">
|
||||
<span class="data-label">MaDao 国家</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<select id="select-madao-country" class="data-select mono">
|
||||
<option value="">请先选择服务商</option>
|
||||
</select>
|
||||
<button id="btn-madao-refresh-countries" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-madao-operator" style="display:none;">
|
||||
<span class="data-label">MaDao 线路</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<select id="select-madao-operator" class="data-select mono">
|
||||
<option value="">任意线路</option>
|
||||
</select>
|
||||
<button id="btn-madao-refresh-operators" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-madao-price-range" style="display:none;">
|
||||
@@ -1896,7 +1905,9 @@
|
||||
<script src="../gopay-utils.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/nexsms.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="../icloud-utils.js"></script>
|
||||
<script src="../mail-provider-utils.js"></script>
|
||||
|
||||
+730
-61
File diff suppressed because it is too large
Load Diff
@@ -149,6 +149,23 @@ let currentState = {
|
||||
inbucketMailbox: '',
|
||||
cloudflareDomain: '',
|
||||
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: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
@@ -280,6 +297,52 @@ async function runAutoSequenceFromStep() {
|
||||
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,
|
||||
stepStatuses: {
|
||||
|
||||
@@ -159,6 +159,7 @@ const bundle = [
|
||||
extractFunction('startAutoRunNodeIdleLogWatchdog'),
|
||||
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('getDownstreamStateResets'),
|
||||
extractFunction('getPostStep6AutoRestartDecision'),
|
||||
NODE_COMPAT_HELPERS,
|
||||
extractFunction('getAutoRunWorkflowNodeIds'),
|
||||
@@ -279,6 +280,7 @@ const events = {
|
||||
steps: [],
|
||||
logs: [],
|
||||
invalidations: [],
|
||||
stateUpdates: [],
|
||||
cancellations: [],
|
||||
stopBroadcasts: 0,
|
||||
};
|
||||
@@ -330,6 +332,10 @@ async function getTabId() {
|
||||
}
|
||||
async function invalidateDownstreamAfterStepRestart(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 = '') {
|
||||
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)));
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
|
||||
@@ -71,6 +71,7 @@ test('background account history settings are normalized independently from hotm
|
||||
extractFunction('normalizeMaDaoIdentifier'),
|
||||
extractFunction('normalizeMaDaoProviderId'),
|
||||
extractFunction('normalizeMaDaoCountry'),
|
||||
extractFunction('normalizeMaDaoOperator'),
|
||||
extractFunction('normalizeMaDaoPrice'),
|
||||
extractFunction('normalizePhonePreferredActivation'),
|
||||
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_NEXSMS = 'nexsms';
|
||||
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_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
||||
const DEFAULT_MADAO_MODE = 'routing_plan';
|
||||
@@ -382,6 +384,7 @@ return {
|
||||
assert.equal(api.normalizePersistentSettingValue('madaoProviderId', ' Upstream A! '), 'upstreama');
|
||||
assert.equal(api.normalizePersistentSettingValue('madaoCountry', ' gb '), 'GB');
|
||||
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('madaoReusePhone', 0), false);
|
||||
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 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', () => {
|
||||
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, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/);
|
||||
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 () => {
|
||||
|
||||
@@ -419,6 +419,7 @@ return {
|
||||
madaoRoutingPlanId: 'rp-openai',
|
||||
madaoProviderId: 'upstream-a',
|
||||
madaoCountry: 'GB',
|
||||
madaoOperator: 'operator-a',
|
||||
madaoAutoPickCountry: false,
|
||||
madaoReusePhone: true,
|
||||
madaoMinPrice: '0.12',
|
||||
@@ -452,6 +453,7 @@ return {
|
||||
assert.equal(api.getPayloadInput().madaoRoutingPlanId, 'rp-openai');
|
||||
assert.equal(api.getPayloadInput().madaoProviderId, 'upstream-a');
|
||||
assert.equal(api.getPayloadInput().madaoCountry, 'GB');
|
||||
assert.equal(api.getPayloadInput().madaoOperator, 'operator-a');
|
||||
assert.equal(api.getPayloadInput().madaoAutoPickCountry, false);
|
||||
assert.equal(api.getPayloadInput().madaoReusePhone, true);
|
||||
assert.equal(api.getPayloadInput().madaoMinPrice, '0.12');
|
||||
|
||||
@@ -118,6 +118,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
madaoRoutingPlanId: '',
|
||||
madaoProviderId: '',
|
||||
madaoCountry: '',
|
||||
madaoOperator: '',
|
||||
madaoAutoPickCountry: true,
|
||||
madaoReusePhone: true,
|
||||
madaoMinPrice: '',
|
||||
@@ -219,6 +220,7 @@ function normalizeMaDaoBaseUrl(value = '') {
|
||||
function normalizeMaDaoMode(value = '') { return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : DEFAULT_MADAO_MODE; }
|
||||
function normalizeMaDaoIdentifier(value = '') { return String(value || '').trim(); }
|
||||
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 = '') {
|
||||
const trimmed = String(value || '').trim();
|
||||
if (!trimmed) return '';
|
||||
@@ -306,6 +308,7 @@ test('buildPersistentSettingsPayload writes canonical settings schema into persi
|
||||
assert.equal(payload.phoneSmsProvider, 'hero-sms');
|
||||
assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST);
|
||||
assert.equal(payload.madaoMode, DEFAULT_MADAO_MODE_FOR_TEST);
|
||||
assert.equal(payload.madaoOperator, '');
|
||||
assert.equal(payload.madaoAutoPickCountry, true);
|
||||
assert.equal(payload.madaoReusePhone, true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
|
||||
@@ -724,6 +727,7 @@ test('buildPersistentSettingsPayload persists normalized MaDao flat settings out
|
||||
madaoRoutingPlanId: ' rp-openai ',
|
||||
madaoProviderId: ' Upstream A! ',
|
||||
madaoCountry: ' gb ',
|
||||
madaoOperator: ' Operator A! ',
|
||||
madaoAutoPickCountry: 0,
|
||||
madaoReusePhone: 1,
|
||||
madaoMinPrice: '0.123456',
|
||||
@@ -737,6 +741,7 @@ test('buildPersistentSettingsPayload persists normalized MaDao flat settings out
|
||||
assert.equal(payload.madaoRoutingPlanId, 'rp-openai');
|
||||
assert.equal(payload.madaoProviderId, 'upstreama');
|
||||
assert.equal(payload.madaoCountry, 'GB');
|
||||
assert.equal(payload.madaoOperator, 'operatora');
|
||||
assert.equal(payload.madaoAutoPickCountry, false);
|
||||
assert.equal(payload.madaoReusePhone, true);
|
||||
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);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const source = fs.readFileSync('flows/openai/background/steps/oauth-login.js', 'utf8');
|
||||
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',
|
||||
madaoProviderId: 'Upstream A!',
|
||||
madaoCountry: 'gb',
|
||||
madaoOperator: 'Operator A!',
|
||||
madaoAutoPickCountry: 'false',
|
||||
madaoReusePhone: '1',
|
||||
madaoMinPrice: '0.01',
|
||||
@@ -54,6 +55,9 @@ test('MaDao direct acquire sends only direct fields and normalizes activation da
|
||||
auto_pick_country: false,
|
||||
reuse_phone: true,
|
||||
country: 'GB',
|
||||
metadata: {
|
||||
operator: 'operatora',
|
||||
},
|
||||
min_price: 0.01,
|
||||
max_price: 0.2,
|
||||
});
|
||||
@@ -100,6 +104,37 @@ test('MaDao routing acquire sends routing plan only', async () => {
|
||||
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 () => {
|
||||
const statusEvents = [];
|
||||
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 () => {
|
||||
const originalDocument = global.document;
|
||||
const originalLocation = global.location;
|
||||
|
||||
@@ -3,6 +3,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
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 = {}) {
|
||||
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: {
|
||||
createProvider: (deps = {}) => ({ provider: '5sim', deps }),
|
||||
},
|
||||
PhoneSmsNexSmsProvider: {
|
||||
createProvider: (deps = {}) => ({ provider: 'nexsms', deps }),
|
||||
},
|
||||
PhoneSmsMaDaoProvider: {
|
||||
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(' MaDao '), 'madao');
|
||||
assert.equal(registry.normalizeProviderId('unknown-provider'), 'hero-sms');
|
||||
assert.equal(registry.getProviderLabel('nexsms'), 'NexSMS');
|
||||
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('madao').moduleKey, 'PhoneSmsMaDaoProvider');
|
||||
assert.equal(registry.getProviderDefinition('sms-bower').moduleKey, 'PhoneSmsBowerProvider');
|
||||
assert.deepStrictEqual(
|
||||
registry.normalizeProviderOrder([
|
||||
{ provider: 'madao' },
|
||||
{ provider: 'sms-bower' },
|
||||
{ provider: 'nexsms' },
|
||||
{ id: '5sim' },
|
||||
{ value: 'hero-sms' },
|
||||
'MADAO',
|
||||
'NEXSMS',
|
||||
]),
|
||||
['madao', 'nexsms', '5sim', 'hero-sms']
|
||||
['madao', 'sms-bower', 'nexsms', '5sim', 'hero-sms']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
registry.normalizeProviderOrder([], ['madao', 'nexsms', '5sim', 'nexsms']),
|
||||
['madao', 'nexsms', '5sim']
|
||||
registry.normalizeProviderOrder([], ['madao', 'sms-bower', 'nexsms', '5sim', 'nexsms']),
|
||||
['madao', 'sms-bower', 'nexsms', '5sim']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
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 }),
|
||||
{ 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 source = fs.readFileSync('background/phone-verification-flow.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
|
||||
const heroSmsSource = fs.readFileSync('phone-sms/providers/hero-sms.js', 'utf8');
|
||||
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 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 } = {}) {
|
||||
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');
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const requests = [];
|
||||
let currentState = {
|
||||
@@ -172,7 +285,6 @@ test('phone verification helper acquires, polls and releases MaDao activation th
|
||||
};
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
createMaDaoProvider: maDaoModule.createProvider,
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
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' });
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const setStateCalls = [];
|
||||
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 () => {
|
||||
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
||||
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
|
||||
let checkCount = 0;
|
||||
let pageStateReads = 0;
|
||||
const contentMessages = [];
|
||||
@@ -1005,7 +1270,6 @@ test('signup phone helper fails stale email-verification on 5sim RECEIVED withou
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
createFiveSimProvider: fiveSimModule.createProvider,
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
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,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(activation, {
|
||||
activationId: '9876543',
|
||||
phoneNumber: '+447911123456',
|
||||
provider: '5sim',
|
||||
serviceCode: 'openai',
|
||||
countryId: 'england',
|
||||
countryCode: 'england',
|
||||
countryLabel: 'England',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
});
|
||||
assert.equal(requests.length, 4);
|
||||
assert.equal(requests[0].pathname, '/v1/guest/prices');
|
||||
assert.equal(requests[0].search.get('country'), 'thailand');
|
||||
assert.equal(requests[0].search.get('product'), 'openai');
|
||||
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||
assert.equal(requests[1].search.get('maxPrice'), '0.08');
|
||||
assert.equal(requests[1].search.get('reuse'), '1');
|
||||
assert.equal(requests[1].headers.Authorization, 'Bearer five-token');
|
||||
assert.equal(requests[2].pathname, '/v1/guest/prices');
|
||||
assert.equal(requests[2].search.get('country'), 'england');
|
||||
assert.equal(requests[2].search.get('product'), 'openai');
|
||||
assert.equal(requests[3].pathname, '/v1/user/buy/activation/england/any/openai');
|
||||
assert.equal(activation.activationId, '9876543');
|
||||
assert.equal(activation.phoneNumber, '+447911123456');
|
||||
assert.equal(activation.provider, '5sim');
|
||||
assert.equal(activation.serviceCode, 'openai');
|
||||
assert.equal(activation.countryId, 'england');
|
||||
assert.equal(activation.countryCode, 'england');
|
||||
assert.equal(activation.successfulUses, 0);
|
||||
assert.equal(activation.maxUses, 3);
|
||||
assert.equal(requests.length, 6);
|
||||
assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
|
||||
assert.equal(requests[1].pathname, '/v1/guest/prices');
|
||||
assert.equal(requests[1].search.get('country'), 'thailand');
|
||||
assert.equal(requests[1].search.get('product'), 'openai');
|
||||
assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||
assert.equal(requests[2].search.get('maxPrice'), '0.08');
|
||||
assert.equal(requests[2].search.get('reuse'), '1');
|
||||
assert.equal(requests[2].headers.Authorization, 'Bearer five-token');
|
||||
assert.equal(requests[3].pathname, '/v1/guest/products/england/any');
|
||||
assert.equal(requests[4].pathname, '/v1/guest/prices');
|
||||
assert.equal(requests[4].search.get('country'), 'england');
|
||||
assert.equal(requests[4].search.get('product'), 'openai');
|
||||
assert.equal(requests[5].pathname, '/v1/user/buy/activation/england/any/openai');
|
||||
});
|
||||
|
||||
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(
|
||||
fiveSimRequests.map((entry) => entry.url.pathname),
|
||||
[
|
||||
'/v1/guest/products/vietnam/any',
|
||||
'/v1/guest/prices',
|
||||
'/v1/user/buy/activation/vietnam/any/openai',
|
||||
'/v1/user/check/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 () => {
|
||||
@@ -2593,20 +2857,18 @@ test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSms
|
||||
heroSmsActivationRetryRounds: 1,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(activation, {
|
||||
activationId: '1234567',
|
||||
phoneNumber: '+66880000000',
|
||||
provider: '5sim',
|
||||
serviceCode: 'openai',
|
||||
countryId: 'thailand',
|
||||
countryCode: 'thailand',
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
});
|
||||
assert.equal(requests[0].pathname, '/v1/guest/prices');
|
||||
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||
assert.equal(requests[1].search.get('reuse'), null);
|
||||
assert.equal(activation.activationId, '1234567');
|
||||
assert.equal(activation.phoneNumber, '+66880000000');
|
||||
assert.equal(activation.provider, '5sim');
|
||||
assert.equal(activation.serviceCode, 'openai');
|
||||
assert.equal(activation.countryId, 'thailand');
|
||||
assert.equal(activation.countryCode, 'thailand');
|
||||
assert.equal(activation.successfulUses, 0);
|
||||
assert.equal(activation.maxUses, 3);
|
||||
assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
|
||||
assert.equal(requests[1].pathname, '/v1/guest/prices');
|
||||
assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||
assert.equal(requests[2].search.get('reuse'), null);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(activation, {
|
||||
activationId: '1234568',
|
||||
phoneNumber: '+66880000001',
|
||||
provider: '5sim',
|
||||
serviceCode: 'openai',
|
||||
countryId: 'thailand',
|
||||
countryCode: 'thailand',
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
});
|
||||
assert.equal(requests[0].pathname, '/v1/guest/prices');
|
||||
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||
assert.equal(requests[1].search.get('reuse'), '1');
|
||||
assert.equal(activation.activationId, '1234568');
|
||||
assert.equal(activation.phoneNumber, '+66880000001');
|
||||
assert.equal(activation.provider, '5sim');
|
||||
assert.equal(activation.serviceCode, 'openai');
|
||||
assert.equal(activation.countryId, 'thailand');
|
||||
assert.equal(activation.countryCode, 'thailand');
|
||||
assert.equal(activation.successfulUses, 0);
|
||||
assert.equal(activation.maxUses, 3);
|
||||
assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
|
||||
assert.equal(requests[1].pathname, '/v1/guest/prices');
|
||||
assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
|
||||
assert.equal(requests[2].search.get('reuse'), '1');
|
||||
});
|
||||
|
||||
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(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 () => {
|
||||
@@ -3242,18 +3502,16 @@ test('phone verification helper reuses 5sim by keeping the original activation',
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(nextActivation, {
|
||||
activationId: '600001',
|
||||
phoneNumber: '+44 7911-123-456',
|
||||
provider: '5sim',
|
||||
serviceCode: 'openai',
|
||||
countryId: 'england',
|
||||
countryCode: 'england',
|
||||
successfulUses: 0,
|
||||
maxUses: 1,
|
||||
source: '5sim-retained-reuse',
|
||||
ignoredPhoneCodeKeys: ['old::111111'],
|
||||
});
|
||||
assert.equal(nextActivation.activationId, '600001');
|
||||
assert.equal(nextActivation.phoneNumber, '+44 7911-123-456');
|
||||
assert.equal(nextActivation.provider, '5sim');
|
||||
assert.equal(nextActivation.serviceCode, 'openai');
|
||||
assert.equal(nextActivation.countryId, 'england');
|
||||
assert.equal(nextActivation.countryCode, 'england');
|
||||
assert.equal(nextActivation.successfulUses, 0);
|
||||
assert.equal(nextActivation.maxUses, 1);
|
||||
assert.equal(nextActivation.source, '5sim-retained-reuse');
|
||||
assert.deepStrictEqual(nextActivation.ignoredPhoneCodeKeys, ['old::111111']);
|
||||
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({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
createFiveSimProvider: fiveSimModule.createProvider,
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
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({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
createFiveSimProvider: fiveSimModule.createProvider,
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
@@ -8724,12 +8976,9 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
|
||||
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({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
createFiveSimProvider: fiveSimModule.createProvider,
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsedUrl = new URL(url);
|
||||
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({
|
||||
addLog: async () => {},
|
||||
createMaDaoProvider: maDaoModule.createProvider,
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsedUrl = new URL(url);
|
||||
@@ -8966,6 +9214,7 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
|
||||
madaoRoutingPlanId: 'rp-stale',
|
||||
madaoProviderId: 'upstream-a',
|
||||
madaoCountry: 'gb',
|
||||
madaoOperator: 'operator-a',
|
||||
madaoAutoPickCountry: false,
|
||||
madaoReusePhone: true,
|
||||
madaoMinPrice: '0.01',
|
||||
@@ -8981,7 +9230,6 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
|
||||
};
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
createMaDaoProvider: maDaoModule.createProvider,
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsedUrl = new URL(url);
|
||||
@@ -9076,6 +9324,9 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
|
||||
auto_pick_country: false,
|
||||
reuse_phone: true,
|
||||
country: 'GB',
|
||||
metadata: {
|
||||
operator: 'operator-a',
|
||||
},
|
||||
min_price: 0.01,
|
||||
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({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
createFiveSimProvider: fiveSimModule.createProvider,
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
|
||||
@@ -1058,7 +1058,10 @@ test('GPC billing restarts when start button returns without subscription done',
|
||||
const { events, executor, pageHarness } = createGpcPageExecutorHarness([
|
||||
{ startButtonText: '开始 Plus 充值', logText: 'SYSTEM 页面已就绪' },
|
||||
{ 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: '开始 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(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);
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
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(
|
||||
@@ -1108,13 +1112,38 @@ test('GPC billing fails current round without restart when account has no trial
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
plusCheckoutTabId: 77,
|
||||
}),
|
||||
/PLUS_CHECKOUT_NON_FREE_TRIAL::.*该账户没有试用资格.*最近日志:该账户没有试用资格/
|
||||
/PLUS_CHECKOUT_NON_FREE_TRIAL::.*该账户没有试用资格.*最近日志:\[02:20:09\] ERROR 该账户没有试用资格/
|
||||
);
|
||||
|
||||
assert.equal(pageHarness.clicks.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 () => {
|
||||
const { executor, pageHarness } = createGpcPageExecutorHarness([
|
||||
{ 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="select-madao-mode"/);
|
||||
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="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="input-madao-country"/);
|
||||
assert.match(html, /id="row-madao-auto-pick-country"/);
|
||||
assert.match(html, /id="input-madao-auto-pick-country"/);
|
||||
assert.match(html, /id="row-madao-reuse-phone"/);
|
||||
assert.match(html, /id="input-madao-reuse-phone"/);
|
||||
assert.match(html, /id="select-madao-country"/);
|
||||
assert.match(html, /id="btn-madao-refresh-countries"/);
|
||||
assert.doesNotMatch(html, /id="input-madao-country"/);
|
||||
assert.match(html, /id="row-madao-operator"/);
|
||||
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="input-madao-min-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) 国家列表失败:'/);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const api = new Function(`
|
||||
${extractFunction('normalizeHeroSmsCountryPayloadEntries')}
|
||||
@@ -698,12 +933,14 @@ const rowNexSmsApiKey = { style: { display: 'none' } };
|
||||
const rowNexSmsCountry = { style: { display: 'none' } };
|
||||
const rowNexSmsCountryFallback = { style: { display: 'none' } };
|
||||
const rowNexSmsServiceCode = { style: { display: 'none' } };
|
||||
const rowSmsBowerApiKey = { style: { display: 'none' } };
|
||||
const rowMaDaoBaseUrl = { style: { display: 'none' } };
|
||||
const rowMaDaoHttpSecret = { style: { display: 'none' } };
|
||||
const rowMaDaoMode = { style: { display: 'none' } };
|
||||
const rowMaDaoRoutingPlanId = { style: { display: 'none' } };
|
||||
const rowMaDaoProviderId = { style: { display: 'none' } };
|
||||
const rowMaDaoCountry = { style: { display: 'none' } };
|
||||
const rowMaDaoOperator = { style: { display: 'none' } };
|
||||
const rowMaDaoAutoPickCountry = { style: { display: 'none' } };
|
||||
const rowMaDaoReusePhone = { style: { display: 'none' } };
|
||||
const rowMaDaoPriceRange = { style: { display: 'none' } };
|
||||
@@ -773,6 +1010,11 @@ const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = ${JSON.stringify({
|
||||
'rowNexSmsServiceCode',
|
||||
],
|
||||
},
|
||||
'sms-bower': {
|
||||
rowKeys: [
|
||||
'rowSmsBowerApiKey',
|
||||
],
|
||||
},
|
||||
madao: {
|
||||
rowKeys: [
|
||||
'rowMaDaoBaseUrl',
|
||||
@@ -785,8 +1027,7 @@ const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = ${JSON.stringify({
|
||||
directRowKeys: [
|
||||
'rowMaDaoProviderId',
|
||||
'rowMaDaoCountry',
|
||||
'rowMaDaoAutoPickCountry',
|
||||
'rowMaDaoReusePhone',
|
||||
'rowMaDaoOperator',
|
||||
'rowMaDaoPriceRange',
|
||||
],
|
||||
},
|
||||
@@ -856,12 +1097,14 @@ return {
|
||||
rowNexSmsCountry,
|
||||
rowNexSmsCountryFallback,
|
||||
rowNexSmsServiceCode,
|
||||
rowSmsBowerApiKey,
|
||||
rowMaDaoBaseUrl,
|
||||
rowMaDaoHttpSecret,
|
||||
rowMaDaoMode,
|
||||
rowMaDaoRoutingPlanId,
|
||||
rowMaDaoProviderId,
|
||||
rowMaDaoCountry,
|
||||
rowMaDaoOperator,
|
||||
rowMaDaoAutoPickCountry,
|
||||
rowMaDaoReusePhone,
|
||||
rowMaDaoPriceRange,
|
||||
@@ -941,6 +1184,7 @@ return {
|
||||
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
|
||||
assert.equal(api.rowMaDaoProviderId.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.rowMaDaoReusePhone.style.display, 'none');
|
||||
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
|
||||
@@ -1057,6 +1301,7 @@ return {
|
||||
assert.equal(api.rowMaDaoRoutingPlanId.style.display, '');
|
||||
assert.equal(api.rowMaDaoProviderId.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.rowMaDaoReusePhone.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.rowMaDaoProviderId.style.display, '');
|
||||
assert.equal(api.rowMaDaoCountry.style.display, '');
|
||||
assert.equal(api.rowMaDaoAutoPickCountry.style.display, '');
|
||||
assert.equal(api.rowMaDaoReusePhone.style.display, '');
|
||||
assert.equal(api.rowMaDaoOperator.style.display, '');
|
||||
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
|
||||
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
|
||||
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 inputMaDaoHttpSecret = { value: 'madao-secret' };
|
||||
const selectMaDaoMode = { value: 'direct' };
|
||||
const inputMaDaoRoutingPlanId = { value: 'plan-1' };
|
||||
const inputMaDaoProviderId = { value: 'Local Provider!!' };
|
||||
const inputMaDaoCountry = { value: 'th' };
|
||||
const selectMaDaoRoutingPlanId = { value: 'plan-1' };
|
||||
const selectMaDaoProviderId = { value: 'Local Provider!!' };
|
||||
const selectMaDaoCountry = { value: 'th' };
|
||||
const selectMaDaoOperator = { value: 'Operator A!' };
|
||||
const inputMaDaoAutoPickCountry = { checked: false };
|
||||
const inputMaDaoReusePhone = { checked: true };
|
||||
const inputMaDaoMinPrice = { value: '0.02' };
|
||||
@@ -1266,7 +1513,9 @@ ${extractFunction('normalizeMaDaoBaseUrlValue')}
|
||||
${extractFunction('normalizeMaDaoModeValue')}
|
||||
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||
${extractFunction('normalizeMaDaoProviderIdValue')}
|
||||
${extractFunction('normalizeMaDaoOperatorValue')}
|
||||
${extractFunction('normalizeMaDaoCountry')}
|
||||
${extractFunction('formatMaDaoCountryDisplayLabel')}
|
||||
${extractFunction('normalizeMaDaoPriceValue')}
|
||||
${extractFunction('normalizeFiveSimCountryCode')}
|
||||
${extractFunction('normalizeFiveSimCountryOrderValue')}
|
||||
@@ -1338,6 +1587,7 @@ return { collectSettingsPayload };
|
||||
assert.equal(payload.madaoRoutingPlanId, 'plan-1');
|
||||
assert.equal(payload.madaoProviderId, 'localprovider');
|
||||
assert.equal(payload.madaoCountry, 'TH');
|
||||
assert.equal(payload.madaoOperator, 'operatora');
|
||||
assert.equal(payload.madaoAutoPickCountry, false);
|
||||
assert.equal(payload.madaoReusePhone, true);
|
||||
assert.equal(payload.madaoMinPrice, '0.02');
|
||||
@@ -1389,6 +1639,7 @@ let latestState = {
|
||||
madaoRoutingPlanId: 'plan-old',
|
||||
madaoProviderId: 'provider-old',
|
||||
madaoCountry: 'TH',
|
||||
madaoOperator: 'operator-old',
|
||||
madaoAutoPickCountry: true,
|
||||
madaoReusePhone: true,
|
||||
madaoMinPrice: '0.01',
|
||||
@@ -1434,9 +1685,34 @@ const inputNexSmsServiceCode = { value: 'ot' };
|
||||
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/poll' };
|
||||
const inputMaDaoHttpSecret = { value: 'madao-live-secret' };
|
||||
const selectMaDaoMode = { value: 'direct' };
|
||||
const inputMaDaoRoutingPlanId = { value: 'plan-live' };
|
||||
const inputMaDaoProviderId = { value: 'Provider Live!' };
|
||||
const inputMaDaoCountry = { value: 'local' };
|
||||
const selectMaDaoRoutingPlanId = {
|
||||
value: 'plan-live',
|
||||
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 inputMaDaoReusePhone = { checked: false };
|
||||
const inputMaDaoMinPrice = { value: '0.02' };
|
||||
@@ -1454,6 +1730,10 @@ let heroSmsCountrySelectionOrder = [];
|
||||
let phoneSmsProviderOrderSelection = ['hero-sms', '5sim'];
|
||||
let lastPhoneSmsProviderBeforeChange = null;
|
||||
let savedPayload = null;
|
||||
let maDaoRoutingPlanOptions = [];
|
||||
let maDaoProviderOptions = [];
|
||||
let maDaoCountryOptions = [];
|
||||
let maDaoOperatorOptions = [];
|
||||
|
||||
${extractFunction('normalizePhoneSmsProvider')}
|
||||
${extractFunction('normalizePhoneSmsProviderValue')}
|
||||
@@ -1483,9 +1763,21 @@ ${extractFunction('normalizeNexSmsServiceCodeValue')}
|
||||
${extractFunction('normalizeMaDaoBaseUrlValue')}
|
||||
${extractFunction('normalizeMaDaoModeValue')}
|
||||
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||
${extractFunction('normalizeMaDaoRoutingPlanIdValue')}
|
||||
${extractFunction('normalizeMaDaoProviderIdValue')}
|
||||
${extractFunction('normalizeMaDaoOperatorValue')}
|
||||
${extractFunction('normalizeMaDaoCountry')}
|
||||
${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('normalizePhoneSmsMaxPriceValue')}
|
||||
${extractFunction('normalizeHeroSmsCountryId')}
|
||||
@@ -1508,6 +1800,7 @@ function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
|
||||
function loadHeroSmsCountries() { return Promise.resolve(); }
|
||||
function loadFiveSimCountries() { return Promise.resolve(); }
|
||||
function loadNexSmsCountries() { return Promise.resolve(); }
|
||||
function loadMaDaoProviders() { return Promise.resolve(); }
|
||||
function applyHeroSmsFallbackSelection() {}
|
||||
function applyFiveSimCountrySelection() {}
|
||||
function applyNexSmsCountrySelection() {}
|
||||
@@ -1564,6 +1857,71 @@ return {
|
||||
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', () => {
|
||||
const api = new Function(`
|
||||
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_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')}
|
||||
@@ -348,13 +353,18 @@ ${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')}
|
||||
@@ -386,6 +396,148 @@ return {
|
||||
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 () => {
|
||||
const api = new Function(`
|
||||
const clicked = [];
|
||||
@@ -447,13 +599,18 @@ ${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')}
|
||||
@@ -481,3 +638,167 @@ return {
|
||||
assert.equal(result.step6Outcome, 'recoverable');
|
||||
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