Compare commits

..

18 Commits

Author SHA1 Message Date
chick 5f871c3c40 feat: add SMS Bower phone SMS provider 2026-05-30 21:30:14 +08:00
QLHazyCoder 8cee2611ca chore(release): bump version to FlowPilot2.6 2026-05-30 04:51:09 +08:00
QLHazyCoder a33073436c Merge pull request #293 from netcookies/fix/madao-country-iso-dev
fix(madao): 适配国家中文标签和线路归一化
2026-05-30 04:09:50 +08:00
QLHazyCoder 932d1e965b fix: route choose-account to target email login 2026-05-30 03:13:42 +08:00
QLHazyCoder 9b7a6dcfbb Merge branch 'dev' of https://github.com/QLHazyCoder/codex-oauth-automation-extension into dev 2026-05-30 02:41:08 +08:00
QLHazyCoder 3b751e8ba3 fix: add fresh attempt identity reset patch and corresponding tests 2026-05-30 02:41:00 +08:00
QLHazyCoder 8c2cb2f8e0 fix: stop GPC flow on no-trial logs 2026-05-30 02:35:07 +08:00
QLHazyCoder 0afa951814 fix: parse latest GPC log entry by timestamp 2026-05-30 02:24:57 +08:00
Isulew c64ff5152c fix(madao): 适配国家中文标签和线路归一化
- 优先显示 MaDao 返回的 label_zh,不在 FlowPilot 内维护国家翻译表
- 兼容旧 provider_value 保存值,刷新直连国家后自动反选 ISO value
- 将 any / Any operator 归一为空线路,避免重复选项和错误下发
- 保留 MaDao 隐藏复用控件缺失时的默认开启状态
2026-05-29 21:59:20 +08:00
QLHazyCoder 3c0bc41794 fix: enhance OAuth session handling and add tests for state resets 2026-05-29 21:03:47 +08:00
Isulew e441c5e7b8 fix: 修复 MaDao direct 常量隔离测试 2026-05-29 17:57:12 +08:00
Isulew ed1d62687c fix: 补齐 MaDao 直连动态选项
恢复 MaDao 直连模式的服务商、国家、线路下拉,并通过 daemon providers/countries/operators 接口级联加载。保存 madaoOperator 并在 direct acquire 中通过 metadata.operator 下发,routing 模式保留已保存的直连配置。
2026-05-29 17:53:28 +08:00
Isulew 0938f5dfba fix: 支持 MaDao 路由计划下拉选择
将 MaDao 路由计划从文本输入改为可刷新下拉列表。

保留已保存计划 ID 作为兜底选项,避免列表加载失败时清空配置。
2026-05-29 16:01:16 +08:00
Isulew f6a9f42089 fix: 收敛 MaDao 直连模式设置项
隐藏 MaDao 直连平台、直连国家、自动选国家和复用开关等 adapter 内部参数。

保留直连模式选择与价格范围配置,避免 UI 暴露底层调度细节。
2026-05-29 15:39:04 +08:00
Isulew 26b1b15cd6 refactor: 收敛接码 provider 生命周期边界 2026-05-29 15:04:19 +08:00
Isulew e50b0fbfeb fix(madao): 修复换号后国家区号残留
让 MaDao activation 使用 ISO country code,并避免 routing replace 沿用旧 ticket 的 country。
提交 OpenAI add-phone 前校验当前国家区号与手机号匹配,防止页面残留 Thailand 区号。
2026-05-29 12:27:54 +08:00
Isulew f26f7e949e fix(phone-sms): 统一 provider adapter 通过 registry 创建
移除 background direct factory fallback,改为通过 PhoneSmsProviderRegistry 创建 5sim 与 MaDao adapter。
补齐 5sim adapter 对国家顺序、最低价格和 countryCode 的主链兼容。
2026-05-29 12:27:53 +08:00
Isulew a38fb250ea fix(phone-sms): 补回 NexSMS provider registry 接入
补回 NexSMS provider adapter,并在 background 与 sidepanel 加载。
同时补充 registry 创建真实 NexSMS provider 的测试,避免后续回归。
2026-05-29 12:27:53 +08:00
32 changed files with 6314 additions and 3648 deletions
+96 -21
View File
@@ -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,
+26
View File
@@ -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),
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,
+188 -11
View File
@@ -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}.`,
});
+22 -3
View File
@@ -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
View File
@@ -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",
+183 -4
View File
@@ -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,
};
});
File diff suppressed because it is too large Load Diff
+283 -11
View File
@@ -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
+8
View File
@@ -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,
+471
View File
@@ -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
View File
@@ -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
View File
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: {
+47
View File
@@ -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');
+59 -1
View File
@@ -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 = {};
+35
View File
@@ -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({
+62
View File
@@ -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;
+58 -5
View File
@@ -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');
});
+334 -86
View File
@@ -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';
+111
View File
@@ -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'],
]
);
});
+25
View File
@@ -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');
});
+321
View File
@@ -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');
});