fix: sync LuckMail PR with dev
This commit is contained in:
@@ -8,11 +8,12 @@
|
||||
|
||||
一百五十个号,一个401:
|
||||
|
||||
进官网,获取最新交流群:<https://apikey.qzz.io/>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="100%">
|
||||
<td align="center" width="50%">
|
||||
<img src="docs/images/交流群.jpg" alt="QQ交流群,便于大家交流" width="100%" />
|
||||
</td>
|
||||
<td align="center" width="50%">
|
||||
<img src="docs/images/十轮自动.png" alt="最新版本运行日志" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
@@ -74,7 +75,6 @@
|
||||
- 至少准备一种验证码接收方式:
|
||||
- DuckDuckGo `@duck.com` + QQ / 163 / Inbucket 转发
|
||||
- Cloudflare 自定义域邮箱前缀 + QQ / 163 / Inbucket 转发
|
||||
- iCloud Hide My Email,可选择直接从 iCloud 收件箱收码,或转发到 QQ / 163 / 163 VIP / 126 / Gmail 后收码
|
||||
- 手动填写一个可收信邮箱
|
||||
- 如果使用 `QQ` / `163` / `163 VIP` / `126` / `Inbucket`,对应页面需要提前能正常打开
|
||||
|
||||
@@ -592,7 +592,6 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
支持:
|
||||
|
||||
- `Hotmail`(远程服务 / 本地助手)
|
||||
- `iCloud` 收件箱,或 iCloud Hide My Email 转发到 QQ / 163 / 163 VIP / 126 / Gmail 后收码
|
||||
- `content/qq-mail.js`
|
||||
- `content/mail-163.js`(163 / 163 VIP / 126)
|
||||
- `content/inbucket-mail.js`
|
||||
@@ -774,7 +773,7 @@ content/utils.js 通用工具:等待元素、点击、日志、停
|
||||
content/vps-panel.js CPA 面板步骤:内部 OAuth 刷新 / Step 10
|
||||
content/signup-page.js ChatGPT 官网 + OpenAI 注册/登录页步骤:Step 1 / 2 / 3 / 5 / 7 / 9
|
||||
hotmail-utils.js Hotmail 收信相关通用辅助
|
||||
mail-provider-utils.js 网页邮箱 provider 与 iCloud 转发收码配置辅助
|
||||
mail-provider-utils.js 网页邮箱 provider 配置辅助
|
||||
content/duck-mail.js Duck 邮箱自动获取
|
||||
content/qq-mail.js QQ 邮箱验证码轮询
|
||||
content/mail-163.js 163 / 163 VIP / 126 邮箱验证码轮询
|
||||
|
||||
+463
@@ -3,10 +3,12 @@
|
||||
importScripts(
|
||||
'managed-alias-utils.js',
|
||||
'mail2925-utils.js',
|
||||
'paypal-utils.js',
|
||||
'background/phone-verification-flow.js',
|
||||
'background/account-run-history.js',
|
||||
'background/contribution-oauth.js',
|
||||
'background/mail-2925-session.js',
|
||||
'background/paypal-account-store.js',
|
||||
'background/ip-proxy-provider-711proxy.js',
|
||||
'background/ip-proxy-core.js',
|
||||
'background/panel-bridge.js',
|
||||
@@ -263,6 +265,21 @@ const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600;
|
||||
const VERIFICATION_RESEND_COUNT_MIN = 0;
|
||||
const VERIFICATION_RESEND_COUNT_MAX = 20;
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const PHONE_CODE_POLL_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_ROUNDS_MAX = 120;
|
||||
const DEFAULT_PHONE_CODE_POLL_ROUNDS = 4;
|
||||
const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
|
||||
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = ['signupVerificationResendCount', 'loginVerificationResendCount'];
|
||||
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
||||
@@ -281,6 +298,10 @@ const HERO_SMS_SERVICE_CODE = 'dr';
|
||||
const HERO_SMS_SERVICE_LABEL = 'OpenAI';
|
||||
const HERO_SMS_COUNTRY_ID = 52;
|
||||
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
||||
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
||||
const PERSISTENT_ALIAS_STATE_KEYS = [
|
||||
@@ -436,6 +457,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
plusModeEnabled: false,
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
currentPayPalAccountId: '',
|
||||
autoRunSkipFailures: false,
|
||||
autoRunFallbackThreadIntervalMinutes: 0,
|
||||
autoRunDelayEnabled: false,
|
||||
@@ -443,6 +465,11 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
autoStepDelaySeconds: null,
|
||||
phoneVerificationEnabled: false,
|
||||
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
|
||||
phoneVerificationReplacementLimit: DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT,
|
||||
phoneCodeWaitSeconds: DEFAULT_PHONE_CODE_WAIT_SECONDS,
|
||||
phoneCodeTimeoutWindows: DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
|
||||
phoneCodePollIntervalSeconds: DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
|
||||
phoneCodePollMaxRounds: DEFAULT_PHONE_CODE_POLL_ROUNDS,
|
||||
mailProvider: '163',
|
||||
mail2925Mode: DEFAULT_MAIL_2925_MODE,
|
||||
mail2925UseAccountPool: false,
|
||||
@@ -480,9 +507,14 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
cloudflareTempEmailDomains: [],
|
||||
hotmailAccounts: [],
|
||||
mail2925Accounts: [],
|
||||
paypalAccounts: [],
|
||||
heroSmsApiKey: '',
|
||||
heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED,
|
||||
heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY,
|
||||
heroSmsMaxPrice: '',
|
||||
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
|
||||
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
|
||||
heroSmsCountryFallback: [],
|
||||
};
|
||||
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
@@ -523,6 +555,8 @@ const DEFAULT_STATE = {
|
||||
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
|
||||
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
|
||||
localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。
|
||||
cpaOAuthState: null, // CPA OAuth state。
|
||||
cpaManagementOrigin: null, // CPA 管理接口 origin。
|
||||
sub2apiSessionId: null, // SUB2API OpenAI Auth 会话 ID。
|
||||
sub2apiOAuthState: null, // SUB2API OpenAI Auth state。
|
||||
sub2apiGroupId: null, // SUB2API 目标分组 ID。
|
||||
@@ -562,7 +596,14 @@ const DEFAULT_STATE = {
|
||||
currentLuckmailPurchase: null,
|
||||
currentLuckmailMailCursor: null,
|
||||
currentPhoneActivation: null,
|
||||
currentPhoneVerificationCode: '',
|
||||
reusablePhoneActivation: null,
|
||||
heroSmsLastPriceTiers: [],
|
||||
heroSmsLastPriceCountryId: 0,
|
||||
heroSmsLastPriceCountryLabel: '',
|
||||
heroSmsLastPriceUserLimit: '',
|
||||
heroSmsLastPriceAt: 0,
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
autoRunning: false, // 当前是否处于自动运行中。
|
||||
autoRunPhase: 'idle', // 当前自动运行阶段。
|
||||
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
|
||||
@@ -579,6 +620,7 @@ const DEFAULT_STATE = {
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
oauthFlowDeadlineSourceUrl: null,
|
||||
currentPayPalAccountId: null,
|
||||
currentHotmailAccountId: null,
|
||||
currentMail2925AccountId: null,
|
||||
preferredIcloudHost: '',
|
||||
@@ -661,6 +703,143 @@ function normalizeVerificationResendCount(value, fallback) {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePhoneVerificationReplacementLimit(value, fallback = DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT) {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
const numeric = Number(rawValue);
|
||||
if (!rawValue || !Number.isFinite(numeric)) {
|
||||
return Math.min(
|
||||
PHONE_REPLACEMENT_LIMIT_MAX,
|
||||
Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT))
|
||||
);
|
||||
}
|
||||
return Math.min(
|
||||
PHONE_REPLACEMENT_LIMIT_MAX,
|
||||
Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.floor(numeric))
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePhoneCodeWaitSeconds(value, fallback = DEFAULT_PHONE_CODE_WAIT_SECONDS) {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
const numeric = Number(rawValue);
|
||||
if (!rawValue || !Number.isFinite(numeric)) {
|
||||
return Math.min(
|
||||
PHONE_CODE_WAIT_SECONDS_MAX,
|
||||
Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_WAIT_SECONDS))
|
||||
);
|
||||
}
|
||||
return Math.min(
|
||||
PHONE_CODE_WAIT_SECONDS_MAX,
|
||||
Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.floor(numeric))
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePhoneCodeTimeoutWindows(value, fallback = DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS) {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
const numeric = Number(rawValue);
|
||||
if (!rawValue || !Number.isFinite(numeric)) {
|
||||
return Math.min(
|
||||
PHONE_CODE_TIMEOUT_WINDOWS_MAX,
|
||||
Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS))
|
||||
);
|
||||
}
|
||||
return Math.min(
|
||||
PHONE_CODE_TIMEOUT_WINDOWS_MAX,
|
||||
Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.floor(numeric))
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePhoneCodePollIntervalSeconds(value, fallback = DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS) {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
const numeric = Number(rawValue);
|
||||
if (!rawValue || !Number.isFinite(numeric)) {
|
||||
return Math.min(
|
||||
PHONE_CODE_POLL_INTERVAL_SECONDS_MAX,
|
||||
Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS))
|
||||
);
|
||||
}
|
||||
return Math.min(
|
||||
PHONE_CODE_POLL_INTERVAL_SECONDS_MAX,
|
||||
Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.floor(numeric))
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePhoneCodePollMaxRounds(value, fallback = DEFAULT_PHONE_CODE_POLL_ROUNDS) {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
const numeric = Number(rawValue);
|
||||
if (!rawValue || !Number.isFinite(numeric)) {
|
||||
return Math.min(
|
||||
PHONE_CODE_POLL_ROUNDS_MAX,
|
||||
Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_POLL_ROUNDS))
|
||||
);
|
||||
}
|
||||
return Math.min(
|
||||
PHONE_CODE_POLL_ROUNDS_MAX,
|
||||
Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.floor(numeric))
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeHeroSmsMaxPrice(value = '') {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
if (!rawValue) {
|
||||
return '';
|
||||
}
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
return '';
|
||||
}
|
||||
return String(Math.round(numeric * 10000) / 10000);
|
||||
}
|
||||
|
||||
function normalizeHeroSmsAcquirePriority(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === HERO_SMS_ACQUIRE_PRIORITY_PRICE
|
||||
? HERO_SMS_ACQUIRE_PRIORITY_PRICE
|
||||
: HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
}
|
||||
|
||||
function normalizeHeroSmsCountryFallback(value = []) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(/[\r\n,,;;]+/)
|
||||
.map((entry) => String(entry || '').trim())
|
||||
.filter(Boolean);
|
||||
const seenIds = new Set();
|
||||
const normalized = [];
|
||||
|
||||
for (const entry of source) {
|
||||
let countryId = 0;
|
||||
let countryLabel = '';
|
||||
|
||||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||
countryId = Math.floor(Number(entry.countryId ?? entry.id) || 0);
|
||||
countryLabel = String((entry.countryLabel ?? entry.label) || '').trim();
|
||||
} else {
|
||||
const text = String(entry || '').trim();
|
||||
const structuredMatch = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/);
|
||||
if (structuredMatch) {
|
||||
countryId = Math.floor(Number(structuredMatch[1]) || 0);
|
||||
countryLabel = String(structuredMatch[2] || '').trim();
|
||||
} else {
|
||||
countryId = Math.floor(Number(text) || 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(countryId) || countryId <= 0 || seenIds.has(countryId)) {
|
||||
continue;
|
||||
}
|
||||
seenIds.add(countryId);
|
||||
normalized.push({
|
||||
id: countryId,
|
||||
label: countryLabel || `Country #${countryId}`,
|
||||
});
|
||||
if (normalized.length >= 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolveLegacyAutoStepDelaySeconds(input = {}) {
|
||||
const hasLegacyMin = input.autoStepRandomDelayMinSeconds !== undefined;
|
||||
const hasLegacyMax = input.autoStepRandomDelayMaxSeconds !== undefined;
|
||||
@@ -1246,6 +1425,8 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return String(value || '').trim();
|
||||
case 'paypalPassword':
|
||||
return String(value || '');
|
||||
case 'currentPayPalAccountId':
|
||||
return String(value || '').trim();
|
||||
case 'autoRunSkipFailures':
|
||||
case 'autoRunDelayEnabled':
|
||||
case 'phoneVerificationEnabled':
|
||||
@@ -1259,6 +1440,16 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds);
|
||||
case 'verificationResendCount':
|
||||
return normalizeVerificationResendCount(value, DEFAULT_VERIFICATION_RESEND_COUNT);
|
||||
case 'phoneVerificationReplacementLimit':
|
||||
return normalizePhoneVerificationReplacementLimit(value, DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT);
|
||||
case 'phoneCodeWaitSeconds':
|
||||
return normalizePhoneCodeWaitSeconds(value, DEFAULT_PHONE_CODE_WAIT_SECONDS);
|
||||
case 'phoneCodeTimeoutWindows':
|
||||
return normalizePhoneCodeTimeoutWindows(value, DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS);
|
||||
case 'phoneCodePollIntervalSeconds':
|
||||
return normalizePhoneCodePollIntervalSeconds(value, DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS);
|
||||
case 'phoneCodePollMaxRounds':
|
||||
return normalizePhoneCodePollMaxRounds(value, DEFAULT_PHONE_CODE_POLL_ROUNDS);
|
||||
case 'mailProvider':
|
||||
return normalizeMailProvider(value);
|
||||
case 'mail2925Mode':
|
||||
@@ -1326,12 +1517,22 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeHotmailAccounts(value);
|
||||
case 'mail2925Accounts':
|
||||
return normalizeMail2925Accounts(value);
|
||||
case 'paypalAccounts':
|
||||
return normalizePayPalAccounts(value);
|
||||
case 'heroSmsApiKey':
|
||||
return String(value || '');
|
||||
case 'heroSmsReuseEnabled':
|
||||
return Boolean(value);
|
||||
case 'heroSmsAcquirePriority':
|
||||
return normalizeHeroSmsAcquirePriority(value);
|
||||
case 'heroSmsMaxPrice':
|
||||
return normalizeHeroSmsMaxPrice(value);
|
||||
case 'heroSmsCountryId':
|
||||
return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID));
|
||||
case 'heroSmsCountryLabel':
|
||||
return String(value || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL;
|
||||
case 'heroSmsCountryFallback':
|
||||
return normalizeHeroSmsCountryFallback(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
@@ -1877,6 +2078,7 @@ async function resetState() {
|
||||
'accounts',
|
||||
'tabRegistry',
|
||||
'sourceLastUrls',
|
||||
'reusablePhoneActivation',
|
||||
'luckmailApiKey',
|
||||
'luckmailBaseUrl',
|
||||
'luckmailEmailType',
|
||||
@@ -1891,6 +2093,25 @@ async function resetState() {
|
||||
getPersistedAliasState(),
|
||||
]);
|
||||
const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), persistedSettings, prev);
|
||||
const reusablePhoneActivation = (
|
||||
prev.reusablePhoneActivation
|
||||
&& typeof prev.reusablePhoneActivation === 'object'
|
||||
&& !Array.isArray(prev.reusablePhoneActivation)
|
||||
&& String(
|
||||
prev.reusablePhoneActivation.activationId
|
||||
?? prev.reusablePhoneActivation.id
|
||||
?? prev.reusablePhoneActivation.activation
|
||||
?? ''
|
||||
).trim()
|
||||
&& String(
|
||||
prev.reusablePhoneActivation.phoneNumber
|
||||
?? prev.reusablePhoneActivation.number
|
||||
?? prev.reusablePhoneActivation.phone
|
||||
?? ''
|
||||
).trim()
|
||||
)
|
||||
? prev.reusablePhoneActivation
|
||||
: null;
|
||||
await chrome.storage.session.clear();
|
||||
await chrome.storage.session.set({
|
||||
...DEFAULT_STATE,
|
||||
@@ -1911,6 +2132,8 @@ async function resetState() {
|
||||
luckmailPreserveTagName: String(prev.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||||
currentLuckmailPurchase: null,
|
||||
currentLuckmailMailCursor: null,
|
||||
// Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses.
|
||||
reusablePhoneActivation,
|
||||
preferredIcloudHost: prev.preferredIcloudHost || '',
|
||||
});
|
||||
}
|
||||
@@ -1941,6 +2164,50 @@ function generatePassword() {
|
||||
return pw.split('').sort(() => Math.random() - 0.5).join('');
|
||||
}
|
||||
|
||||
function normalizePayPalAccount(account = {}) {
|
||||
if (self.PayPalUtils?.normalizePayPalAccount) {
|
||||
return self.PayPalUtils.normalizePayPalAccount(account);
|
||||
}
|
||||
return {
|
||||
id: String(account.id || crypto.randomUUID()),
|
||||
email: String(account.email || '').trim().toLowerCase(),
|
||||
password: String(account.password || ''),
|
||||
createdAt: Number.isFinite(Number(account.createdAt)) ? Number(account.createdAt) : Date.now(),
|
||||
updatedAt: Number.isFinite(Number(account.updatedAt)) ? Number(account.updatedAt) : Date.now(),
|
||||
lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePayPalAccounts(accounts) {
|
||||
if (self.PayPalUtils?.normalizePayPalAccounts) {
|
||||
return self.PayPalUtils.normalizePayPalAccounts(accounts);
|
||||
}
|
||||
return Array.isArray(accounts) ? accounts.map((account) => normalizePayPalAccount(account)) : [];
|
||||
}
|
||||
|
||||
function findPayPalAccount(accounts, accountId) {
|
||||
if (self.PayPalUtils?.findPayPalAccount) {
|
||||
return self.PayPalUtils.findPayPalAccount(accounts, accountId);
|
||||
}
|
||||
const normalizedId = String(accountId || '').trim();
|
||||
if (!normalizedId) return null;
|
||||
return normalizePayPalAccounts(accounts).find((account) => account.id === normalizedId) || null;
|
||||
}
|
||||
|
||||
function upsertPayPalAccountInList(accounts, nextAccount) {
|
||||
if (self.PayPalUtils?.upsertPayPalAccountInList) {
|
||||
return self.PayPalUtils.upsertPayPalAccountInList(accounts, nextAccount);
|
||||
}
|
||||
const normalizedNext = normalizePayPalAccount(nextAccount);
|
||||
const list = normalizePayPalAccounts(accounts);
|
||||
const existingIndex = list.findIndex((account) => account.id === normalizedNext.id);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = normalizedNext;
|
||||
return list;
|
||||
}
|
||||
return [...list, normalizedNext];
|
||||
}
|
||||
|
||||
function normalizeHotmailAccount(account = {}) {
|
||||
const normalizedLastAuthAt = Number.isFinite(Number(account.lastAuthAt)) ? Number(account.lastAuthAt) : 0;
|
||||
const normalizedStatus = String(
|
||||
@@ -5251,6 +5518,13 @@ async function finalizeIcloudAliasAfterSuccessfulFlow(state) {
|
||||
}
|
||||
}
|
||||
|
||||
async function finalizePhoneActivationAfterSuccessfulFlow(state) {
|
||||
if (typeof phoneVerificationHelpers?.finalizePendingPhoneActivationConfirmation !== 'function') {
|
||||
return null;
|
||||
}
|
||||
return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Tab Registry
|
||||
// ============================================================
|
||||
@@ -5973,6 +6247,8 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
return {
|
||||
...plusRuntimeResets,
|
||||
oauthUrl: null,
|
||||
cpaOAuthState: null,
|
||||
cpaManagementOrigin: null,
|
||||
sub2apiSessionId: null,
|
||||
sub2apiOAuthState: null,
|
||||
sub2apiGroupId: null,
|
||||
@@ -5986,9 +6262,11 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
oauthFlowDeadlineSourceUrl: null,
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
currentPhoneVerificationCode: '',
|
||||
};
|
||||
}
|
||||
if (step === 2) {
|
||||
@@ -6000,9 +6278,11 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
oauthFlowDeadlineSourceUrl: null,
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
currentPhoneVerificationCode: '',
|
||||
};
|
||||
}
|
||||
if (step === 3 || step === 4) {
|
||||
@@ -6013,9 +6293,11 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
oauthFlowDeadlineSourceUrl: null,
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
currentPhoneVerificationCode: '',
|
||||
};
|
||||
}
|
||||
if (step === 5 || step === 6 || step === 7 || step === 8) {
|
||||
@@ -6035,13 +6317,17 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
oauthFlowDeadlineSourceUrl: null,
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
localhostUrl: null,
|
||||
currentPhoneVerificationCode: '',
|
||||
};
|
||||
}
|
||||
if (step === 9) {
|
||||
return {
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
plusReturnUrl: '',
|
||||
localhostUrl: null,
|
||||
currentPhoneVerificationCode: '',
|
||||
};
|
||||
}
|
||||
if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') {
|
||||
@@ -6050,11 +6336,14 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
oauthFlowDeadlineSourceUrl: null,
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
localhostUrl: null,
|
||||
currentPhoneVerificationCode: '',
|
||||
};
|
||||
}
|
||||
if (stepKey === 'confirm-oauth') {
|
||||
return {
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
localhostUrl: null,
|
||||
};
|
||||
}
|
||||
@@ -6811,6 +7100,8 @@ async function handleStepData(step, payload) {
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
|
||||
if (payload.cpaManagementOrigin !== undefined) updates.cpaManagementOrigin = payload.cpaManagementOrigin || null;
|
||||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||||
if (Object.keys(updates).length) {
|
||||
@@ -6905,6 +7196,7 @@ async function handleStepData(step, payload) {
|
||||
if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) {
|
||||
await setEmailStateSilently(null);
|
||||
}
|
||||
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -7556,6 +7848,39 @@ function isMail2925PoolExhaustedPauseError(error) {
|
||||
return /^MAIL2925_POOL_EXHAUSTED_PAUSE::/.test(message);
|
||||
}
|
||||
|
||||
const payPalAccountStore = self.MultiPageBackgroundPayPalAccountStore?.createPayPalAccountStore({
|
||||
broadcastDataUpdate,
|
||||
findPayPalAccount,
|
||||
getState,
|
||||
normalizePayPalAccount,
|
||||
normalizePayPalAccounts,
|
||||
setPersistentSettings,
|
||||
setState,
|
||||
upsertPayPalAccountInList,
|
||||
});
|
||||
|
||||
async function syncPayPalAccounts(accounts) {
|
||||
return payPalAccountStore?.syncPayPalAccounts?.(accounts) || [];
|
||||
}
|
||||
|
||||
async function upsertPayPalAccount(input = {}) {
|
||||
if (!payPalAccountStore?.upsertPayPalAccount) {
|
||||
throw new Error('PayPal 账号存储能力尚未接入。');
|
||||
}
|
||||
return payPalAccountStore.upsertPayPalAccount(input);
|
||||
}
|
||||
|
||||
async function setCurrentPayPalAccount(accountId) {
|
||||
if (!payPalAccountStore?.setCurrentPayPalAccount) {
|
||||
throw new Error('PayPal 账号选择能力尚未接入。');
|
||||
}
|
||||
return payPalAccountStore.setCurrentPayPalAccount(accountId);
|
||||
}
|
||||
|
||||
function getCurrentPayPalAccount(state = null) {
|
||||
return payPalAccountStore?.getCurrentPayPalAccount?.(state || {}) || null;
|
||||
}
|
||||
|
||||
const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({
|
||||
addLog,
|
||||
buildGeneratedAliasEmail,
|
||||
@@ -8505,6 +8830,11 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
|
||||
const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.createPhoneVerificationHelpers({
|
||||
addLog,
|
||||
DEFAULT_HERO_SMS_BASE_URL,
|
||||
DEFAULT_HERO_SMS_REUSE_ENABLED,
|
||||
DEFAULT_PHONE_CODE_WAIT_SECONDS,
|
||||
DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
|
||||
DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
|
||||
DEFAULT_PHONE_CODE_POLL_ROUNDS,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getState,
|
||||
@@ -8741,6 +9071,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
deleteHotmailAccounts,
|
||||
deleteIcloudAlias,
|
||||
deleteUsedIcloudAliases,
|
||||
findPayPalAccount,
|
||||
disableUsedLuckmailPurchases,
|
||||
doesStepUseCompletionSignal,
|
||||
ensureMail2925MailboxSession,
|
||||
@@ -8749,6 +9080,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
executeStepViaCompletionSignal,
|
||||
exportSettingsBundle,
|
||||
fetchGeneratedEmail,
|
||||
finalizePhoneActivationAfterSuccessfulFlow,
|
||||
finalizeStep3Completion: async () => {
|
||||
const currentState = await getState();
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
@@ -8785,9 +9117,11 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
listIcloudAliases,
|
||||
listLuckmailPurchasesForManagement,
|
||||
refreshIpProxyPool,
|
||||
getCurrentPayPalAccount,
|
||||
getCurrentMail2925Account,
|
||||
normalizeHotmailAccounts,
|
||||
normalizeMail2925Accounts,
|
||||
normalizePayPalAccounts,
|
||||
normalizeRunCount,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||||
notifyStepComplete,
|
||||
@@ -8803,6 +9137,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
selectLuckmailPurchase,
|
||||
switchIpProxy,
|
||||
changeIpProxyExit,
|
||||
setCurrentPayPalAccount,
|
||||
setCurrentHotmailAccount,
|
||||
setCurrentMail2925Account,
|
||||
setContributionMode,
|
||||
@@ -8822,9 +9157,11 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
startAutoRunLoop,
|
||||
pollContributionStatus: (...args) => contributionOAuthManager?.pollContributionStatus?.(...args),
|
||||
syncHotmailAccounts,
|
||||
syncPayPalAccounts,
|
||||
deleteMail2925Account,
|
||||
deleteMail2925Accounts,
|
||||
testHotmailAccountMailAccess,
|
||||
upsertPayPalAccount,
|
||||
upsertMail2925Account,
|
||||
upsertHotmailAccount,
|
||||
verifyHotmailAccount,
|
||||
@@ -9348,6 +9685,10 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
const hasTransientNetworkSignal = /connect:\s*connection refused|failed to fetch|i\/o timeout|context deadline exceeded|eof|connection reset by peer/i.test(normalizedMessage);
|
||||
return mentionsTokenExchange && hasTransientNetworkSignal;
|
||||
};
|
||||
const isPhoneVerificationLocalFailure = (errorMessage = '') => {
|
||||
const normalizedMessage = String(errorMessage || '');
|
||||
return /HeroSMS|phone verification did not succeed|number replacements|sms_timeout_after_resend|phone number is already linked|add-phone keeps rejecting current number|接码|手机号|手机验证码|步骤\s*9.*(?:手机号|验证码)|Step\s*9.*phone verification/i.test(normalizedMessage);
|
||||
};
|
||||
|
||||
const normalizedStep = Number(step);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
@@ -9378,6 +9719,17 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
};
|
||||
}
|
||||
|
||||
if (isPhoneVerificationLocalFailure(errorMessage)) {
|
||||
return {
|
||||
shouldRestart: false,
|
||||
blockedByAddPhone: true,
|
||||
forcedByPhoneVerificationTimeout: false,
|
||||
restartStep: authChainStartStep,
|
||||
errorMessage,
|
||||
authState: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (shouldForceRestartFromStep7) {
|
||||
return {
|
||||
shouldRestart: true,
|
||||
@@ -9980,6 +10332,116 @@ function getStep8EffectLabel(effect) {
|
||||
}
|
||||
}
|
||||
|
||||
function isStep9OAuthLocalhostTimeoutError(error, visibleStep = 9) {
|
||||
const message = getErrorMessage(error);
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
if (!/从拿到 OAuth 登录地址开始/.test(message)) {
|
||||
return false;
|
||||
}
|
||||
if (!/localhost 回调|OAuth localhost 回调/i.test(message)) {
|
||||
return false;
|
||||
}
|
||||
const normalizedStep = Number(visibleStep);
|
||||
if (Number.isFinite(normalizedStep) && normalizedStep > 0) {
|
||||
const stepPrefix = new RegExp(`步骤\\s*${normalizedStep}\\s*:`);
|
||||
if (!stepPrefix.test(message)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function recoverOAuthLocalhostTimeout(details = {}) {
|
||||
const {
|
||||
error,
|
||||
state,
|
||||
visibleStep = 9,
|
||||
} = details;
|
||||
|
||||
if (!isStep9OAuthLocalhostTimeoutError(error, visibleStep)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authLoginStep = typeof getAuthChainStartStepId === 'function'
|
||||
? getAuthChainStartStepId(state || {})
|
||||
: FINAL_OAUTH_CHAIN_START_STEP;
|
||||
const loginCodeStep = Number(visibleStep) >= 12 ? 11 : 8;
|
||||
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:检测到 OAuth localhost 回调等待窗口已过期,正在复核认证页并回到步骤 ${authLoginStep} 重拉授权链路。`,
|
||||
'warn'
|
||||
);
|
||||
|
||||
let authState = null;
|
||||
try {
|
||||
authState = await getLoginAuthStateFromContent({
|
||||
timeoutMs: 10000,
|
||||
responseTimeoutMs: 10000,
|
||||
logMessage: `步骤 ${visibleStep}:正在复核认证页状态,确认是否可自动恢复 localhost 回调链路...`,
|
||||
});
|
||||
} catch (inspectError) {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:复核认证页状态失败(${getErrorMessage(inspectError)}),将先尝试按步骤 ${loginCodeStep} 收尾恢复。`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
|
||||
if (isAddPhoneAuthState(authState)) {
|
||||
const stateLabel = getLoginAuthStateLabel(authState.state);
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:当前认证页为 ${stateLabel},将直接回到步骤 ${authLoginStep} 重新拉起授权链路,避免步骤 8/9 恢复冲突。`,
|
||||
'warn'
|
||||
);
|
||||
} else if (authState && authState.state && !['verification_page', 'oauth_consent_page'].includes(authState.state)) {
|
||||
const stateLabel = getLoginAuthStateLabel(authState.state);
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:当前认证页为 ${stateLabel},不满足快速恢复条件,将回到步骤 ${authLoginStep} 重开授权链路。`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
|
||||
const latestState = await getState();
|
||||
if (!step7Executor?.executeStep7 || !step8Executor?.executeStep8) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:正在自动重开步骤 ${authLoginStep} -> ${loginCodeStep},恢复到可继续捕获 localhost 回调的状态。`,
|
||||
'warn'
|
||||
);
|
||||
await step7Executor.executeStep7({
|
||||
...latestState,
|
||||
visibleStep: authLoginStep,
|
||||
});
|
||||
|
||||
const stateAfterStep7 = await getState();
|
||||
await step8Executor.executeStep8({
|
||||
...stateAfterStep7,
|
||||
visibleStep: loginCodeStep,
|
||||
});
|
||||
|
||||
const recoveredState = await getState();
|
||||
const oauthUrl = String(recoveredState?.oauthUrl || state?.oauthUrl || '').trim();
|
||||
if (oauthUrl && typeof startOAuthFlowTimeoutWindow === 'function') {
|
||||
await startOAuthFlowTimeoutWindow({
|
||||
step: Number(visibleStep) || 9,
|
||||
oauthUrl,
|
||||
});
|
||||
}
|
||||
|
||||
await setState({
|
||||
localhostUrl: null,
|
||||
});
|
||||
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:已恢复到步骤 ${authLoginStep} -> ${loginCodeStep} 收尾状态,并刷新 OAuth localhost 回调等待窗口,准备重试当前步骤。`,
|
||||
'warn'
|
||||
);
|
||||
return await getState();
|
||||
}
|
||||
|
||||
const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
||||
addLog,
|
||||
chrome,
|
||||
@@ -9997,6 +10459,7 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
||||
getStep8TabUpdatedListener,
|
||||
isTabAlive,
|
||||
prepareStep8DebuggerClick,
|
||||
recoverOAuthLocalhostTimeout,
|
||||
reloadStep8ConsentPage,
|
||||
reuseOrCreateTab,
|
||||
setStep8PendingReject,
|
||||
|
||||
@@ -392,6 +392,7 @@
|
||||
inbucketMailbox: prevState.inbucketMailbox,
|
||||
cloudflareDomain: prevState.cloudflareDomain,
|
||||
cloudflareDomains: prevState.cloudflareDomains,
|
||||
reusablePhoneActivation: prevState.reusablePhoneActivation,
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
autoRunSessionId: sessionId,
|
||||
tabRegistry: {},
|
||||
|
||||
+132
-144
@@ -3,19 +3,15 @@
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundContributionOAuthModule() {
|
||||
const API_BASE_URL = 'https://apikey.qzz.io/oauth/api';
|
||||
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
|
||||
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'expired', 'error']);
|
||||
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
|
||||
const CALLBACK_FINAL_STATUSES = new Set(['submitted']);
|
||||
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
|
||||
const CONTRIBUTION_SOURCE_CPA = 'cpa';
|
||||
const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
|
||||
const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
|
||||
const CONTRIBUTION_SUB2API_PLUS_GROUP_NAME = 'openai-plus';
|
||||
|
||||
const RUNTIME_DEFAULTS = {
|
||||
contributionMode: false,
|
||||
contributionModeExpected: false,
|
||||
contributionSource: CONTRIBUTION_SOURCE_SUB2API,
|
||||
contributionTargetGroupName: CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME,
|
||||
contributionSource: 'sub2api',
|
||||
contributionTargetGroupName: 'codex号池',
|
||||
contributionNickname: '',
|
||||
contributionQq: '',
|
||||
contributionSessionId: '',
|
||||
@@ -44,7 +40,8 @@
|
||||
} = deps;
|
||||
|
||||
let listenersBound = false;
|
||||
const inFlightCapturedCallbackTasks = new Map();
|
||||
const pendingCallbackSubmissions = new Map();
|
||||
const pendingCapturedCallbacks = new Map();
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
@@ -71,6 +68,9 @@
|
||||
case 'auto_rejected':
|
||||
case 'rejected':
|
||||
return 'auto_rejected';
|
||||
case 'manual_review_required':
|
||||
case 'manual_review':
|
||||
return 'manual_review_required';
|
||||
case 'expired':
|
||||
case 'timeout':
|
||||
return 'expired';
|
||||
@@ -111,63 +111,6 @@
|
||||
return FINAL_STATUSES.has(normalizeContributionStatus(status));
|
||||
}
|
||||
|
||||
function runCapturedCallbackOnce(callbackUrl, executor) {
|
||||
const normalizedUrl = normalizeString(callbackUrl);
|
||||
if (!normalizedUrl) {
|
||||
return Promise.resolve().then(executor);
|
||||
}
|
||||
|
||||
const existingTask = inFlightCapturedCallbackTasks.get(normalizedUrl);
|
||||
if (existingTask) {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
let task = null;
|
||||
task = Promise.resolve()
|
||||
.then(executor)
|
||||
.finally(() => {
|
||||
if (inFlightCapturedCallbackTasks.get(normalizedUrl) === task) {
|
||||
inFlightCapturedCallbackTasks.delete(normalizedUrl);
|
||||
}
|
||||
});
|
||||
inFlightCapturedCallbackTasks.set(normalizedUrl, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
function normalizeContributionSource(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
return normalized === CONTRIBUTION_SOURCE_SUB2API
|
||||
? CONTRIBUTION_SOURCE_SUB2API
|
||||
: CONTRIBUTION_SOURCE_CPA;
|
||||
}
|
||||
|
||||
function resolveContributionRouting(state = {}) {
|
||||
const currentStatus = normalizeContributionStatus(state.contributionStatus);
|
||||
const currentSource = normalizeContributionSource(state.contributionSource);
|
||||
const hasActiveSession = Boolean(
|
||||
normalizeString(state.contributionSessionId)
|
||||
&& currentStatus
|
||||
&& !FINAL_STATUSES.has(currentStatus)
|
||||
);
|
||||
|
||||
if (hasActiveSession) {
|
||||
return {
|
||||
source: currentSource,
|
||||
targetGroupName: currentSource === CONTRIBUTION_SOURCE_SUB2API
|
||||
? (normalizeString(state.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME)
|
||||
: '',
|
||||
};
|
||||
}
|
||||
|
||||
const source = CONTRIBUTION_SOURCE_SUB2API;
|
||||
return {
|
||||
source,
|
||||
targetGroupName: Boolean(state.plusModeEnabled)
|
||||
? CONTRIBUTION_SUB2API_PLUS_GROUP_NAME
|
||||
: (normalizeString(state.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME),
|
||||
};
|
||||
}
|
||||
|
||||
function getStatusLabel(status = '') {
|
||||
switch (normalizeContributionStatus(status)) {
|
||||
case 'started':
|
||||
@@ -175,11 +118,13 @@
|
||||
case 'waiting':
|
||||
return '等待提交回调';
|
||||
case 'processing':
|
||||
return '已提交回调,等待服务端确认';
|
||||
return '已提交回调,等待 CPA 确认';
|
||||
case 'auto_approved':
|
||||
return '贡献成功,服务端已确认';
|
||||
return '贡献成功,CPA 已确认';
|
||||
case 'auto_rejected':
|
||||
return '贡献未通过确认';
|
||||
case 'manual_review_required':
|
||||
return '已提交,等待人工处理';
|
||||
case 'expired':
|
||||
return '贡献会话已超时';
|
||||
case 'error':
|
||||
@@ -315,6 +260,41 @@
|
||||
return qq;
|
||||
}
|
||||
|
||||
function isPlusModeState(state = {}) {
|
||||
return Boolean(state?.plusModeEnabled);
|
||||
}
|
||||
|
||||
function normalizeContributionModeSource(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
return normalized === 'sub2api' ? 'sub2api' : 'cpa';
|
||||
}
|
||||
|
||||
function resolveContributionModeRoutingState(state = {}) {
|
||||
const currentStatus = normalizeString(state?.contributionStatus).toLowerCase();
|
||||
const currentSource = normalizeContributionModeSource(state?.contributionSource);
|
||||
const hasActiveSession = Boolean(
|
||||
normalizeString(state?.contributionSessionId)
|
||||
&& currentStatus
|
||||
&& !FINAL_STATUSES.has(currentStatus)
|
||||
);
|
||||
|
||||
if (hasActiveSession) {
|
||||
return {
|
||||
source: currentSource,
|
||||
targetGroupName: currentSource === 'sub2api'
|
||||
? (normalizeString(state?.contributionTargetGroupName) || 'codex号池')
|
||||
: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
source: 'sub2api',
|
||||
targetGroupName: isPlusModeState(state)
|
||||
? 'openai-plus'
|
||||
: (normalizeString(state?.contributionTargetGroupName) || 'codex号池'),
|
||||
};
|
||||
}
|
||||
|
||||
function buildStatusMessage(status, payload = {}) {
|
||||
const label = getStatusLabel(status);
|
||||
const details = [
|
||||
@@ -494,68 +474,87 @@
|
||||
return currentState;
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'submitting',
|
||||
contributionCallbackMessage: buildCallbackMessage('submitting'),
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = await fetchContributionJson('/submit-callback', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
callback_url: normalizedUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const nextStatus = 'submitted';
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: nextStatus,
|
||||
contributionCallbackMessage: buildCallbackMessage(nextStatus, payload),
|
||||
});
|
||||
|
||||
if (typeof closeLocalhostCallbackTabs === 'function') {
|
||||
await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
|
||||
}
|
||||
|
||||
return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
|
||||
} catch (error) {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'failed',
|
||||
contributionCallbackMessage: `回调提交失败:${error.message}`,
|
||||
});
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:回调提交失败:${error.message}`, 'warn');
|
||||
}
|
||||
|
||||
throw error;
|
||||
const dedupeKey = `${sessionId}::${normalizedUrl}`;
|
||||
if (pendingCallbackSubmissions.has(dedupeKey)) {
|
||||
return pendingCallbackSubmissions.get(dedupeKey);
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'submitting',
|
||||
contributionCallbackMessage: buildCallbackMessage('submitting'),
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = await fetchContributionJson('/submit-callback', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
callback_url: normalizedUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const nextStatus = 'submitted';
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: nextStatus,
|
||||
contributionCallbackMessage: buildCallbackMessage(nextStatus, payload),
|
||||
});
|
||||
|
||||
if (typeof closeLocalhostCallbackTabs === 'function') {
|
||||
await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
|
||||
}
|
||||
|
||||
return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
|
||||
} catch (error) {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'failed',
|
||||
contributionCallbackMessage: `回调提交失败:${error.message}`,
|
||||
});
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:回调提交失败:${error.message}`, 'warn');
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
pendingCallbackSubmissions.delete(dedupeKey);
|
||||
}
|
||||
})();
|
||||
|
||||
pendingCallbackSubmissions.set(dedupeKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async function handleCapturedCallback(rawUrl, metadata = {}) {
|
||||
const currentState = await getState();
|
||||
if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
|
||||
return currentState;
|
||||
}
|
||||
if (!isContributionCallbackUrl(rawUrl, currentState)) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const normalizedUrl = normalizeString(rawUrl);
|
||||
return runCapturedCallbackOnce(normalizedUrl, async () => {
|
||||
const currentState = await getState();
|
||||
if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
|
||||
return currentState;
|
||||
}
|
||||
if (!isContributionCallbackUrl(normalizedUrl, currentState)) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
|
||||
if (
|
||||
normalizedUrl
|
||||
&& normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
|
||||
&& (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
|
||||
) {
|
||||
return currentState;
|
||||
}
|
||||
const callbackDedupeKey = `${normalizeString(currentState.contributionSessionId)}::${normalizedUrl}`;
|
||||
if (pendingCapturedCallbacks.has(callbackDedupeKey)) {
|
||||
return pendingCapturedCallbacks.get(callbackDedupeKey);
|
||||
}
|
||||
if (pendingCallbackSubmissions.has(callbackDedupeKey)) {
|
||||
return pendingCallbackSubmissions.get(callbackDedupeKey);
|
||||
}
|
||||
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
|
||||
if (
|
||||
normalizedUrl
|
||||
&& normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
|
||||
&& (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
|
||||
) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'captured',
|
||||
@@ -573,8 +572,13 @@
|
||||
});
|
||||
} catch {
|
||||
return getState();
|
||||
} finally {
|
||||
pendingCapturedCallbacks.delete(callbackDedupeKey);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
pendingCapturedCallbacks.set(callbackDedupeKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async function pollContributionStatus(options = {}) {
|
||||
@@ -597,16 +601,6 @@
|
||||
const callbackState = deriveCallbackState(mergedPayload, currentState);
|
||||
const updates = {
|
||||
contributionLastPollAt: Date.now(),
|
||||
contributionSource: normalizeContributionSource(
|
||||
mergedPayload.source
|
||||
|| mergedPayload.source_kind
|
||||
|| currentState.contributionSource
|
||||
),
|
||||
contributionTargetGroupName: normalizeString(
|
||||
mergedPayload.target_group_name
|
||||
|| mergedPayload.group_name
|
||||
|| currentState.contributionTargetGroupName
|
||||
),
|
||||
contributionStatus: normalizedStatus,
|
||||
contributionStatusMessage: buildStatusMessage(normalizedStatus, mergedPayload),
|
||||
contributionCallbackUrl: callbackState.callbackUrl,
|
||||
@@ -648,7 +642,6 @@
|
||||
async function startContributionFlow(options = {}) {
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const shouldOpenAuthTab = options.openAuthTab !== false;
|
||||
const routing = resolveContributionRouting(currentState);
|
||||
if (!currentState.contributionMode) {
|
||||
throw new Error('请先进入贡献模式。');
|
||||
}
|
||||
@@ -666,14 +659,15 @@
|
||||
return pollContributionStatus({ reason: 'resume_existing' });
|
||||
}
|
||||
|
||||
const routingState = resolveContributionModeRoutingState(currentState);
|
||||
const payload = await fetchContributionJson('/start', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
nickname: buildNickname(currentState, options.nickname),
|
||||
qq: buildContributionQq(currentState, options.qq),
|
||||
email: normalizeString(currentState.email),
|
||||
source: routing.source,
|
||||
target_group_name: routing.targetGroupName,
|
||||
source: routingState.source,
|
||||
target_group_name: routingState.targetGroupName,
|
||||
channel: 'codex-extension',
|
||||
},
|
||||
});
|
||||
@@ -686,12 +680,6 @@
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionSource: normalizeContributionSource(payload.source || routing.source),
|
||||
contributionTargetGroupName: normalizeString(
|
||||
payload.target_group_name
|
||||
|| payload.group_name
|
||||
|| routing.targetGroupName
|
||||
),
|
||||
contributionSessionId: sessionId,
|
||||
contributionAuthUrl: authUrl,
|
||||
contributionAuthState: authState,
|
||||
@@ -722,7 +710,7 @@
|
||||
}
|
||||
|
||||
function onTabUpdated(tabId, changeInfo, tab) {
|
||||
const candidateUrl = normalizeString(changeInfo?.url);
|
||||
const candidateUrl = normalizeString(changeInfo?.url || tab?.url);
|
||||
if (!candidateUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,11 +32,14 @@
|
||||
executeStepViaCompletionSignal,
|
||||
exportSettingsBundle,
|
||||
fetchGeneratedEmail,
|
||||
finalizePhoneActivationAfterSuccessfulFlow,
|
||||
finalizeStep3Completion,
|
||||
finalizeIcloudAliasAfterSuccessfulFlow,
|
||||
findHotmailAccount,
|
||||
findPayPalAccount,
|
||||
flushCommand,
|
||||
getCurrentLuckmailPurchase,
|
||||
getCurrentPayPalAccount,
|
||||
getCurrentMail2925Account,
|
||||
getPendingAutoRunTimerPlan,
|
||||
getSourceLabel,
|
||||
@@ -62,6 +65,7 @@
|
||||
refreshIpProxyPool,
|
||||
normalizeHotmailAccounts,
|
||||
normalizeMail2925Accounts,
|
||||
normalizePayPalAccounts,
|
||||
normalizeRunCount,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||||
notifyStepComplete,
|
||||
@@ -79,6 +83,7 @@
|
||||
selectLuckmailPurchase,
|
||||
switchIpProxy,
|
||||
changeIpProxyExit,
|
||||
setCurrentPayPalAccount,
|
||||
setCurrentMail2925Account,
|
||||
setCurrentHotmailAccount,
|
||||
setContributionMode,
|
||||
@@ -99,7 +104,9 @@
|
||||
deleteMail2925Account,
|
||||
deleteMail2925Accounts,
|
||||
syncHotmailAccounts,
|
||||
syncPayPalAccounts,
|
||||
testHotmailAccountMailAccess,
|
||||
upsertPayPalAccount,
|
||||
upsertMail2925Account,
|
||||
upsertHotmailAccount,
|
||||
verifyHotmailAccount,
|
||||
@@ -203,9 +210,33 @@
|
||||
});
|
||||
}
|
||||
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
|
||||
if (typeof finalizePhoneActivationAfterSuccessfulFlow === 'function') {
|
||||
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStepData(step, payload) {
|
||||
if (step === 1) {
|
||||
const updates = {};
|
||||
if (payload.oauthUrl) {
|
||||
updates.oauthUrl = payload.oauthUrl;
|
||||
broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
|
||||
}
|
||||
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||||
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
|
||||
if (payload.cpaManagementOrigin !== undefined) updates.cpaManagementOrigin = payload.cpaManagementOrigin || null;
|
||||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||||
if (Object.keys(updates).length) {
|
||||
await setState(updates);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const stateForStep = await getState();
|
||||
const stepKey = getStepKeyForState(step, stateForStep);
|
||||
|
||||
@@ -252,24 +283,6 @@
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 1: {
|
||||
const updates = {};
|
||||
if (payload.oauthUrl) {
|
||||
updates.oauthUrl = payload.oauthUrl;
|
||||
broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
|
||||
}
|
||||
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||||
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||||
if (Object.keys(updates).length) {
|
||||
await setState(updates);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
if (payload.email) {
|
||||
await setEmailState(payload.email);
|
||||
@@ -779,6 +792,16 @@
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'UPSERT_PAYPAL_ACCOUNT': {
|
||||
const account = await upsertPayPalAccount(message.payload || {});
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'SELECT_PAYPAL_ACCOUNT': {
|
||||
const account = await setCurrentPayPalAccount(String(message.payload?.accountId || ''));
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'DELETE_HOTMAIL_ACCOUNT': {
|
||||
await deleteHotmailAccount(String(message.payload?.accountId || ''));
|
||||
return { ok: true };
|
||||
|
||||
+108
-37
@@ -43,6 +43,78 @@
|
||||
return message || `Codex2API 请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
function deriveCpaManagementOrigin(vpsUrl) {
|
||||
const normalizedUrl = String(vpsUrl || '').trim();
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(normalizedUrl);
|
||||
} catch {
|
||||
throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
function getCpaApiErrorMessage(payload, responseStatus = 500) {
|
||||
const candidates = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
];
|
||||
const message = candidates
|
||||
.map((value) => String(value || '').trim())
|
||||
.find(Boolean);
|
||||
return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchCpaManagementJson(origin, path, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const managementKey = String(options.managementKey || '').trim();
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (managementKey) {
|
||||
headers.Authorization = `Bearer ${managementKey}`;
|
||||
headers['X-Management-Key'] = managementKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCpaApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('CPA 管理接口请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCodex2ApiJson(origin, path, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
const controller = new AbortController();
|
||||
@@ -97,49 +169,48 @@
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await addLog(`${logLabel}:正在打开 CPA 面板...`);
|
||||
|
||||
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
|
||||
await closeConflictingTabsForSource('vps-panel', state.vpsUrl);
|
||||
|
||||
const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true });
|
||||
const tabId = tab.id;
|
||||
await rememberSourceLastUrl('vps-panel', state.vpsUrl);
|
||||
|
||||
await addLog(`${logLabel}:CPA 面板已打开,正在等待页面进入目标地址...`);
|
||||
const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, {
|
||||
timeoutMs: 15000,
|
||||
retryDelayMs: 400,
|
||||
});
|
||||
if (!matchedTab) {
|
||||
await addLog(`${logLabel}:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...`, 'warn');
|
||||
const managementKey = String(state.vpsPassword || '').trim();
|
||||
if (!managementKey) {
|
||||
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('vps-panel', tabId, {
|
||||
inject: injectFiles,
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `${logLabel}:CPA 面板仍在加载,正在重试连接内容脚本...`,
|
||||
const origin = deriveCpaManagementOrigin(state.vpsUrl);
|
||||
|
||||
await addLog(`${logLabel}:正在通过 CPA 管理接口获取 OAuth 授权链接...`);
|
||||
const result = await fetchCpaManagementJson(origin, '/v0/management/codex-auth-url', {
|
||||
method: 'GET',
|
||||
managementKey,
|
||||
});
|
||||
|
||||
const result = await sendToContentScriptResilient('vps-panel', {
|
||||
type: 'REQUEST_OAUTH_URL',
|
||||
source: 'background',
|
||||
payload: {
|
||||
vpsPassword: state.vpsPassword,
|
||||
logStep: 7,
|
||||
},
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `${logLabel}:CPA 面板通信未就绪,正在等待页面恢复...`,
|
||||
});
|
||||
const oauthUrl = String(
|
||||
result?.url
|
||||
|| result?.auth_url
|
||||
|| result?.authUrl
|
||||
|| result?.data?.url
|
||||
|| result?.data?.auth_url
|
||||
|| result?.data?.authUrl
|
||||
|| ''
|
||||
).trim();
|
||||
const oauthState = String(
|
||||
result?.state
|
||||
|| result?.auth_state
|
||||
|| result?.authState
|
||||
|| result?.data?.state
|
||||
|| result?.data?.auth_state
|
||||
|| result?.data?.authState
|
||||
|| ''
|
||||
).trim()
|
||||
|| extractStateFromAuthUrl(oauthUrl);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
if (!oauthUrl || !oauthUrl.startsWith('http')) {
|
||||
throw new Error('CPA 管理接口未返回有效的 auth_url。');
|
||||
}
|
||||
return result || {};
|
||||
|
||||
return {
|
||||
oauthUrl,
|
||||
cpaOAuthState: oauthState || null,
|
||||
cpaManagementOrigin: origin,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestCodex2ApiOAuthUrl(state, options = {}) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
(function attachBackgroundPayPalAccountStore(root, factory) {
|
||||
root.MultiPageBackgroundPayPalAccountStore = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPayPalAccountStoreModule() {
|
||||
function createPayPalAccountStore(deps = {}) {
|
||||
const {
|
||||
broadcastDataUpdate,
|
||||
findPayPalAccount,
|
||||
getState,
|
||||
normalizePayPalAccount,
|
||||
normalizePayPalAccounts,
|
||||
setPersistentSettings,
|
||||
setState,
|
||||
upsertPayPalAccountInList,
|
||||
} = deps;
|
||||
|
||||
async function syncSelectedPayPalAccountState(account = null) {
|
||||
const updates = account
|
||||
? {
|
||||
currentPayPalAccountId: account.id,
|
||||
paypalEmail: String(account.email || '').trim(),
|
||||
paypalPassword: String(account.password || ''),
|
||||
}
|
||||
: {
|
||||
currentPayPalAccountId: '',
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
};
|
||||
|
||||
await setPersistentSettings(updates);
|
||||
await setState({
|
||||
currentPayPalAccountId: updates.currentPayPalAccountId || null,
|
||||
paypalEmail: updates.paypalEmail,
|
||||
paypalPassword: updates.paypalPassword,
|
||||
});
|
||||
broadcastDataUpdate({
|
||||
currentPayPalAccountId: updates.currentPayPalAccountId || null,
|
||||
paypalEmail: updates.paypalEmail,
|
||||
paypalPassword: updates.paypalPassword,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncPayPalAccounts(accounts) {
|
||||
const normalized = normalizePayPalAccounts(accounts);
|
||||
await setPersistentSettings({ paypalAccounts: normalized });
|
||||
await setState({ paypalAccounts: normalized });
|
||||
broadcastDataUpdate({ paypalAccounts: normalized });
|
||||
|
||||
const state = await getState();
|
||||
if (state.currentPayPalAccountId && !findPayPalAccount(normalized, state.currentPayPalAccountId)) {
|
||||
await syncSelectedPayPalAccountState(null);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getCurrentPayPalAccount(state = {}) {
|
||||
return findPayPalAccount(state.paypalAccounts, state.currentPayPalAccountId) || null;
|
||||
}
|
||||
|
||||
async function upsertPayPalAccount(input = {}) {
|
||||
const state = await getState();
|
||||
const accounts = normalizePayPalAccounts(state.paypalAccounts);
|
||||
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
|
||||
const existing = input?.id
|
||||
? findPayPalAccount(accounts, input.id)
|
||||
: accounts.find((account) => account.email === normalizedEmail) || null;
|
||||
const normalized = normalizePayPalAccount({
|
||||
...(existing || {}),
|
||||
...input,
|
||||
id: input?.id || existing?.id || crypto.randomUUID(),
|
||||
createdAt: existing?.createdAt || Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
const nextAccounts = typeof upsertPayPalAccountInList === 'function'
|
||||
? upsertPayPalAccountInList(accounts, normalized)
|
||||
: accounts.concat(normalized);
|
||||
|
||||
await syncPayPalAccounts(nextAccounts);
|
||||
|
||||
if (state.currentPayPalAccountId === normalized.id) {
|
||||
await syncSelectedPayPalAccountState(normalized);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function setCurrentPayPalAccount(accountId) {
|
||||
const state = await getState();
|
||||
const accounts = normalizePayPalAccounts(state.paypalAccounts);
|
||||
const account = findPayPalAccount(accounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到对应的 PayPal 账号。');
|
||||
}
|
||||
|
||||
await syncSelectedPayPalAccountState(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentPayPalAccount,
|
||||
setCurrentPayPalAccount,
|
||||
syncPayPalAccounts,
|
||||
upsertPayPalAccount,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPayPalAccountStore,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
prepareStep8DebuggerClick,
|
||||
recoverOAuthLocalhostTimeout,
|
||||
reloadStep8ConsentPage,
|
||||
reuseOrCreateTab,
|
||||
sleepWithStop,
|
||||
@@ -44,19 +45,43 @@
|
||||
|
||||
async function executeStep9(state) {
|
||||
const visibleStep = getVisibleStep(state, 9);
|
||||
if (!state.oauthUrl) {
|
||||
let activeState = state;
|
||||
|
||||
if (!activeState.oauthUrl) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`);
|
||||
|
||||
const callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(240000, {
|
||||
step: visibleStep,
|
||||
actionLabel: 'OAuth localhost 回调',
|
||||
})
|
||||
: 240000;
|
||||
let callbackTimeoutMs = 240000;
|
||||
let timeoutRecoveryAttempted = false;
|
||||
while (true) {
|
||||
try {
|
||||
callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(240000, {
|
||||
step: visibleStep,
|
||||
actionLabel: 'OAuth localhost 回调',
|
||||
oauthUrl: activeState?.oauthUrl || '',
|
||||
})
|
||||
: 240000;
|
||||
break;
|
||||
} catch (error) {
|
||||
if (timeoutRecoveryAttempted || typeof recoverOAuthLocalhostTimeout !== 'function') {
|
||||
throw error;
|
||||
}
|
||||
const recoveredState = await recoverOAuthLocalhostTimeout({
|
||||
error,
|
||||
state: activeState,
|
||||
visibleStep,
|
||||
});
|
||||
if (!recoveredState) {
|
||||
throw error;
|
||||
}
|
||||
activeState = recoveredState;
|
||||
timeoutRecoveryAttempted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false;
|
||||
@@ -124,7 +149,7 @@
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await addLog(`步骤 ${visibleStep}:已切回认证页,正在准备调试器点击...`);
|
||||
} else {
|
||||
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
signupTabId = await reuseOrCreateTab('signup-page', activeState.oauthUrl);
|
||||
await addLog(`步骤 ${visibleStep}:已重新打开认证页,正在准备调试器点击...`);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
reuseOrCreateTab,
|
||||
setState,
|
||||
shouldUseCustomRegistrationEmail,
|
||||
sleepWithStop,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||
throwIfStopped,
|
||||
@@ -167,10 +168,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function runStep8Attempt(state) {
|
||||
function getStep8ResendIntervalMs(state = {}) {
|
||||
const mail = getMailConfig(state);
|
||||
if (mail?.provider === LUCKMAIL_PROVIDER) {
|
||||
return 15000;
|
||||
}
|
||||
if (mail?.provider === HOTMAIL_PROVIDER || mail?.provider === '2925') {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
|
||||
}
|
||||
|
||||
async function runStep8Attempt(state, runtime = {}) {
|
||||
const visibleStep = getVisibleStep(state, 8);
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
|
||||
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
|
||||
? runtime.onResendRequestedAt
|
||||
: null;
|
||||
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
@@ -267,6 +284,16 @@
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
|
||||
requestFreshCodeFirst: false,
|
||||
lastResendAt: latestResendAt,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
const numericRequestedAt = Number(requestedAt) || 0;
|
||||
if (numericRequestedAt > 0) {
|
||||
latestResendAt = Math.max(latestResendAt, numericRequestedAt);
|
||||
}
|
||||
if (notifyResendRequestedAt) {
|
||||
await notifyResendRequestedAt(latestResendAt);
|
||||
}
|
||||
},
|
||||
targetEmail: fixedTargetEmail,
|
||||
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
|
||||
? 15000
|
||||
@@ -274,6 +301,9 @@
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
|
||||
});
|
||||
return {
|
||||
lastResendAt: latestResendAt,
|
||||
};
|
||||
}
|
||||
|
||||
function isStep8RestartStep7Error(error) {
|
||||
@@ -285,10 +315,22 @@
|
||||
let currentState = state;
|
||||
let mailPollingAttempt = 1;
|
||||
let lastMailPollingError = null;
|
||||
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await runStep8Attempt(currentState);
|
||||
const result = await runStep8Attempt(currentState, {
|
||||
stickyLastResendAt,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
const numericRequestedAt = Number(requestedAt) || 0;
|
||||
if (numericRequestedAt > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, numericRequestedAt);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (Number(result?.lastResendAt) > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, Number(result.lastResendAt) || 0);
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
const visibleStep = getVisibleStep(currentState, 8);
|
||||
@@ -323,7 +365,29 @@
|
||||
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}。`,
|
||||
'warn'
|
||||
);
|
||||
currentState = await getState();
|
||||
const latestState = await getState();
|
||||
const latestStateResendAt = Number(latestState?.loginVerificationRequestedAt) || 0;
|
||||
if (latestStateResendAt > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, latestStateResendAt);
|
||||
}
|
||||
currentState = latestState;
|
||||
if (stickyLastResendAt > 0 && (!latestStateResendAt || latestStateResendAt < stickyLastResendAt)) {
|
||||
currentState = {
|
||||
...latestState,
|
||||
loginVerificationRequestedAt: stickyLastResendAt,
|
||||
};
|
||||
}
|
||||
const resendIntervalMs = getStep8ResendIntervalMs(currentState);
|
||||
const remainingBeforeRetryMs = stickyLastResendAt > 0 && resendIntervalMs > 0
|
||||
? Math.max(0, resendIntervalMs - (Date.now() - stickyLastResendAt))
|
||||
: 0;
|
||||
if (remainingBeforeRetryMs > 0 && typeof sleepWithStop === 'function') {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:上轮已触发重发验证码,为避免重复重发,先等待 ${Math.ceil(remainingBeforeRetryMs / 1000)} 秒后继续当前链路重试。`,
|
||||
'info'
|
||||
);
|
||||
await sleepWithStop(Math.min(remainingBeforeRetryMs, 3000));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
await addLog(
|
||||
|
||||
@@ -40,17 +40,14 @@
|
||||
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
|
||||
}
|
||||
|
||||
function getVisibleStep(state, fallback = 7) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : fallback;
|
||||
}
|
||||
|
||||
async function executeStep7(state) {
|
||||
const visibleStep = getVisibleStep(state, 7);
|
||||
if (!state.email) {
|
||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||
}
|
||||
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
||||
|
||||
let attempt = 0;
|
||||
let lastError = null;
|
||||
|
||||
@@ -60,22 +57,22 @@
|
||||
try {
|
||||
const currentState = attempt === 1 ? state : await getState();
|
||||
const password = currentState.password || currentState.customPassword || '';
|
||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState, { visibleStep });
|
||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
||||
if (typeof startOAuthFlowTimeoutWindow === 'function') {
|
||||
await startOAuthFlowTimeoutWindow({ step: visibleStep, oauthUrl });
|
||||
await startOAuthFlowTimeoutWindow({ step: 7, oauthUrl });
|
||||
}
|
||||
const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(180000, {
|
||||
step: visibleStep,
|
||||
step: 7,
|
||||
actionLabel: 'OAuth 登录并进入验证码页',
|
||||
oauthUrl,
|
||||
})
|
||||
: 180000;
|
||||
|
||||
if (attempt === 1) {
|
||||
await addLog(`步骤 ${visibleStep}:正在打开最新 OAuth 链接并登录...`);
|
||||
await addLog('步骤 7:正在打开最新 OAuth 链接并登录...');
|
||||
} else {
|
||||
await addLog(`步骤 ${visibleStep}:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
|
||||
await addLog(`步骤 7:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
|
||||
}
|
||||
|
||||
await reuseOrCreateTab('signup-page', oauthUrl);
|
||||
@@ -89,14 +86,13 @@
|
||||
payload: {
|
||||
email: currentState.email,
|
||||
password,
|
||||
visibleStep,
|
||||
},
|
||||
},
|
||||
{
|
||||
timeoutMs: loginTimeoutMs,
|
||||
responseTimeoutMs: loginTimeoutMs,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪后继续登录...`,
|
||||
logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -108,13 +104,14 @@
|
||||
const completionPayload = {
|
||||
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
|
||||
};
|
||||
if (result.skipLoginVerificationStep) {
|
||||
completionPayload.skipLoginVerificationStep = true;
|
||||
if (Object.prototype.hasOwnProperty.call(result || {}, 'skipLoginVerificationStep')) {
|
||||
completionPayload.skipLoginVerificationStep = Boolean(result.skipLoginVerificationStep);
|
||||
}
|
||||
if (result.directOAuthConsentPage) {
|
||||
completionPayload.directOAuthConsentPage = true;
|
||||
if (Object.prototype.hasOwnProperty.call(result || {}, 'directOAuthConsentPage')) {
|
||||
completionPayload.directOAuthConsentPage = Boolean(result.directOAuthConsentPage);
|
||||
}
|
||||
await completeStepFromBackground(visibleStep, completionPayload);
|
||||
|
||||
await completeStepFromBackground(completionStep, completionPayload);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +129,7 @@
|
||||
}
|
||||
if (isManagementSecretConfigError(err)) {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||
`步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||
'error'
|
||||
);
|
||||
throw err;
|
||||
@@ -142,11 +139,11 @@
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
|
||||
await addLog(`步骤 7:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`步骤 ${visibleStep}:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
||||
throw new Error(`步骤 7:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
||||
}
|
||||
|
||||
return { executeStep7 };
|
||||
|
||||
@@ -99,17 +99,30 @@
|
||||
return result || {};
|
||||
}
|
||||
|
||||
function resolvePayPalCredentials(state = {}) {
|
||||
const currentId = String(state?.currentPayPalAccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : [];
|
||||
const selectedAccount = currentId
|
||||
? accounts.find((account) => String(account?.id || '').trim() === currentId) || null
|
||||
: null;
|
||||
return {
|
||||
email: String(selectedAccount?.email || state?.paypalEmail || '').trim(),
|
||||
password: String(selectedAccount?.password || state?.paypalPassword || ''),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitLogin(tabId, state = {}) {
|
||||
if (!state.paypalPassword) {
|
||||
throw new Error('步骤 8:未配置 PayPal 密码,请先在侧边栏填写。');
|
||||
const credentials = resolvePayPalCredentials(state);
|
||||
if (!credentials.password) {
|
||||
throw new Error('步骤 8:未配置可用的 PayPal 账号,请先在侧边栏添加并选择账号。');
|
||||
}
|
||||
await addLog('步骤 8:正在填写 PayPal 登录信息并提交...', 'info');
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_SUBMIT_LOGIN',
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: state.paypalEmail || '',
|
||||
password: state.paypalPassword || '',
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
},
|
||||
});
|
||||
if (result?.error) {
|
||||
|
||||
@@ -26,31 +26,23 @@
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function getVisibleStep(state, fallback = 10) {
|
||||
function resolvePlatformVerifyStep(state = {}) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : fallback;
|
||||
return visibleStep >= 10 ? visibleStep : 10;
|
||||
}
|
||||
|
||||
function getConfirmStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 13 ? 12 : 9;
|
||||
}
|
||||
|
||||
function getAuthLoginStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 13 ? 10 : 7;
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl, visibleStep = 10, confirmStep = 9) {
|
||||
function parseLocalhostCallback(rawUrl) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。');
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const state = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !state) {
|
||||
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -72,6 +64,77 @@
|
||||
return details || `Codex2API 请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
function deriveCpaManagementOrigin(vpsUrl) {
|
||||
const normalizedUrl = normalizeString(vpsUrl);
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(normalizedUrl);
|
||||
} catch {
|
||||
throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
function getCpaApiErrorMessage(payload, responseStatus = 500) {
|
||||
const details = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
]
|
||||
.map((value) => normalizeString(value))
|
||||
.find(Boolean);
|
||||
return details || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchCpaManagementJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const managementKey = normalizeString(options.managementKey);
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (managementKey) {
|
||||
headers.Authorization = `Bearer ${managementKey}`;
|
||||
headers['X-Management-Key'] = managementKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCpaApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('CPA 管理接口请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCodex2ApiJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
@@ -122,95 +185,88 @@
|
||||
}
|
||||
|
||||
async function executeCpaStep10(state) {
|
||||
const visibleStep = getVisibleStep(state, 10);
|
||||
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
}
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||
}
|
||||
|
||||
if (shouldBypassStep9ForLocalCpa(state)) {
|
||||
await addLog(`步骤 ${visibleStep}:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。`, 'info');
|
||||
await completeStepFromBackground(visibleStep, {
|
||||
await addLog('步骤 10:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。', 'info');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: state.localhostUrl,
|
||||
verifiedStatus: 'local-auto',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在打开 CPA 面板...`);
|
||||
|
||||
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
|
||||
let tabId = await getTabId('vps-panel');
|
||||
const alive = tabId && await isTabAlive('vps-panel');
|
||||
|
||||
if (!alive) {
|
||||
tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, {
|
||||
inject: injectFiles,
|
||||
reloadIfSameUrl: true,
|
||||
});
|
||||
} else {
|
||||
await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] });
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
await rememberSourceLastUrl('vps-panel', state.vpsUrl);
|
||||
const callback = parseLocalhostCallback(state.localhostUrl);
|
||||
const expectedState = normalizeString(state.cpaOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
|
||||
}
|
||||
const managementKey = normalizeString(state.vpsPassword);
|
||||
if (!managementKey) {
|
||||
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('vps-panel', tabId, {
|
||||
inject: injectFiles,
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `步骤 ${visibleStep}:CPA 面板仍在加载,正在重试连接...`,
|
||||
});
|
||||
await addLog('步骤 10:正在通过 CPA 管理接口提交回调地址...');
|
||||
try {
|
||||
const origin = normalizeString(state.cpaManagementOrigin) || deriveCpaManagementOrigin(state.vpsUrl);
|
||||
const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', {
|
||||
method: 'POST',
|
||||
managementKey,
|
||||
body: {
|
||||
provider: 'codex',
|
||||
redirect_url: callback.url,
|
||||
},
|
||||
});
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在填写回调地址...`);
|
||||
const result = await sendToContentScriptResilient('vps-panel', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: visibleStep,
|
||||
source: 'background',
|
||||
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword, visibleStep },
|
||||
}, {
|
||||
timeoutMs: 125000,
|
||||
responseTimeoutMs: 125000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${visibleStep}:CPA 面板通信未就绪,正在等待页面恢复...`,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
const verifiedStatus = normalizeString(result?.message)
|
||||
|| normalizeString(result?.status_message)
|
||||
|| 'CPA 已通过接口提交回调';
|
||||
await addLog(`步骤 10:${verifiedStatus}`, 'ok');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
} catch (error) {
|
||||
const reason = normalizeString(error?.message) || 'unknown error';
|
||||
await addLog(`步骤 10:CPA 接口提交失败:${reason}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCodex2ApiStep10(state) {
|
||||
const visibleStep = getVisibleStep(state, 10);
|
||||
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
}
|
||||
if (!state.codex2apiSessionId) {
|
||||
throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
|
||||
throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。');
|
||||
}
|
||||
if (!normalizeString(state.codex2apiAdminKey)) {
|
||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const callback = parseLocalhostCallback(state.localhostUrl, visibleStep, confirmStep);
|
||||
const callback = parseLocalhostCallback(state.localhostUrl);
|
||||
const expectedState = normalizeString(state.codex2apiOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
|
||||
throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
|
||||
}
|
||||
|
||||
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
||||
const origin = new URL(codex2apiUrl).origin;
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在向 Codex2API 提交回调并创建账号...`);
|
||||
await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...');
|
||||
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
|
||||
adminKey: state.codex2apiAdminKey,
|
||||
method: 'POST',
|
||||
@@ -222,21 +278,19 @@
|
||||
});
|
||||
|
||||
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
|
||||
await addLog(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok');
|
||||
await completeStepFromBackground(visibleStep, {
|
||||
await addLog(`步骤 10:${verifiedStatus}`, 'ok');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSub2ApiStep10(state) {
|
||||
const visibleStep = getVisibleStep(state, 10);
|
||||
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
}
|
||||
if (!state.sub2apiSessionId) {
|
||||
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
|
||||
@@ -251,7 +305,7 @@
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在打开 SUB2API 后台...`);
|
||||
await addLog('步骤 10:正在打开 SUB2API 后台...');
|
||||
|
||||
let tabId = await getTabId('sub2api-panel');
|
||||
const alive = tabId && await isTabAlive('sub2api-panel');
|
||||
@@ -273,13 +327,12 @@
|
||||
injectSource: 'sub2api-panel',
|
||||
});
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`);
|
||||
await addLog('步骤 10:正在向 SUB2API 提交回调并创建账号...');
|
||||
const result = await sendToContentScript('sub2api-panel', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: visibleStep,
|
||||
step: 10,
|
||||
source: 'background',
|
||||
payload: {
|
||||
visibleStep,
|
||||
localhostUrl: state.localhostUrl,
|
||||
sub2apiUrl,
|
||||
sub2apiEmail: state.sub2apiEmail,
|
||||
|
||||
@@ -222,6 +222,28 @@
|
||||
return Math.min(20, Math.max(0, Math.floor(numeric)));
|
||||
}
|
||||
|
||||
function getVerificationRequestedAtStateKey(step) {
|
||||
if (Number(step) === 4) return 'signupVerificationRequestedAt';
|
||||
if (Number(step) === 8) return 'loginVerificationRequestedAt';
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveInitialVerificationRequestedAt(step, state = {}, fallback = 0) {
|
||||
const stateKey = getVerificationRequestedAtStateKey(step);
|
||||
const candidateValues = [
|
||||
fallback,
|
||||
stateKey ? state?.[stateKey] : 0,
|
||||
];
|
||||
|
||||
for (const value of candidateValues) {
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric) && numeric > 0) {
|
||||
return Math.floor(numeric);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getLegacyVerificationResendCountDefault(step, options = {}) {
|
||||
const requestFreshCodeFirst = Boolean(options.requestFreshCodeFirst);
|
||||
const legacyMaxRounds = Math.max(1, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1));
|
||||
@@ -985,9 +1007,23 @@
|
||||
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
|
||||
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
|
||||
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
|
||||
let lastResendAt = Number(options.lastResendAt) || 0;
|
||||
const externalOnResendRequestedAt = typeof options.onResendRequestedAt === 'function'
|
||||
? options.onResendRequestedAt
|
||||
: null;
|
||||
let lastResendAt = resolveInitialVerificationRequestedAt(
|
||||
step,
|
||||
state,
|
||||
Number(options.lastResendAt) || 0
|
||||
);
|
||||
|
||||
const updateFilterAfterTimestampForVerificationStep = async (_requestedAt) => {
|
||||
const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
|
||||
if (externalOnResendRequestedAt) {
|
||||
try {
|
||||
await externalOnResendRequestedAt(requestedAt);
|
||||
} catch (_) {
|
||||
// Keep resend flow best-effort; state sync callback failures should not break verification.
|
||||
}
|
||||
}
|
||||
return nextFilterAfterTimestamp;
|
||||
};
|
||||
|
||||
|
||||
+16
-8
@@ -227,6 +227,18 @@ function getPayPalLoginPhase(emailInput, passwordInput) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function refillPayPalEmailInput(emailInput, email) {
|
||||
if (!emailInput) return;
|
||||
if (typeof emailInput.focus === 'function') {
|
||||
emailInput.focus();
|
||||
}
|
||||
fillInput(emailInput, '');
|
||||
fillInput(emailInput, email);
|
||||
if (typeof emailInput.blur === 'function') {
|
||||
emailInput.blur();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPayPalLogin(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
|
||||
@@ -241,9 +253,7 @@ async function submitPayPalLogin(payload = {}) {
|
||||
const emailNextButton = findEmailNextButton();
|
||||
|
||||
if (emailInput && emailNextButton && isEnabledControl(emailNextButton) && (!passwordInput || !findPasswordLoginButton())) {
|
||||
if (normalizeText(emailInput.value || '') !== email) {
|
||||
fillInput(emailInput, email);
|
||||
}
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
simulateClick(emailNextButton);
|
||||
return {
|
||||
submitted: false,
|
||||
@@ -253,9 +263,7 @@ async function submitPayPalLogin(payload = {}) {
|
||||
}
|
||||
|
||||
if (!passwordInput && emailInput && email) {
|
||||
if (normalizeText(emailInput.value || '') !== email) {
|
||||
fillInput(emailInput, email);
|
||||
}
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
const nextButton = await waitUntil(() => {
|
||||
const button = findEmailNextButton() || findLoginNextButton();
|
||||
return button && isEnabledControl(button) ? button : null;
|
||||
@@ -272,8 +280,8 @@ async function submitPayPalLogin(payload = {}) {
|
||||
};
|
||||
} else if (!passwordInput && emailInput && !email) {
|
||||
throw new Error('PayPal 账号为空,请先在侧边栏配置。');
|
||||
} else if (emailInput && email && normalizeText(emailInput.value || '') !== email) {
|
||||
fillInput(emailInput, email);
|
||||
} else if (emailInput && email) {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
}
|
||||
|
||||
passwordInput = passwordInput || await waitUntil(() => findPasswordInput(), {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
throwIfStopped,
|
||||
waitForElement,
|
||||
} = deps;
|
||||
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
|
||||
const PHONE_RESEND_THROTTLED_PATTERN = /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试|重试次数过多/i;
|
||||
|
||||
function dispatchInputEvents(element) {
|
||||
if (!element) return;
|
||||
@@ -312,6 +314,90 @@
|
||||
return matches?.[0] ? matches[0].replace(/\s+/g, ' ').trim() : '';
|
||||
}
|
||||
|
||||
function getAddPhoneErrorText() {
|
||||
const form = getAddPhoneForm();
|
||||
if (!form) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const messages = [];
|
||||
const selectors = [
|
||||
'.react-aria-FieldError',
|
||||
'[slot="errorMessage"]',
|
||||
'[id$="-error"]',
|
||||
'[data-invalid="true"] + *',
|
||||
'[aria-invalid="true"] + *',
|
||||
'[class*="error"]',
|
||||
];
|
||||
for (const selector of selectors) {
|
||||
form.querySelectorAll(selector).forEach((el) => {
|
||||
const text = String(el?.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
if (text) {
|
||||
messages.push(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const invalidInput = form.querySelector('input[aria-invalid="true"], input[data-invalid="true"]');
|
||||
if (invalidInput) {
|
||||
const wrapper = invalidInput.closest('form, [data-rac], div');
|
||||
const text = String(wrapper?.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
if (text) {
|
||||
messages.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
const preferred = messages.find((text) => (
|
||||
/already|used|linked|eligible|invalid|phone|号码|手机号|错误|失败|try\s+again/i.test(text)
|
||||
));
|
||||
return preferred || messages[0] || '';
|
||||
}
|
||||
|
||||
function getPhoneVerificationInlineMessages() {
|
||||
const form = getPhoneVerificationForm();
|
||||
if (!form) {
|
||||
return [];
|
||||
}
|
||||
const messages = [];
|
||||
const selectors = [
|
||||
'.react-aria-FieldError',
|
||||
'[slot="errorMessage"]',
|
||||
'[id$="-error"]',
|
||||
'[data-invalid="true"] + *',
|
||||
'[aria-invalid="true"] + *',
|
||||
'[class*="error"]',
|
||||
];
|
||||
for (const selector of selectors) {
|
||||
form.querySelectorAll(selector).forEach((element) => {
|
||||
const text = String(element?.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
if (text) {
|
||||
messages.push(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
const verificationError = String(getVerificationErrorText?.() || '').trim();
|
||||
if (verificationError) {
|
||||
messages.push(verificationError);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
function getPhoneResendThrottleText() {
|
||||
const inlineMatch = getPhoneVerificationInlineMessages()
|
||||
.find((text) => PHONE_RESEND_THROTTLED_PATTERN.test(text));
|
||||
if (inlineMatch) {
|
||||
return inlineMatch;
|
||||
}
|
||||
const pageSnapshot = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim();
|
||||
if (pageSnapshot && PHONE_RESEND_THROTTLED_PATTERN.test(pageSnapshot)) {
|
||||
const concise = pageSnapshot.match(
|
||||
/tried\s+to\s+resend\s+too\s+many\s+times[^.。!?]*[.。!?]?|please\s+try\s+again\s+later[^.。!?]*[.。!?]?|发送.*过于频繁[^。!?]*[。!?]?|稍后再试[^。!?]*[。!?]?/i
|
||||
);
|
||||
return String(concise?.[0] || pageSnapshot).trim();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function waitForAddPhoneReady(timeout = 20000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
@@ -335,8 +421,28 @@
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
if (isAddPhonePageReady()) {
|
||||
const errorText = getAddPhoneErrorText();
|
||||
if (errorText) {
|
||||
return {
|
||||
addPhoneRejected: true,
|
||||
errorText,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
}
|
||||
await sleep(150);
|
||||
}
|
||||
if (isAddPhonePageReady()) {
|
||||
const errorText = getAddPhoneErrorText();
|
||||
if (errorText) {
|
||||
return {
|
||||
addPhoneRejected: true,
|
||||
errorText,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error('Timed out waiting for phone verification page.');
|
||||
}
|
||||
|
||||
@@ -466,11 +572,19 @@
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
const throttledText = getPhoneResendThrottleText();
|
||||
if (throttledText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`);
|
||||
}
|
||||
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
||||
if (resendButton && isActionEnabled(resendButton)) {
|
||||
await humanPause(250, 700);
|
||||
simulateClick(resendButton);
|
||||
await sleep(1000);
|
||||
const afterClickThrottleText = getPhoneResendThrottleText();
|
||||
if (afterClickThrottleText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`);
|
||||
}
|
||||
return {
|
||||
resent: true,
|
||||
url: location.href,
|
||||
@@ -479,6 +593,11 @@
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
const timeoutThrottleText = getPhoneResendThrottleText();
|
||||
if (timeoutThrottleText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`);
|
||||
}
|
||||
|
||||
throw new Error('Timed out waiting for the phone verification resend button.');
|
||||
}
|
||||
|
||||
|
||||
+201
-7
@@ -1,6 +1,6 @@
|
||||
# Codex 注册扩展相关项目、更新、邮箱、PayPal 与 Clash Verge 配置教程
|
||||
|
||||
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email`、`iCloud 隐私邮箱` 与 `QQ 邮箱` 的使用方法、`PayPal` 注册绑卡流程、节点检测与纯净度检查,以及 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询` 配置方法。
|
||||
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email`、`iCloud 隐私邮箱` 与 `QQ 邮箱` 的使用方法、`HeroSMS` 手机接码扩展能力、`PayPal` 注册绑卡流程、扩展内置动态 IP 代理、节点检测与纯净度检查,以及 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询` 配置方法。
|
||||
|
||||
## 适用场景
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
- 需要使用 `iCloud+` 的 `隐藏邮件地址` 作为隐私邮箱
|
||||
- 需要临时切换 `QQ 邮箱` 地址继续使用
|
||||
- 需要使用 `网易邮箱`、`网易邮箱大师` 注册多个邮箱或替身邮箱
|
||||
- 需要在 `Step 9` 的手机号验证阶段使用 `HeroSMS` 接码(含多国家回退与价格策略)
|
||||
- 需要注册并使用 `PayPal` 个人账户
|
||||
- 需要在扩展内使用 `711Proxy` 动态 IP,并在自动流程中按轮次切换出口
|
||||
- 需要检查当前节点的出口 IP、纯净度、泄露情况和访问速度
|
||||
- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
|
||||
|
||||
@@ -25,7 +27,9 @@
|
||||
- 一个可正常登录的 `QQ 邮箱`
|
||||
- 手机端已安装 `网易邮箱` 或 `网易邮箱大师`
|
||||
- 一个可正常接收短信的手机号
|
||||
- 一个可用的 `HeroSMS API Key`(用于手机号接码)
|
||||
- 一张可在线支付的借记卡或信用卡
|
||||
- 如需使用扩展内置动态 IP,需准备 `711Proxy` 的 Host、Port、代理账号和代理密码
|
||||
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
|
||||
- 已安装 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev),并已导入可用订阅
|
||||
|
||||
@@ -219,7 +223,71 @@
|
||||
只要网络环境不要太差,并且不要连续高频注册,一般不容易马上触发额外手机号验证。
|
||||
建议注册一个后换节奏再继续,不要一口气连续创建。
|
||||
|
||||
### 第七部分:`PayPal` 注册与绑卡使用教程
|
||||
### 第七部分:`HeroSMS` 手机接码扩展使用教程(新增)
|
||||
|
||||
本部分用于说明扩展中 `Step 9` 手机号验证链路的最新接码能力与推荐用法。
|
||||
目标是减少“重复重发导致封禁”与“同国家持续拿不到码”的失败率,并让失败可以在 `Step 9` 内部自愈。
|
||||
|
||||
#### 一、入口与启用
|
||||
|
||||
1. 打开扩展侧栏,找到 `接码设置`。
|
||||
2. 打开右侧接码开关(`IP 代理`下方同风格开关)。
|
||||
3. 展开设置后,先确认 `接码平台` 显示为 `HeroSMS / OpenAI`。
|
||||
|
||||
#### 二、国家与优先级(新增能力)
|
||||
|
||||
1. 在 `接码国家` 中多选国家(最多 `3` 个),按你的业务顺序排列。
|
||||
2. 在 `国家优先级` 中选择策略:
|
||||
- `国家优先`:严格按你选择的顺序先后尝试。
|
||||
- `价格优先`:在候选国家里先选可用价格更低的国家,再尝试拿号。
|
||||
3. 当同一国家连续失败达到阈值后,流程会自动优先尝试下一个候选国家。
|
||||
|
||||
#### 三、价格控制与价格预览(新增能力)
|
||||
|
||||
1. 填写 `接码 API`(支持右侧眼睛按钮显示/隐藏,避免粘贴错误)。
|
||||
2. 在 `价格` 一行点击 `查询价格`,查看候选国家当前档位。
|
||||
3. 设置 `价格上限`(`maxPrice`)限制成本,避免异常高价拿号。
|
||||
4. 如果日志提示 `no numbers within maxPrice`,说明上限太低,按当前价格结果适度上调即可。
|
||||
|
||||
#### 四、号码复用与参数建议
|
||||
|
||||
1. `号码复用` 开关:
|
||||
- 开启:同号码在可复用范围内优先复用,减少频繁拿号。
|
||||
- 关闭:每次优先新号,适合风控更严格的场景。
|
||||
2. 建议参数(稳妥起步):
|
||||
- `验证码重发`:`0` 或 `1`
|
||||
- `换号上限`:`3`
|
||||
- `验证码限时`:`60` 秒
|
||||
- `超时次数`:`2`
|
||||
- `轮询间隔`:`5` 秒
|
||||
- `轮询次数`:`3-4` 次(不要设置过高)
|
||||
|
||||
#### 五、失败自愈策略(新增能力)
|
||||
|
||||
当前版本已内置以下保护逻辑:
|
||||
|
||||
1. 当页面出现 `Tried to resend too many times. Please try again later.` 时,流程会停止继续狂点 `Resend`,改为换号/换国家路径。
|
||||
2. 单个号码不会无限重发,避免被目标站点判定重发过频。
|
||||
3. 收不到短信时优先在 `Step 9` 内局部恢复,不直接回卷到 `Step 1`。
|
||||
|
||||
#### 六、运行状态怎么看
|
||||
|
||||
在 `运行状态` 中重点看:
|
||||
|
||||
1. `当前分配`:当前拿到的手机号与国家。
|
||||
2. `验证码`:是否已收到可提交验证码。
|
||||
3. `价格预览`:当前国家候选的可用价格信息(查询后刷新)。
|
||||
|
||||
#### 七、常见问题与处理
|
||||
|
||||
1. `no numbers available across ...`
|
||||
表示候选国家当前无号或价格上限过低。先提高价格上限,再调整国家顺序。
|
||||
2. `NO_NUMBERS` 频繁出现
|
||||
增加候选国家数量(最多 3 个),并优先启用 `价格优先`。
|
||||
3. 页面提示重发过多
|
||||
不要手工持续点重发,让流程自动换号/换国家即可。
|
||||
|
||||
### 第八部分:`PayPal` 注册与绑卡使用教程
|
||||
|
||||
1. 打开注册页面
|
||||
打开 [https://www.paypal.com/signin](https://www.paypal.com/signin)。
|
||||
@@ -271,14 +339,14 @@
|
||||
常见情况是上传身份证件。
|
||||
`PayPal` 官方帮助中心说明,通常会在 `2 个工作日` 内审核,但某些情况可能更久。
|
||||
|
||||
### 第八部分:0元试用 ChatGPT Plus 教程
|
||||
### 第九部分:0元试用 ChatGPT Plus 教程
|
||||
|
||||
本部分说明如何在已登录 ChatGPT 的状态下,通过脚本快速生成 Plus 支付链接,然后选择 PayPal 支付完成0元试用订阅。
|
||||
|
||||
#### 准备工作
|
||||
|
||||
1. 已有一个登录状态的 ChatGPT 账户。
|
||||
2. 一个可用的 PayPal 账户(参考第七部分进行注册和绑卡)。
|
||||
2. 一个可用的 PayPal 账户(参考第八部分进行注册和绑卡)。
|
||||
3. 能够接收生成的账单地址的真实地址或虚拟地址。
|
||||
4. Chrome 浏览器(推荐使用地址补全功能)。
|
||||
|
||||
@@ -426,7 +494,113 @@
|
||||
- **PayPal 登录后页面无法继续跳转**
|
||||
稍等片刻让页面加载完毕。如果长时间未响应,检查浏览器是否有弹窗被隐藏,或尝试刷新页面。
|
||||
|
||||
### 第九部分:节点检测与纯净度检查网站
|
||||
### 第十部分:扩展内置动态 IP 代理使用教程
|
||||
|
||||
本部分说明扩展侧边栏里的 `IP代理` 功能。
|
||||
它和 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 这类系统代理不一样:扩展会通过浏览器代理 API 给当前浏览器设置 PAC 代理,并自动回填代理鉴权。当前首版只开放 `711Proxy` 的账号密码模式,`API 拉取` 和多账号列表入口暂未开放。
|
||||
|
||||
#### 一、什么时候需要打开 `IP代理`
|
||||
|
||||
如果你已经在 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 或其他客户端里稳定使用节点,可以继续使用外部代理。
|
||||
如果你希望在扩展里直接控制动态 IP、检查当前出口、按成功轮次自动切换,或者不想依赖系统全局代理,就使用这里的 `IP代理`。
|
||||
|
||||
开启 `IP代理` 后,扩展会接管浏览器代理。
|
||||
关闭 `IP代理` 后,浏览器会恢复默认代理或外部全局代理。
|
||||
|
||||
#### 二、注册或登录 `711Proxy`
|
||||
|
||||
在侧边栏打开 `IP代理` 后,`代理服务` 当前固定为 `711Proxy(首版)`。
|
||||
点击旁边的 `注册` 按钮,按页面提示完成注册或登录。
|
||||
|
||||
在网站中完成注册、登录和套餐准备后,进入代理信息页面,找到可用于账号密码代理的 `Host`、`Port`、`Username` 和 `Password`。
|
||||
|
||||
#### 三、填写固定账号代理
|
||||
|
||||
1. 在扩展侧边栏打开 `IP代理` 开关。
|
||||
2. `代理模式` 保持 `账号密码`。
|
||||
3. 填写 `代理 Host`,例如 `global.rotgb.711proxy.com`。
|
||||
4. 填写 `代理 Port`,例如 `10000`。
|
||||
5. `代理协议` 一般保持 `HTTP`,除非你的代理信息明确要求 `HTTPS`、`SOCKS5` 或 `SOCKS4`。
|
||||
6. 填写 `代理账号` 和 `代理密码`。
|
||||
7. 按需填写 `地区参数`,例如 `US`、`DE`、`HK`。
|
||||
8. 按需填写 `会话(session)` 和 `时长(life)`。
|
||||
|
||||
`地区参数` 只接受两个字母的国家或地区代码。
|
||||
`会话(session)` 会被追加或写回到用户名里的 `session-xxxx`。
|
||||
`时长(life)` 会被追加或写回到用户名里的 `sessTime-xx`,711Proxy 的有效范围按 `5-180` 分钟使用更稳。
|
||||
|
||||
如果你的 `711Proxy` 用户名里已经带了 `region-US`、`session-xxxx` 或 `sessTime-30`,扩展会优先识别用户名里的值。
|
||||
如果你在表单里修改地区、session 或时长,扩展会把它们同步回最终生效的代理用户名。
|
||||
|
||||
#### 四、同步、下一条、Change 和检测出口
|
||||
|
||||
填写或修改代理信息后,不要只保存后就直接开跑。建议按下面顺序操作:
|
||||
|
||||
1. 点击 `同步`
|
||||
扩展会保存当前配置,生成代理规则,应用到浏览器,并自动做一次出口检测。
|
||||
这是修改 Host、Port、账号、密码、地区、session 或时长后最常用的按钮。
|
||||
|
||||
2. 查看 `代理状态`
|
||||
状态卡会显示当前代理、当前出口 IP、出口地区和诊断详情。
|
||||
如果显示 `当前代理` 和 `当前出口`,说明扩展已经能看到代理出口。
|
||||
|
||||
3. 点击 `检测出口`
|
||||
它只重新检测当前出口,不主动更换节点。
|
||||
如果你怀疑检测结果没刷新,或者刚刚切换过外部网络,可以点它复测。
|
||||
|
||||
4. 点击 `检查IP`
|
||||
它会打开 `https://ipinfo.io/what-is-my-ip`,用于人工确认网页看到的公网 IP。
|
||||
|
||||
5. 点击 `下一条`
|
||||
当前首版主要是固定账号模式。如果只有一条可用代理,`下一条` 会重绑当前节点并复测,不保证一定换出口。
|
||||
如果后续开放多账号列表或 API 池,`下一条` 才会真正切到池里的下一条。
|
||||
|
||||
6. 点击 `Change`
|
||||
`Change` 只支持 `711Proxy + 账号密码模式 + 用户名包含 session` 的场景。
|
||||
它会保持当前 session,强制重绑代理链路并刷新出口,适合想换出口但不想改整条账号配置时使用。
|
||||
|
||||
#### 五、任务切换阈值
|
||||
|
||||
`任务切换阈值` 表示自动流程成功多少轮后尝试自动切换一次代理。
|
||||
默认是 `20` 轮,可填写 `1-500`。
|
||||
|
||||
需要注意:
|
||||
|
||||
- 当前只有一条固定账号时,即使命中阈值,也会跳过自动切换。
|
||||
- 如果后续有多条可切换代理,命中阈值后会自动执行 `下一条`。
|
||||
- 这个阈值只影响自动流程成功后的切换节奏,不会替代你手动点击 `同步` 或 `检测出口`。
|
||||
|
||||
#### 六、状态卡怎么看
|
||||
|
||||
常见状态含义如下:
|
||||
|
||||
- `未开启`:扩展没有接管浏览器代理,继续使用默认网络或外部全局代理。
|
||||
- `当前代理:host:port;当前出口:x.x.x.x`:代理已经应用,并检测到出口 IP。
|
||||
- `已启用,但账号模式没有可用代理`:Host/Port 为空或格式不完整,需要先补齐代理信息。
|
||||
- `连通性失败`:代理应用了,但没有检测到可用出口,优先检查 Host、Port、协议、账号和密码。
|
||||
- `地区校验未通过`:检测到的出口地区和你填写的地区参数不一致。711Proxy 场景下扩展会保留接管并给出警告,你需要决定是否继续使用这个出口。
|
||||
- `当前链路未收到 407 代理鉴权挑战`:代理服务没有按浏览器扩展代理 API 预期返回标准鉴权挑战,可能导致账号密码无法自动回填。可以尝试 `Change`、关闭再开启 `IP代理`、换 session,或者更换代理节点。
|
||||
|
||||
如果代理不可用,扩展会启用目标站点阻断保护,避免在代理失效时继续直连访问目标站点。修好代理后重新点击 `同步` 或 `检测出口` 即可恢复。
|
||||
|
||||
#### 七、推荐使用顺序
|
||||
|
||||
首次配置建议按这个顺序:
|
||||
|
||||
1. 点击 `注册` 打开 `711Proxy`,准备代理账号信息。
|
||||
2. 打开扩展侧边栏的 `IP代理`。
|
||||
3. 填写 Host、Port、协议、账号、密码。
|
||||
4. 填写地区参数,例如 `US`。
|
||||
5. 填写 session 和时长,例如 session 为随机前缀、时长为 `30`。
|
||||
6. 点击 `同步`。
|
||||
7. 确认 `代理状态` 里有出口 IP。
|
||||
8. 点击 `检查IP`,人工确认网页检测结果。
|
||||
9. 再继续执行注册、登录或自动流程。
|
||||
|
||||
如果后续要换出口,优先尝试 `Change`。
|
||||
如果 `Change` 不可用,再调整 session 或代理账号后点击 `同步`。
|
||||
|
||||
### 第十一部分:节点检测与纯净度检查网站
|
||||
|
||||
本部分用于检查当前代理节点的出口 IP、归属地、风险标签、泄露情况和网站访问速度。
|
||||
建议每次切换节点后都重新打开这些网站检查一次。
|
||||
@@ -460,7 +634,7 @@
|
||||
接着打开 [IPPure](https://ippure.com/) 检查纯净度、黑名单、代理识别和泄露情况。
|
||||
最后用 [TCPTest 网站测速](https://www.tcptest.cn/?cckey=e62f31db) 测试目标网站的访问速度和连通性。
|
||||
|
||||
### 第十部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
|
||||
### 第十二部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
|
||||
|
||||
#### 第一步:添加扩展脚本
|
||||
|
||||
@@ -569,7 +743,7 @@ function main(config, profileName) {
|
||||
5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。
|
||||

|
||||
|
||||
### 第十一部分:订阅节点与自建推荐
|
||||
### 第十三部分:订阅节点与自建推荐
|
||||
|
||||
如果你还没有订阅节点或想要寻找稳定、便宜的科学上网方式,可以参考以下三种方案获取。
|
||||
|
||||
@@ -661,6 +835,23 @@ function main(config, profileName) {
|
||||
|
||||
请先确认脚本已经完整粘贴并保存,然后回到 `代理` 页面重新查看。若仍未出现,请继续确认当前订阅可用、`代理模式` 为 `规则模式`,并且 `系统代理` 已开启。
|
||||
|
||||
### 扩展 `IP代理` 开启后,为什么状态还是没有出口 IP?
|
||||
|
||||
先确认 Host、Port、协议、代理账号和代理密码都已填写正确。
|
||||
修改这些字段后,需要点击 `同步` 才会重新应用代理并检测出口。
|
||||
如果诊断提示没有收到 `407` 代理鉴权挑战,可以尝试点击 `Change`、关闭再开启 `IP代理`、更换 session,或者换一条代理节点。
|
||||
|
||||
### `Change` 按钮为什么点不了?
|
||||
|
||||
`Change` 只在 `711Proxy` 的账号密码模式下可用,并且当前生效用户名必须包含 `session-xxxx` 这类 session 参数。
|
||||
如果没有 session,请先在 `会话(session)` 中填写一个随机前缀,再点击 `同步`。
|
||||
|
||||
### `IP代理` 和 Clash Verge 应该同时开吗?
|
||||
|
||||
通常二选一即可。
|
||||
如果开启扩展内置 `IP代理`,浏览器会优先使用扩展设置的 PAC 代理;如果关闭它,浏览器会回到默认网络或外部全局代理。
|
||||
排查问题时建议先只保留一种代理方式,确认出口 IP 稳定后再继续流程。
|
||||
|
||||
### 网站测速结果好,是否代表节点纯净?
|
||||
|
||||
不代表。
|
||||
@@ -688,6 +879,9 @@ function main(config, profileName) {
|
||||
- 中国大陆居民注册 `PayPal` 个人账户时,姓名请按身份证上的中文姓名填写,不要使用拼音。
|
||||
- `PayPal` 官方说明,绑定新卡时可能会向发卡行发送最高 `1 USD` 或等值货币的临时授权验证,所以完全没有余额的卡也可能因为授权失败而无法绑定。
|
||||
- 为了控制风险,更稳妥的做法是使用单独管理、余额较低的借记卡,并在绑卡后及时检查 `钱包` 页面和右上角通知。
|
||||
- 使用扩展内置 `IP代理` 时,修改 Host、Port、账号、密码、地区、session 或时长后,请点击 `同步` 让新配置真正生效。
|
||||
- `711Proxy` 的 `Change` 需要用户名中包含 session;没有 session 时请先填写 `会话(session)` 并同步。
|
||||
- 扩展 `IP代理` 报连通性失败时,不要直接继续自动流程,先用 `检测出口` 和 `检查IP` 确认网页看到的出口 IP。
|
||||
- 检查节点时,先确认出口 IP,再看风险标签和泄露检测,最后再看网站测速。
|
||||
- [TCPTest 网站测速](https://www.tcptest.cn/?cckey=e62f31db) 主要用于判断访问速度和连通性,不要单独用它判断节点纯净度。
|
||||
- [IPPure](https://ippure.com/) 如果提示 `WebRTC` 或 `DNS` 泄露,请优先检查浏览器和代理客户端的相关设置。
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
(function attachPayPalUtils(root, factory) {
|
||||
root.PayPalUtils = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createPayPalUtils() {
|
||||
function normalizePayPalAccount(account = {}) {
|
||||
const normalizedEmail = String(account.email || '').trim().toLowerCase();
|
||||
const now = Date.now();
|
||||
return {
|
||||
id: String(account.id || crypto.randomUUID()),
|
||||
email: normalizedEmail,
|
||||
password: String(account.password || ''),
|
||||
createdAt: Number.isFinite(Number(account.createdAt)) ? Number(account.createdAt) : now,
|
||||
updatedAt: Number.isFinite(Number(account.updatedAt)) ? Number(account.updatedAt) : now,
|
||||
lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePayPalAccounts(accounts) {
|
||||
if (!Array.isArray(accounts)) return [];
|
||||
|
||||
const deduped = new Map();
|
||||
for (const account of accounts) {
|
||||
const normalized = normalizePayPalAccount(account);
|
||||
if (!normalized.email) continue;
|
||||
deduped.set(normalized.id, normalized);
|
||||
}
|
||||
return [...deduped.values()];
|
||||
}
|
||||
|
||||
function findPayPalAccount(accounts = [], accountId = '') {
|
||||
const normalizedId = String(accountId || '').trim();
|
||||
if (!normalizedId) return null;
|
||||
return normalizePayPalAccounts(accounts).find((account) => account.id === normalizedId) || null;
|
||||
}
|
||||
|
||||
function upsertPayPalAccountInList(accounts = [], nextAccount = null) {
|
||||
if (!nextAccount) {
|
||||
return normalizePayPalAccounts(accounts);
|
||||
}
|
||||
|
||||
const normalizedNext = normalizePayPalAccount(nextAccount);
|
||||
const list = normalizePayPalAccounts(accounts);
|
||||
const existingIndex = list.findIndex((account) => account.id === normalizedNext.id);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = normalizedNext;
|
||||
return list;
|
||||
}
|
||||
return [...list, normalizedNext];
|
||||
}
|
||||
|
||||
return {
|
||||
findPayPalAccount,
|
||||
normalizePayPalAccount,
|
||||
normalizePayPalAccounts,
|
||||
upsertPayPalAccountInList,
|
||||
};
|
||||
});
|
||||
@@ -123,6 +123,48 @@ def log_info(message):
|
||||
print(f"[HotmailHelper] {message}", flush=True)
|
||||
|
||||
|
||||
def is_openai_sender(address):
|
||||
sender = str(address or "").strip().lower()
|
||||
return "openai" in sender
|
||||
|
||||
|
||||
def get_message_body_content(message):
|
||||
body = message.get("body") or {}
|
||||
if not isinstance(body, dict):
|
||||
return ""
|
||||
return str(body.get("content") or "").strip()
|
||||
|
||||
|
||||
def log_openai_messages(messages, transport=""):
|
||||
for message in messages or []:
|
||||
sender_info = message.get("from", {}).get("emailAddress", {}) or {}
|
||||
sender = str(sender_info.get("address") or "").strip()
|
||||
if not is_openai_sender(sender):
|
||||
continue
|
||||
|
||||
sender_name = str(sender_info.get("name") or "").strip()
|
||||
mailbox = str(message.get("mailbox") or "").strip() or "INBOX"
|
||||
subject = str(message.get("subject") or "").strip()
|
||||
transport_label = str(transport or "").strip() or "unknown"
|
||||
base = (
|
||||
f"transport={transport_label} mailbox={mailbox} sender={sender} "
|
||||
f"senderName={sender_name or '-'} subject={subject}"
|
||||
)
|
||||
|
||||
log_info(f"openai mail received {base}")
|
||||
|
||||
body_content = get_message_body_content(message)
|
||||
if body_content:
|
||||
log_info(f"openai mail full body start {base}")
|
||||
print(body_content, flush=True)
|
||||
log_info("openai mail full body end")
|
||||
continue
|
||||
|
||||
preview = str(message.get("bodyPreview") or "").strip()
|
||||
if preview:
|
||||
log_info(f"openai mail preview {base} preview={compact_text(preview, 1000)}")
|
||||
|
||||
|
||||
def get_proxy_debug_context():
|
||||
names = ["all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY"]
|
||||
parts = []
|
||||
@@ -481,6 +523,9 @@ def normalize_message(message_id, raw_bytes, mailbox):
|
||||
}
|
||||
},
|
||||
"bodyPreview": body[:500],
|
||||
"body": {
|
||||
"content": body,
|
||||
},
|
||||
"receivedDateTime": to_iso_string(timestamp_ms),
|
||||
"receivedTimestamp": timestamp_ms,
|
||||
}
|
||||
@@ -680,6 +725,7 @@ def collect_messages(email_addr, client_id, refresh_token, mailboxes, top):
|
||||
try:
|
||||
log_info(f"message collection start transport={transport_name}")
|
||||
result = collector(email_addr, client_id, refresh_token, mailboxes, top)
|
||||
log_openai_messages(result.get("messages") or [], transport=transport_name)
|
||||
log_info(
|
||||
f"message collection success transport={transport_name} "
|
||||
f"tokenEndpoint={result['token_payload'].get('token_endpoint', '')}"
|
||||
@@ -722,6 +768,10 @@ def select_latest_code(messages, sender_filters, subject_filters, exclude_codes,
|
||||
preview = str(message.get("bodyPreview", ""))
|
||||
combined = " ".join([sender, subject.lower(), preview.lower()])
|
||||
code = extract_code(" ".join([subject, preview, sender]))
|
||||
if not code:
|
||||
body_content = get_message_body_content(message)
|
||||
if body_content:
|
||||
code = extract_code(" ".join([subject, body_content, sender]))
|
||||
if not code or code in excluded:
|
||||
return None
|
||||
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
(function attachSidepanelFormDialog(globalScope) {
|
||||
const FORM_EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
|
||||
const FORM_EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
|
||||
|
||||
function syncPasswordToggleButton(button, input, labels) {
|
||||
if (!button || !input) return;
|
||||
const isHidden = input.type === 'password';
|
||||
button.innerHTML = isHidden ? FORM_EYE_OPEN_ICON : FORM_EYE_CLOSED_ICON;
|
||||
button.setAttribute('aria-label', isHidden ? labels.show : labels.hide);
|
||||
button.title = isHidden ? labels.show : labels.hide;
|
||||
}
|
||||
|
||||
function createFormDialog(context = {}) {
|
||||
const {
|
||||
overlay = null,
|
||||
titleNode = null,
|
||||
closeButton = null,
|
||||
messageNode = null,
|
||||
alertNode = null,
|
||||
fieldsContainer = null,
|
||||
cancelButton = null,
|
||||
confirmButton = null,
|
||||
documentRef = globalScope.document,
|
||||
} = context;
|
||||
|
||||
let resolver = null;
|
||||
let currentConfig = null;
|
||||
let currentInputs = [];
|
||||
|
||||
function setHidden(node, hidden) {
|
||||
if (!node) return;
|
||||
node.hidden = Boolean(hidden);
|
||||
}
|
||||
|
||||
function resetAlert() {
|
||||
if (!alertNode) return;
|
||||
alertNode.textContent = '';
|
||||
alertNode.className = 'modal-alert modal-form-alert';
|
||||
alertNode.hidden = true;
|
||||
}
|
||||
|
||||
function setAlert(message = '', tone = 'danger') {
|
||||
if (!alertNode) return;
|
||||
const text = String(message || '').trim();
|
||||
if (!text) {
|
||||
resetAlert();
|
||||
return;
|
||||
}
|
||||
alertNode.textContent = text;
|
||||
alertNode.className = `modal-alert modal-form-alert${tone === 'danger' ? ' is-danger' : ''}`;
|
||||
alertNode.hidden = false;
|
||||
}
|
||||
|
||||
function close(result = null) {
|
||||
if (resolver) {
|
||||
resolver(result);
|
||||
resolver = null;
|
||||
}
|
||||
currentConfig = null;
|
||||
currentInputs = [];
|
||||
resetAlert();
|
||||
if (fieldsContainer) {
|
||||
fieldsContainer.innerHTML = '';
|
||||
}
|
||||
if (overlay) {
|
||||
overlay.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function buildFieldNode(field, values) {
|
||||
const wrapper = documentRef.createElement('div');
|
||||
wrapper.className = 'modal-form-row';
|
||||
|
||||
const label = documentRef.createElement('label');
|
||||
label.className = 'modal-form-label';
|
||||
const labelText = String(field.label || field.key || '').trim();
|
||||
label.textContent = labelText;
|
||||
wrapper.appendChild(label);
|
||||
|
||||
let input = null;
|
||||
if (field.type === 'textarea') {
|
||||
input = documentRef.createElement('textarea');
|
||||
input.className = 'data-textarea';
|
||||
} else if (field.type === 'select') {
|
||||
input = documentRef.createElement('select');
|
||||
input.className = 'data-select';
|
||||
const options = Array.isArray(field.options) ? field.options : [];
|
||||
options.forEach((option) => {
|
||||
const optionNode = documentRef.createElement('option');
|
||||
optionNode.value = String(option?.value || '');
|
||||
optionNode.textContent = String(option?.label || option?.value || '');
|
||||
input.appendChild(optionNode);
|
||||
});
|
||||
} else {
|
||||
input = documentRef.createElement('input');
|
||||
input.type = field.type === 'password' ? 'password' : 'text';
|
||||
input.className = field.type === 'password'
|
||||
? 'data-input data-input-with-icon'
|
||||
: 'data-input';
|
||||
}
|
||||
|
||||
const normalizedValue = Object.prototype.hasOwnProperty.call(values, field.key)
|
||||
? values[field.key]
|
||||
: field.value;
|
||||
if (normalizedValue !== undefined && normalizedValue !== null) {
|
||||
input.value = String(normalizedValue);
|
||||
}
|
||||
if (field.placeholder) {
|
||||
input.placeholder = String(field.placeholder);
|
||||
}
|
||||
if (field.autocomplete) {
|
||||
input.autocomplete = String(field.autocomplete);
|
||||
}
|
||||
if (field.inputMode) {
|
||||
input.inputMode = String(field.inputMode);
|
||||
}
|
||||
if (field.rows && field.type === 'textarea') {
|
||||
input.rows = Number(field.rows) || 3;
|
||||
}
|
||||
input.dataset.fieldKey = String(field.key || '');
|
||||
label.htmlFor = field.key;
|
||||
input.id = field.key;
|
||||
if (field.type === 'password') {
|
||||
const inputShell = documentRef.createElement('div');
|
||||
inputShell.className = 'input-with-icon';
|
||||
inputShell.appendChild(input);
|
||||
|
||||
const toggleButton = documentRef.createElement('button');
|
||||
toggleButton.className = 'input-icon-btn';
|
||||
toggleButton.type = 'button';
|
||||
const labels = {
|
||||
show: String(field.showPasswordLabel || `\u663e\u793a${labelText || '\u5bc6\u7801'}`),
|
||||
hide: String(field.hidePasswordLabel || `\u9690\u85cf${labelText || '\u5bc6\u7801'}`),
|
||||
};
|
||||
syncPasswordToggleButton(toggleButton, input, labels);
|
||||
toggleButton.addEventListener('click', () => {
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
syncPasswordToggleButton(toggleButton, input, labels);
|
||||
});
|
||||
|
||||
inputShell.appendChild(toggleButton);
|
||||
wrapper.appendChild(inputShell);
|
||||
} else {
|
||||
wrapper.appendChild(input);
|
||||
}
|
||||
|
||||
if (field.type !== 'textarea') {
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
void handleConfirm();
|
||||
});
|
||||
}
|
||||
|
||||
currentInputs.push({ field, input });
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function collectValues() {
|
||||
return currentInputs.reduce((result, item) => {
|
||||
result[item.field.key] = item.input.value;
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!currentConfig) {
|
||||
close(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = collectValues();
|
||||
resetAlert();
|
||||
|
||||
for (const item of currentInputs) {
|
||||
const { field, input } = item;
|
||||
const rawValue = values[field.key];
|
||||
const textValue = String(rawValue || '').trim();
|
||||
if (field.required && !textValue) {
|
||||
setAlert(field.requiredMessage || `${field.label || field.key}不能为空。`);
|
||||
input.focus?.();
|
||||
return;
|
||||
}
|
||||
if (typeof field.validate === 'function') {
|
||||
const validationMessage = await field.validate(rawValue, values);
|
||||
if (validationMessage) {
|
||||
setAlert(validationMessage);
|
||||
input.focus?.();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(values);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
overlay?.addEventListener('click', (event) => {
|
||||
if (event.target === overlay) {
|
||||
close(null);
|
||||
}
|
||||
});
|
||||
closeButton?.addEventListener('click', () => close(null));
|
||||
cancelButton?.addEventListener('click', () => close(null));
|
||||
confirmButton?.addEventListener('click', () => {
|
||||
void handleConfirm();
|
||||
});
|
||||
}
|
||||
|
||||
async function open(config = {}) {
|
||||
if (!overlay || !titleNode || !fieldsContainer || !confirmButton) {
|
||||
return null;
|
||||
}
|
||||
if (resolver) {
|
||||
close(null);
|
||||
}
|
||||
|
||||
currentConfig = config || {};
|
||||
currentInputs = [];
|
||||
titleNode.textContent = String(currentConfig.title || '填写表单');
|
||||
if (messageNode) {
|
||||
const message = String(currentConfig.message || '').trim();
|
||||
messageNode.textContent = message;
|
||||
setHidden(messageNode, !message);
|
||||
}
|
||||
resetAlert();
|
||||
if (currentConfig.alert?.text) {
|
||||
setAlert(currentConfig.alert.text, currentConfig.alert.tone || 'danger');
|
||||
}
|
||||
|
||||
confirmButton.textContent = String(currentConfig.confirmLabel || '确认');
|
||||
confirmButton.className = `btn ${currentConfig.confirmVariant || 'btn-primary'} btn-sm`;
|
||||
fieldsContainer.innerHTML = '';
|
||||
|
||||
const initialValues = currentConfig.initialValues && typeof currentConfig.initialValues === 'object'
|
||||
? currentConfig.initialValues
|
||||
: {};
|
||||
const fields = Array.isArray(currentConfig.fields) ? currentConfig.fields : [];
|
||||
fields.forEach((field) => {
|
||||
fieldsContainer.appendChild(buildFieldNode(field, initialValues));
|
||||
});
|
||||
|
||||
overlay.hidden = false;
|
||||
const firstInput = currentInputs[0]?.input || null;
|
||||
if (firstInput && typeof globalScope.requestAnimationFrame === 'function') {
|
||||
globalScope.requestAnimationFrame(() => firstInput.focus?.());
|
||||
} else {
|
||||
firstInput?.focus?.();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
|
||||
return {
|
||||
close,
|
||||
open,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelFormDialog = {
|
||||
createFormDialog,
|
||||
};
|
||||
})(window);
|
||||
+82
-44
@@ -14,6 +14,47 @@ const ipProxyActionState = {
|
||||
busy: false,
|
||||
action: '',
|
||||
};
|
||||
const IP_PROXY_SECTION_EXPANDED_STORAGE_KEY = 'multipage-ip-proxy-section-expanded';
|
||||
let ipProxySectionExpanded = false;
|
||||
|
||||
function readIpProxySectionExpanded() {
|
||||
try {
|
||||
return globalThis.localStorage?.getItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY) === '1';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function persistIpProxySectionExpanded(expanded) {
|
||||
try {
|
||||
if (expanded) {
|
||||
globalThis.localStorage?.setItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY, '1');
|
||||
} else {
|
||||
globalThis.localStorage?.removeItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore storage errors; the in-memory collapsed state is still enough for this session.
|
||||
}
|
||||
}
|
||||
|
||||
function setIpProxySectionExpanded(expanded) {
|
||||
ipProxySectionExpanded = Boolean(expanded);
|
||||
persistIpProxySectionExpanded(ipProxySectionExpanded);
|
||||
if (typeof updateIpProxyUI === 'function') {
|
||||
updateIpProxyUI(latestState);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleIpProxySectionExpanded() {
|
||||
setIpProxySectionExpanded(!ipProxySectionExpanded);
|
||||
}
|
||||
|
||||
function initIpProxySectionExpandedState() {
|
||||
ipProxySectionExpanded = readIpProxySectionExpanded();
|
||||
if (typeof updateIpProxyUI === 'function') {
|
||||
updateIpProxyUI(latestState);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeIpProxyActionType(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
@@ -866,14 +907,18 @@ function buildIpProxyActionHintText(options = {}) {
|
||||
const mode = normalizeIpProxyModeForCurrentRelease(options?.mode || DEFAULT_IP_PROXY_MODE);
|
||||
const poolCount = Math.max(0, Number(options?.poolCount) || 0);
|
||||
const changeAvailable = Boolean(options?.changeAvailable);
|
||||
const dynamicPoolCount = poolCount > 0 ? poolCount : 1;
|
||||
|
||||
if (mode === 'api') {
|
||||
return '下一条:切到已拉取代理池的下一条。Change:仅账号模式可用。';
|
||||
const nextPart = poolCount > 1
|
||||
? `下一条:当前共 ${dynamicPoolCount} 条节点,切到已拉取代理池的下一条节点。`
|
||||
: `下一条:当前仅 ${dynamicPoolCount} 条节点,执行重绑复测(不保证更换出口)。`;
|
||||
return `${nextPart} Change:仅账号模式可用。`;
|
||||
}
|
||||
|
||||
const nextPart = poolCount > 1
|
||||
? '下一条:切到代理池的下一条节点。'
|
||||
: '下一条:当前仅 1 条节点,执行重绑复测(不保证更换出口)。';
|
||||
? `下一条:当前共 ${dynamicPoolCount} 条节点,切到代理池的下一条节点。`
|
||||
: `下一条:当前仅 ${dynamicPoolCount} 条节点,执行重绑复测(不保证更换出口)。`;
|
||||
const changePart = changeAvailable
|
||||
? 'Change:保持当前 session 重绑链路并复测出口。'
|
||||
: 'Change:需 711 账号模式且用户名包含 session。';
|
||||
@@ -890,7 +935,6 @@ function setIpProxyCurrentDisplay(text = '', hasValue = false) {
|
||||
function formatIpProxyCurrentDisplay(state = latestState) {
|
||||
const mode = normalizeIpProxyModeForCurrentRelease(state?.ipProxyMode);
|
||||
if (mode === 'account') {
|
||||
const runtime = getIpProxyRuntimeSnapshot(state, mode);
|
||||
const current = getIpProxyCurrentEntry(state);
|
||||
if (!current) {
|
||||
return {
|
||||
@@ -898,10 +942,8 @@ function formatIpProxyCurrentDisplay(state = latestState) {
|
||||
hasValue: false,
|
||||
};
|
||||
}
|
||||
const count = runtime.pool.length > 0 ? runtime.pool.length : 1;
|
||||
const index = runtime.index;
|
||||
return {
|
||||
text: `${current.host}:${current.port}${current.region ? ` [${current.region}]` : ''} (${Math.min(index + 1, count)}/${count})`,
|
||||
text: `${current.host}:${current.port}${current.region ? ` [${current.region}]` : ''}`,
|
||||
hasValue: true,
|
||||
};
|
||||
}
|
||||
@@ -919,7 +961,7 @@ function formatIpProxyCurrentDisplay(state = latestState) {
|
||||
const region = String(current.region || '').trim();
|
||||
const label = region ? `${current.host}:${current.port} [${region}]` : `${current.host}:${current.port}`;
|
||||
return {
|
||||
text: `${label}${count ? ` (${Math.min(index + 1, count)}/${count})` : ''}`,
|
||||
text: label,
|
||||
hasValue: true,
|
||||
};
|
||||
}
|
||||
@@ -945,19 +987,7 @@ function buildIpProxyCurrentDisplayText(display = {}, runtimeStatus = {}) {
|
||||
if (!hasValue || !rawText) {
|
||||
return rawText;
|
||||
}
|
||||
const runtimeText = String(runtimeStatus?.text || '').trim().toLowerCase();
|
||||
if (!runtimeText) {
|
||||
return rawText;
|
||||
}
|
||||
const endpointToken = extractIpProxyEndpointToken(rawText);
|
||||
if (!endpointToken || !runtimeText.includes(endpointToken)) {
|
||||
return rawText;
|
||||
}
|
||||
const indexToken = extractIpProxyIndexToken(rawText);
|
||||
if (indexToken) {
|
||||
return `节点 ${indexToken}`;
|
||||
}
|
||||
return '当前节点';
|
||||
return rawText;
|
||||
}
|
||||
|
||||
function formatIpProxyRuntimeStatus(state = latestState) {
|
||||
@@ -1158,6 +1188,7 @@ function setIpProxyEnabledInlineStatus(state = {}, enabled = getSelectedIpProxyE
|
||||
|
||||
function updateIpProxyUI(state = latestState) {
|
||||
const enabled = getSelectedIpProxyEnabled();
|
||||
const showSettings = enabled && ipProxySectionExpanded;
|
||||
const mode = getSelectedIpProxyMode();
|
||||
const service = normalizeIpProxyService(selectIpProxyService?.value || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE);
|
||||
const apiModeAvailable = isIpProxyApiModeAvailable();
|
||||
@@ -1172,64 +1203,70 @@ function updateIpProxyUI(state = latestState) {
|
||||
const busyAction = normalizeIpProxyActionType(actionState.action);
|
||||
const runtimeState = state || latestState || {};
|
||||
|
||||
setIpProxyEnabledInlineStatus(runtimeState, enabled);
|
||||
|
||||
if (rowIpProxyEnabled) {
|
||||
rowIpProxyEnabled.style.display = '';
|
||||
}
|
||||
if (btnToggleIpProxySection) {
|
||||
btnToggleIpProxySection.disabled = !enabled;
|
||||
btnToggleIpProxySection.textContent = showSettings ? '收起设置' : '展开设置';
|
||||
btnToggleIpProxySection.title = enabled
|
||||
? (showSettings ? '收起 IP 代理设置' : '展开 IP 代理设置')
|
||||
: '开启 IP 代理后可展开设置';
|
||||
btnToggleIpProxySection.setAttribute('aria-expanded', String(showSettings));
|
||||
}
|
||||
if (rowIpProxyFold) {
|
||||
rowIpProxyFold.style.display = enabled ? '' : 'none';
|
||||
rowIpProxyFold.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyService) {
|
||||
rowIpProxyService.style.display = enabled ? '' : 'none';
|
||||
rowIpProxyService.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyMode) {
|
||||
rowIpProxyMode.style.display = enabled ? '' : 'none';
|
||||
rowIpProxyMode.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyLayout) {
|
||||
rowIpProxyLayout.style.display = enabled ? '' : 'none';
|
||||
rowIpProxyLayout.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyApiUrl) {
|
||||
rowIpProxyApiUrl.style.display = enabled && apiModeAvailable && isApiMode ? '' : 'none';
|
||||
rowIpProxyApiUrl.style.display = showSettings && apiModeAvailable && isApiMode ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyAccountList) {
|
||||
rowIpProxyAccountList.style.display = enabled && isAccountMode && accountListAvailable ? '' : 'none';
|
||||
rowIpProxyAccountList.style.display = showSettings && isAccountMode && accountListAvailable ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyAccountSessionPrefix) {
|
||||
rowIpProxyAccountSessionPrefix.style.display = enabled && showSessionOptions ? '' : 'none';
|
||||
rowIpProxyAccountSessionPrefix.style.display = showSettings && showSessionOptions ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyAccountLifeMinutes) {
|
||||
rowIpProxyAccountLifeMinutes.style.display = enabled && showSessionOptions ? '' : 'none';
|
||||
rowIpProxyAccountLifeMinutes.style.display = showSettings && showSessionOptions ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyPoolTargetCount) {
|
||||
rowIpProxyPoolTargetCount.style.display = enabled ? '' : 'none';
|
||||
rowIpProxyPoolTargetCount.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyHost) {
|
||||
rowIpProxyHost.style.display = enabled && isAccountMode ? '' : 'none';
|
||||
rowIpProxyHost.style.display = showSettings && isAccountMode ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyPort) {
|
||||
rowIpProxyPort.style.display = enabled && isAccountMode ? '' : 'none';
|
||||
rowIpProxyPort.style.display = showSettings && isAccountMode ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyProtocol) {
|
||||
rowIpProxyProtocol.style.display = enabled ? '' : 'none';
|
||||
rowIpProxyProtocol.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyUsername) {
|
||||
rowIpProxyUsername.style.display = enabled && isAccountMode ? '' : 'none';
|
||||
rowIpProxyUsername.style.display = showSettings && isAccountMode ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyPassword) {
|
||||
rowIpProxyPassword.style.display = enabled && isAccountMode ? '' : 'none';
|
||||
rowIpProxyPassword.style.display = showSettings && isAccountMode ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyRegion) {
|
||||
rowIpProxyRegion.style.display = enabled && isAccountMode ? '' : 'none';
|
||||
rowIpProxyRegion.style.display = showSettings && isAccountMode ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyActions) {
|
||||
rowIpProxyActions.style.display = enabled ? '' : 'none';
|
||||
rowIpProxyActions.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (ipProxyActionButtons) {
|
||||
ipProxyActionButtons.style.display = enabled ? '' : 'none';
|
||||
ipProxyActionButtons.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyRuntimeStatus) {
|
||||
rowIpProxyRuntimeStatus.style.display = enabled ? '' : 'none';
|
||||
rowIpProxyRuntimeStatus.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (ipProxyLayout) {
|
||||
ipProxyLayout.classList.toggle('is-account-only', !apiModeAvailable);
|
||||
@@ -1256,7 +1293,7 @@ function updateIpProxyUI(state = latestState) {
|
||||
ipProxyApiPanel.classList.toggle('is-disabled', !apiModeAvailable);
|
||||
ipProxyApiPanel.setAttribute('aria-disabled', String(!apiModeAvailable));
|
||||
ipProxyApiPanel.hidden = !apiModeAvailable;
|
||||
ipProxyApiPanel.style.display = enabled && apiModeAvailable ? '' : 'none';
|
||||
ipProxyApiPanel.style.display = showSettings && apiModeAvailable ? '' : 'none';
|
||||
}
|
||||
if (inputIpProxyApiUrl) {
|
||||
inputIpProxyApiUrl.disabled = !enabled || !apiModeAvailable;
|
||||
@@ -1305,11 +1342,12 @@ function updateIpProxyUI(state = latestState) {
|
||||
setIpProxyCurrentDisplay(currentDisplayText, currentDisplay.hasValue);
|
||||
const runtimeSnapshot = getIpProxyRuntimeSnapshot(runtimeState, mode, service);
|
||||
const runtimePoolCount = Array.isArray(runtimeSnapshot?.pool) ? runtimeSnapshot.pool.length : 0;
|
||||
const runtimePoolCountForDisplay = runtimePoolCount > 0 ? runtimePoolCount : 1;
|
||||
const hasCurrentEntry = Boolean(getIpProxyCurrentEntry(runtimeState));
|
||||
const changeAvailable = canChangeIpProxyExitWithCurrentSession(runtimeState);
|
||||
const nextActionTitle = runtimePoolCount > 1
|
||||
? '切换到代理池下一条节点并应用'
|
||||
: '当前仅 1 条节点:重绑当前节点并复测连通性(不保证更换出口)';
|
||||
? `切换到代理池下一条节点并应用(当前共 ${runtimePoolCountForDisplay} 条)`
|
||||
: `当前仅 ${runtimePoolCountForDisplay} 条节点:重绑当前节点并复测连通性(不保证更换出口)`;
|
||||
|
||||
if (btnIpProxyRefresh) {
|
||||
btnIpProxyRefresh.disabled = actionBusy || !enabled || !canOperate;
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
(function attachSidepanelPayPalManager(globalScope) {
|
||||
function createPayPalManager(context = {}) {
|
||||
const {
|
||||
state,
|
||||
dom,
|
||||
helpers,
|
||||
runtime,
|
||||
paypalUtils = {},
|
||||
} = context;
|
||||
|
||||
let actionInFlight = false;
|
||||
|
||||
function getPayPalAccounts(currentState = state.getLatestState()) {
|
||||
return helpers.getPayPalAccounts(currentState);
|
||||
}
|
||||
|
||||
function getCurrentPayPalAccountId(currentState = state.getLatestState()) {
|
||||
return String(currentState?.currentPayPalAccountId || '').trim();
|
||||
}
|
||||
|
||||
function buildSelectOptions(accounts = []) {
|
||||
if (!accounts.length) {
|
||||
return '<option value="">请先添加 PayPal 账号</option>';
|
||||
}
|
||||
return accounts.map((account) => (
|
||||
`<option value="${helpers.escapeHtml(account.id)}">${helpers.escapeHtml(account.email || '(未命名账号)')}</option>`
|
||||
)).join('');
|
||||
}
|
||||
|
||||
function applyPayPalAccountMutation(account) {
|
||||
if (!account?.id) return;
|
||||
const latestState = state.getLatestState();
|
||||
const nextAccounts = typeof paypalUtils.upsertPayPalAccountInList === 'function'
|
||||
? paypalUtils.upsertPayPalAccountInList(getPayPalAccounts(latestState), account)
|
||||
: [...getPayPalAccounts(latestState), account];
|
||||
state.syncLatestState({ paypalAccounts: nextAccounts });
|
||||
renderPayPalAccounts();
|
||||
}
|
||||
|
||||
function renderPayPalAccounts() {
|
||||
if (!dom.selectPayPalAccount) return;
|
||||
|
||||
const latestState = state.getLatestState();
|
||||
const accounts = getPayPalAccounts(latestState);
|
||||
const currentId = getCurrentPayPalAccountId(latestState);
|
||||
|
||||
dom.selectPayPalAccount.innerHTML = buildSelectOptions(accounts);
|
||||
dom.selectPayPalAccount.disabled = accounts.length === 0;
|
||||
dom.selectPayPalAccount.value = accounts.some((account) => account.id === currentId) ? currentId : '';
|
||||
}
|
||||
|
||||
async function syncSelectedPayPalAccount(options = {}) {
|
||||
const { silent = false } = options;
|
||||
const accountId = String(dom.selectPayPalAccount?.value || '').trim();
|
||||
if (!accountId) {
|
||||
state.syncLatestState({
|
||||
currentPayPalAccountId: null,
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
});
|
||||
renderPayPalAccounts();
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SELECT_PAYPAL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: { accountId },
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
state.syncLatestState({
|
||||
currentPayPalAccountId: response.account?.id || accountId,
|
||||
paypalEmail: String(response.account?.email || '').trim(),
|
||||
paypalPassword: String(response.account?.password || ''),
|
||||
});
|
||||
renderPayPalAccounts();
|
||||
if (!silent) {
|
||||
helpers.showToast(`已切换当前 PayPal 账号为 ${response.account?.email || accountId}`, 'success', 1800);
|
||||
}
|
||||
return response.account || null;
|
||||
}
|
||||
|
||||
async function openPayPalAccountDialog() {
|
||||
if (typeof helpers.openFormDialog !== 'function') {
|
||||
throw new Error('表单弹窗能力未加载,请刷新扩展后重试。');
|
||||
}
|
||||
return helpers.openFormDialog({
|
||||
title: '添加 PayPal 账号',
|
||||
confirmLabel: '保存账号',
|
||||
confirmVariant: 'btn-primary',
|
||||
fields: [
|
||||
{
|
||||
key: 'email',
|
||||
label: 'PayPal 账号',
|
||||
type: 'text',
|
||||
placeholder: '请输入 PayPal 登录邮箱',
|
||||
autocomplete: 'username',
|
||||
required: true,
|
||||
requiredMessage: '请先填写 PayPal 账号。',
|
||||
validate: (value) => {
|
||||
const normalized = String(value || '').trim();
|
||||
if (!normalized.includes('@')) {
|
||||
return 'PayPal 账号需填写邮箱格式。';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'PayPal 密码',
|
||||
type: 'password',
|
||||
placeholder: '请输入 PayPal 登录密码',
|
||||
autocomplete: 'current-password',
|
||||
required: true,
|
||||
requiredMessage: '请先填写 PayPal 密码。',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAddPayPalAccount() {
|
||||
if (actionInFlight) return;
|
||||
|
||||
const formValues = await openPayPalAccountDialog();
|
||||
if (!formValues) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionInFlight = true;
|
||||
if (dom.btnAddPayPalAccount) {
|
||||
dom.btnAddPayPalAccount.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'UPSERT_PAYPAL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
email: String(formValues.email || '').trim(),
|
||||
password: String(formValues.password || ''),
|
||||
},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
applyPayPalAccountMutation(response.account);
|
||||
if (response.account?.id) {
|
||||
state.syncLatestState({ currentPayPalAccountId: response.account.id });
|
||||
renderPayPalAccounts();
|
||||
dom.selectPayPalAccount.value = response.account.id;
|
||||
await syncSelectedPayPalAccount({ silent: true });
|
||||
}
|
||||
helpers.showToast(`已保存 PayPal 账号 ${response.account?.email || ''}`, 'success', 2200);
|
||||
} catch (error) {
|
||||
helpers.showToast(`保存 PayPal 账号失败:${error.message}`, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
if (dom.btnAddPayPalAccount) {
|
||||
dom.btnAddPayPalAccount.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bindPayPalEvents() {
|
||||
dom.btnAddPayPalAccount?.addEventListener('click', () => {
|
||||
void handleAddPayPalAccount();
|
||||
});
|
||||
dom.selectPayPalAccount?.addEventListener('change', () => {
|
||||
void syncSelectedPayPalAccount().catch((error) => {
|
||||
helpers.showToast(error.message, 'error');
|
||||
renderPayPalAccounts();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bindPayPalEvents,
|
||||
renderPayPalAccounts,
|
||||
syncSelectedPayPalAccount,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelPayPalManager = {
|
||||
createPayPalManager,
|
||||
};
|
||||
})(window);
|
||||
+425
-46
@@ -607,7 +607,12 @@ header {
|
||||
Data Card
|
||||
============================================================ */
|
||||
|
||||
#data-section { margin-bottom: 14px; }
|
||||
#data-section {
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
background: var(--bg-surface);
|
||||
@@ -769,6 +774,21 @@ header {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#settings-card .data-row.module-divider-start {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
#settings-card .data-row.module-divider-start::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border) 76%, transparent);
|
||||
}
|
||||
|
||||
.data-check-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
@@ -785,6 +805,19 @@ header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ip-proxy-card {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ip-proxy-header-actions {
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#btn-toggle-ip-proxy-section {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ip-proxy-fold {
|
||||
width: 100%;
|
||||
border: none;
|
||||
@@ -801,6 +834,39 @@ header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.phone-verification-card {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.phone-verification-header-actions {
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#btn-toggle-phone-verification-section {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.phone-verification-fold-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.phone-verification-fold {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.phone-verification-fold-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-layout-row {
|
||||
display: block;
|
||||
}
|
||||
@@ -858,17 +924,20 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.ip-proxy-enabled-inline {
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ip-proxy-actions-inline {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
row-gap: 6px;
|
||||
}
|
||||
|
||||
#row-ip-proxy-actions {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#row-ip-proxy-actions > .data-label {
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.ip-proxy-action-grid {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -888,42 +957,6 @@ header {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ip-proxy-enabled-status {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
min-width: 92px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ip-proxy-enabled-status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--text-muted);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text-muted) 25%, transparent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-enabled-status.is-on {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.ip-proxy-enabled-status.is-on .ip-proxy-enabled-status-dot {
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--green) 30%, transparent);
|
||||
}
|
||||
|
||||
.ip-proxy-enabled-status.is-off {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-status {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
@@ -946,19 +979,25 @@ header {
|
||||
|
||||
.ip-proxy-runtime-main {
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ip-proxy-check-ip-btn {
|
||||
min-width: 64px;
|
||||
padding-inline: 10px;
|
||||
min-width: 0;
|
||||
padding-inline: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-current {
|
||||
@@ -972,6 +1011,14 @@ header {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#row-ip-proxy-runtime-status {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#row-ip-proxy-runtime-status > .data-label {
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -1000,20 +1047,53 @@ header {
|
||||
.ip-proxy-runtime-details {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
padding-right: 84px;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details-row {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-height: 24px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details summary::after {
|
||||
content: '▾';
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: inherit;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: center;
|
||||
transition: transform var(--transition);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details[open] summary {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details[open] summary::after {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details-text {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
@@ -1869,6 +1949,271 @@ header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-sms-country-stack {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hero-sms-country-mainline {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-sms-country-note {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.hero-sms-reuse-max-inline {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.hero-sms-reuse-max-left {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-sms-reuse-max-right {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.hero-sms-max-price-input {
|
||||
width: 72px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-sms-country-menu {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-btn {
|
||||
width: 100%;
|
||||
height: 33px;
|
||||
min-height: 33px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-btn[aria-expanded="true"] {
|
||||
border-color: var(--blue);
|
||||
color: var(--blue);
|
||||
background: var(--blue-soft);
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-base);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-search {
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-dropdown[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-item-label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hero-sms-country-menu-item-badge {
|
||||
flex: 0 0 auto;
|
||||
min-width: 42px;
|
||||
text-align: right;
|
||||
color: var(--brand);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-sms-runtime-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 4px 12px;
|
||||
}
|
||||
|
||||
.hero-sms-runtime-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-sms-runtime-cell-span2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.hero-sms-runtime-key {
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hero-sms-runtime-value {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-sms-price-preview-stack {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hero-sms-price-preview-head {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#btn-hero-sms-price-preview {
|
||||
height: 33px;
|
||||
min-height: 33px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.hero-sms-price-controls-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px 12px;
|
||||
}
|
||||
|
||||
.hero-sms-price-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-sms-price-control .setting-controls {
|
||||
margin-left: auto;
|
||||
width: 104px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.hero-sms-price-control-reuse {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#row-hero-sms-max-price,
|
||||
#row-phone-code-settings-group {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#row-hero-sms-max-price > .data-label,
|
||||
#row-phone-code-settings-group > .data-label {
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.hero-sms-toggle-controls {
|
||||
width: 104px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.hero-sms-price-preview-result {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface);
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.hero-sms-price-preview-text {
|
||||
display: block;
|
||||
white-space: pre-line;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.hero-sms-settings-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px 12px;
|
||||
}
|
||||
|
||||
.hero-sms-settings-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-sms-settings-cell .setting-controls {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.hero-sms-settings-caption {
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-unit {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
@@ -2722,6 +3067,40 @@ header {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-form-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.modal-form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.modal-form-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.modal-form-row .data-input,
|
||||
.modal-form-row .data-select,
|
||||
.modal-form-row .data-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-form-row .input-with-icon {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-form-alert {
|
||||
margin-top: -4px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
+455
-261
@@ -184,7 +184,13 @@
|
||||
</div>
|
||||
<div class="data-row" id="row-sub2api-password" style="display:none;">
|
||||
<span class="data-label">密码</span>
|
||||
<input type="password" id="input-sub2api-password" class="data-input" placeholder="请输入 SUB2API 登录密码" />
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-sub2api-password" class="data-input data-input-with-icon"
|
||||
placeholder="请输入 SUB2API 登录密码" />
|
||||
<button id="btn-toggle-sub2api-password" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-sub2api-password" data-show-label="显示 SUB2API 密码"
|
||||
data-hide-label="隐藏 SUB2API 密码" aria-label="显示 SUB2API 密码" title="显示 SUB2API 密码"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-sub2api-group" style="display:none;">
|
||||
<span class="data-label">分组</span>
|
||||
@@ -194,19 +200,387 @@
|
||||
<span class="data-label">默认代理</span>
|
||||
<input type="text" id="input-sub2api-default-proxy" class="data-input" placeholder="留空则不使用代理;或填写代理名称 / ID" />
|
||||
</div>
|
||||
<div class="data-row" id="row-ip-proxy-enabled">
|
||||
<span class="data-label">IP代理</span>
|
||||
<div class="data-inline ip-proxy-enabled-inline">
|
||||
<div class="data-row" id="row-codex2api-url" style="display:none;">
|
||||
<span class="data-label">Codex2API</span>
|
||||
<input type="text" id="input-codex2api-url" class="data-input"
|
||||
placeholder="http://localhost:8080/admin/accounts" />
|
||||
</div>
|
||||
<div class="data-row" id="row-codex2api-admin-key" style="display:none;">
|
||||
<span class="data-label">管理密钥</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-codex2api-admin-key" class="data-input data-input-with-icon"
|
||||
placeholder="请输入 Codex2API Admin Secret" />
|
||||
<button id="btn-toggle-codex2api-admin-key" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-codex2api-admin-key" data-show-label="显示 Codex2API 管理密钥"
|
||||
data-hide-label="隐藏 Codex2API 管理密钥" aria-label="显示 Codex2API 管理密钥"
|
||||
title="显示 Codex2API 管理密钥"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-custom-password">
|
||||
<span class="data-label">账户密码</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-password" class="data-input data-input-with-icon"
|
||||
placeholder="codex密码,留空则自动生成" />
|
||||
<button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码" title="显示密码"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-plus-mode">
|
||||
<span class="data-label">Plus 模式</span>
|
||||
<div class="data-inline setting-pair">
|
||||
<div class="setting-group setting-group-primary">
|
||||
<label class="toggle-switch" for="input-plus-mode-enabled" title="开启后使用 Plus Checkout + PayPal 授权流程">
|
||||
<input type="checkbox" id="input-plus-mode-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="setting-caption">PayPal 订阅链路</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-paypal-account" style="display:none;">
|
||||
<span class="data-label">PayPal 账号</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-paypal-account" class="data-select">
|
||||
<option value="">请先添加 PayPal 账号</option>
|
||||
</select>
|
||||
<button id="btn-add-paypal-account" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-mail-provider">
|
||||
<span class="data-label">邮箱服务</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-mail-provider" class="data-select">
|
||||
<option value="custom">自定义邮箱</option>
|
||||
<option value="hotmail-api">Hotmail(账号池)</option>
|
||||
<option value="luckmail-api">LuckMail(API 购邮)</option>
|
||||
<option value="icloud">iCloud 邮箱</option>
|
||||
<option value="163">163 邮箱 (mail.163.com)</option>
|
||||
<option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
|
||||
<option value="126">126 邮箱 (mail.126.com)</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
|
||||
<option value="inbucket">Inbucket(自定义主机)</option>
|
||||
<option value="2925">2925 邮箱 (2925.com)</option>
|
||||
<option value="gmail">Gmail 邮箱 (mail.google.com)</option>
|
||||
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
|
||||
</select>
|
||||
<button id="btn-mail-login" class="btn btn-outline btn-sm data-inline-btn" type="button" disabled>登录</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row data-check-row" id="row-custom-mail-provider-pool" style="display:none;">
|
||||
<span class="data-label">自定义号池</span>
|
||||
<textarea id="input-custom-mail-provider-pool" class="data-textarea"
|
||||
placeholder="每行一个注册邮箱,例如 alias001@example.com alias002@example.com"></textarea>
|
||||
</div>
|
||||
<div class="data-row" id="row-mail-2925-mode" style="display:none;">
|
||||
<span class="data-label">2925 模式</span>
|
||||
<div id="mail-2925-mode-group" class="choice-group" role="group" aria-label="2925 邮箱模式">
|
||||
<button type="button" class="choice-btn" data-mail2925-mode="provide">提供邮箱</button>
|
||||
<button type="button" class="choice-btn" data-mail2925-mode="receive">接收邮箱</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-email-generator">
|
||||
<span class="data-label">邮箱生成</span>
|
||||
<select id="select-email-generator" class="data-select">
|
||||
<option value="gmail-alias">Gmail +tag</option>
|
||||
<option value="duck">DuckDuckGo</option>
|
||||
<option value="custom-pool">自定义邮箱池</option>
|
||||
<option value="cloudflare">Cloudflare</option>
|
||||
<option value="icloud">iCloud 隐私邮箱</option>
|
||||
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row data-check-row" id="row-custom-email-pool" style="display:none;">
|
||||
<span class="data-label">邮箱池</span>
|
||||
<textarea id="input-custom-email-pool" class="data-textarea"
|
||||
placeholder="每行一个邮箱,例如 alias001@gmail.com alias002@gmail.com"></textarea>
|
||||
</div>
|
||||
<div class="data-row" id="row-cf-domain" style="display:none;">
|
||||
<span class="data-label">CF 域名</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-cf-domain" class="data-select"></select>
|
||||
<input type="text" id="input-cf-domain" class="data-input" placeholder="例如 yourdomain.xyz"
|
||||
style="display:none;" />
|
||||
<button id="btn-cf-domain-mode" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-mail2925-pool-settings" style="display:none;">
|
||||
<span class="data-label">2925 号池</span>
|
||||
<div class="data-inline mail2925-base-inline">
|
||||
<label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool"
|
||||
for="input-mail2925-use-account-pool" style="display:none;" title="开启后启用 2925 账号池">
|
||||
<input type="checkbox" id="input-mail2925-use-account-pool" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
<span>号池</span>
|
||||
</label>
|
||||
<select id="select-mail2925-pool-account" class="data-select mail2925-pool-select" style="display:none;">
|
||||
<option value="">请选择号池邮箱</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-email-prefix" style="display:none;">
|
||||
<span class="data-label" id="label-email-prefix">别名基邮箱</span>
|
||||
<div class="data-inline">
|
||||
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input"
|
||||
placeholder="例如 yourname@example.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-inbucket-host" style="display:none;">
|
||||
<span class="data-label">Inbucket</span>
|
||||
<input type="text" id="input-inbucket-host" class="data-input" placeholder="填写主机或 https://主机地址" />
|
||||
</div>
|
||||
<div class="data-row" id="row-inbucket-mailbox" style="display:none;">
|
||||
<span class="data-label">邮箱名</span>
|
||||
<input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="例如 zju2001" />
|
||||
</div>
|
||||
<div class="data-row" id="row-auto-run-controls">
|
||||
<span class="data-label">注册邮箱</span>
|
||||
<div class="data-inline">
|
||||
<input type="text" id="input-email" class="data-input" placeholder="自动生成或手动粘贴邮箱" />
|
||||
<button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-auto-delay-settings">
|
||||
<span class="data-label">延迟</span>
|
||||
<div class="data-inline setting-pair">
|
||||
<div class="setting-group setting-group-primary">
|
||||
<label class="toggle-switch" for="input-auto-delay-enabled">
|
||||
<input type="checkbox" id="input-auto-delay-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-auto-delay-minutes" class="data-input auto-delay-input" value="30" min="1"
|
||||
max="1440" step="1" title="启动前倒计时分钟数" />
|
||||
<span class="data-unit">分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-group setting-group-secondary">
|
||||
<span class="setting-caption">步间间隔</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-auto-step-delay-seconds" class="data-input auto-delay-input" value=""
|
||||
min="0" max="600" step="1" title="步间延迟秒数,0 或留空则不延迟" />
|
||||
<span class="data-unit">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">自动重试</span>
|
||||
<div class="data-inline setting-pair">
|
||||
<div class="setting-group setting-group-primary">
|
||||
<label class="toggle-switch" for="input-auto-skip-failures">
|
||||
<input type="checkbox" id="input-auto-skip-failures" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-group setting-group-secondary">
|
||||
<span class="setting-caption">线程间隔</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-auto-skip-failures-thread-interval-minutes"
|
||||
class="data-input auto-delay-input" value="0" min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
|
||||
<span class="data-unit">分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-oauth-display">
|
||||
<span class="data-label">OAuth</span>
|
||||
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">回调</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<span id="display-localhost-url" class="data-value data-value-fill mono truncate">等待中...</span>
|
||||
<button id="btn-save-settings" class="btn btn-outline btn-sm data-inline-btn" type="button">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="phone-verification-section" class="data-card phone-verification-card">
|
||||
<div class="section-mini-header">
|
||||
<div class="section-mini-copy">
|
||||
<span class="section-label">接码设置</span>
|
||||
<span class="data-value">手机号验证与 HeroSMS 获取策略</span>
|
||||
</div>
|
||||
<div id="row-phone-verification-enabled" class="section-mini-actions phone-verification-header-actions">
|
||||
<button id="btn-toggle-phone-verification-section" class="btn btn-ghost btn-xs" type="button"
|
||||
aria-expanded="false" aria-controls="row-phone-verification-fold">展开设置</button>
|
||||
<label class="toggle-switch" for="input-phone-verification-enabled" title="启用或禁用手机号接码流程">
|
||||
<input type="checkbox" id="input-phone-verification-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row phone-verification-fold-row" id="row-phone-verification-fold" style="display:none;">
|
||||
<div id="phone-verification-fold" class="phone-verification-fold">
|
||||
<div class="phone-verification-fold-body">
|
||||
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
|
||||
<span class="data-label">同步服务</span>
|
||||
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono"
|
||||
placeholder="http://127.0.0.1:17373" />
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-platform" style="display:none;">
|
||||
<span class="data-label">接码平台</span>
|
||||
<span id="display-hero-sms-platform" class="data-value mono">HeroSMS / OpenAI / Thailand</span>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-country" style="display:none;">
|
||||
<span class="data-label">国家优先级</span>
|
||||
<div class="data-inline hero-sms-country-stack">
|
||||
<select id="select-hero-sms-country" class="data-input mono" multiple size="6" style="display:none;">
|
||||
<option value="52" selected>Thailand</option>
|
||||
</select>
|
||||
<div class="hero-sms-country-mainline">
|
||||
<div id="hero-sms-country-menu-shell" class="hero-sms-country-menu">
|
||||
<button id="btn-hero-sms-country-menu" class="btn btn-outline btn-sm hero-sms-country-menu-btn" type="button" aria-haspopup="listbox" aria-expanded="false">
|
||||
Thailand (1/3)
|
||||
</button>
|
||||
<div id="hero-sms-country-menu" class="hero-sms-country-menu-dropdown" role="listbox" aria-multiselectable="true" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="data-value hero-sms-country-note">多选最多 3 个,按点击顺序生效。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-country-fallback" style="display:none;">
|
||||
<span class="data-label">生效顺序</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<span id="display-hero-sms-country-fallback-order" class="data-value data-value-fill mono">Thailand(52)</span>
|
||||
<button id="btn-hero-sms-country-clear" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-acquire-priority" style="display:none;">
|
||||
<span class="data-label">拿号优先级</span>
|
||||
<select id="select-hero-sms-acquire-priority" class="data-input mono">
|
||||
<option value="country">国家优先(默认)</option>
|
||||
<option value="price">价格优先(同价按国家顺序)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
|
||||
<span class="data-label">接码 API</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-hero-sms-api-key" class="data-input data-input-with-icon mono"
|
||||
placeholder="请输入 HeroSMS API Key" />
|
||||
<button id="btn-toggle-hero-sms-api-key" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-hero-sms-api-key" data-show-label="显示 HeroSMS API Key"
|
||||
data-hide-label="隐藏 HeroSMS API Key" aria-label="显示 HeroSMS API Key" title="显示 HeroSMS API Key"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-max-price" style="display:none;">
|
||||
<span class="data-label">价格</span>
|
||||
<div class="data-inline hero-sms-price-preview-stack">
|
||||
<div class="hero-sms-price-preview-head">
|
||||
<button id="btn-hero-sms-price-preview" class="btn btn-outline btn-xs data-inline-btn" type="button">查询价格</button>
|
||||
</div>
|
||||
<div id="row-hero-sms-price-tiers" class="hero-sms-price-preview-result" style="display:none;">
|
||||
<span id="display-hero-sms-price-tiers" class="data-value mono hero-sms-price-preview-text">未获取</span>
|
||||
</div>
|
||||
<div class="hero-sms-price-controls-grid">
|
||||
<div class="hero-sms-price-control">
|
||||
<span class="hero-sms-settings-caption">价格上限</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-hero-sms-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.12" min="0" step="0.0001" title="接码价格上限;可空(空=自动价格)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-sms-price-control hero-sms-price-control-reuse">
|
||||
<span class="hero-sms-settings-caption">号码复用</span>
|
||||
<div class="setting-controls hero-sms-toggle-controls">
|
||||
<label class="toggle-switch hero-sms-price-reuse-toggle" for="input-hero-sms-reuse-enabled" title="开启后会优先复用未超次数的可用号码">
|
||||
<input type="checkbox" id="input-hero-sms-reuse-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-phone-code-settings-group" style="display:none;">
|
||||
<span class="data-label">接码参数</span>
|
||||
<div class="data-inline hero-sms-settings-grid">
|
||||
<div id="row-phone-verification-resend-count" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">验证码重发</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
|
||||
min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
|
||||
<span class="data-unit">次</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-replacement-limit" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">换号上限</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-replacement-limit" class="data-input auto-delay-input" value="3" min="1" max="20" step="1" title="步骤 9 内部允许更换号码的最大次数" />
|
||||
<span class="data-unit">次</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-code-wait-seconds" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">验证码限时</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-code-wait-seconds" class="data-input auto-delay-input" value="60" min="15" max="300" step="1" title="每轮等待验证码的秒数" />
|
||||
<span class="data-unit">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-code-timeout-windows" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">超时次数</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-code-timeout-windows" class="data-input auto-delay-input" value="2" min="1" max="10" step="1" title="验证码超时后,最多继续等待几轮再换号" />
|
||||
<span class="data-unit">轮</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-code-poll-interval-seconds" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">轮询间隔</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-code-poll-interval-seconds" class="data-input auto-delay-input" value="5" min="1" max="30" step="1" title="向 HeroSMS 查询验证码状态的间隔秒数" />
|
||||
<span class="data-unit">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-code-poll-max-rounds" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">轮询次数</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-code-poll-max-rounds" class="data-input auto-delay-input" value="4" min="1" max="120" step="1" title="每轮验证码等待窗口最多轮询次数" />
|
||||
<span class="data-unit">次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-runtime-pair" style="display:none;">
|
||||
<span class="data-label">运行状态</span>
|
||||
<div class="data-inline hero-sms-runtime-grid">
|
||||
<div id="row-hero-sms-current-number" class="hero-sms-runtime-cell" style="display:none;">
|
||||
<span class="hero-sms-runtime-key">当前分配</span>
|
||||
<span id="display-hero-sms-current-number" class="data-value mono hero-sms-runtime-value">未分配</span>
|
||||
</div>
|
||||
<div id="row-hero-sms-current-code" class="hero-sms-runtime-cell" style="display:none;">
|
||||
<span class="hero-sms-runtime-key">验证码</span>
|
||||
<span id="display-hero-sms-current-code" class="data-value mono hero-sms-runtime-value">未获取</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ip-proxy-section" class="data-card ip-proxy-card">
|
||||
<div class="section-mini-header">
|
||||
<div class="section-mini-copy">
|
||||
<span class="section-label">IP 代理</span>
|
||||
<span class="data-value">用于浏览器代理接管与出口切换</span>
|
||||
</div>
|
||||
<div id="row-ip-proxy-enabled" class="section-mini-actions ip-proxy-header-actions">
|
||||
<button id="btn-toggle-ip-proxy-section" class="btn btn-ghost btn-xs" type="button"
|
||||
aria-expanded="false" aria-controls="row-ip-proxy-fold">展开设置</button>
|
||||
<label class="toggle-switch" for="input-ip-proxy-enabled" title="启用或禁用 IP 代理接管">
|
||||
<input type="checkbox" id="input-ip-proxy-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
<span id="ip-proxy-enabled-status" class="ip-proxy-enabled-status" aria-live="polite">
|
||||
<span id="ip-proxy-enabled-status-dot" class="ip-proxy-enabled-status-dot" aria-hidden="true"></span>
|
||||
<span id="ip-proxy-enabled-status-text" class="ip-proxy-enabled-status-text">未开启</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-fold-row" id="row-ip-proxy-fold" style="display:none;">
|
||||
@@ -219,7 +593,7 @@
|
||||
<option value="711proxy">711Proxy(首版)</option>
|
||||
</select>
|
||||
<button id="btn-ip-proxy-service-login" class="btn btn-outline btn-sm data-inline-btn" type="button">
|
||||
登录
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,259 +705,26 @@
|
||||
</div>
|
||||
<div class="data-row" id="row-ip-proxy-runtime-status" style="display:none;">
|
||||
<span class="data-label">代理状态</span>
|
||||
<div id="ip-proxy-runtime-status" class="ip-proxy-runtime-status" aria-live="polite">
|
||||
<div id="ip-proxy-runtime-status" class="ip-proxy-runtime-status state-idle">
|
||||
<span id="ip-proxy-runtime-dot" class="ip-proxy-runtime-dot" aria-hidden="true"></span>
|
||||
<div class="ip-proxy-runtime-content">
|
||||
<div class="ip-proxy-runtime-main">
|
||||
<span id="ip-proxy-runtime-text" class="data-value data-value-fill">未启用,沿用浏览器默认/全局代理。</span>
|
||||
</div>
|
||||
<div id="ip-proxy-runtime-text" class="ip-proxy-runtime-main">未启用,沿用浏览器默认/全局代理。</div>
|
||||
<div class="ip-proxy-runtime-meta">
|
||||
<span id="ip-proxy-current" class="ip-proxy-runtime-current mono truncate">未启用</span>
|
||||
<button id="btn-ip-proxy-check-ip" class="btn btn-outline btn-xs data-inline-btn ip-proxy-check-ip-btn" type="button">检查IP</button>
|
||||
<span id="ip-proxy-current" class="ip-proxy-runtime-current">暂无可用代理</span>
|
||||
</div>
|
||||
<div class="ip-proxy-runtime-details-row">
|
||||
<details id="ip-proxy-runtime-details" class="ip-proxy-runtime-details" hidden>
|
||||
<summary>详情</summary>
|
||||
<div id="ip-proxy-runtime-details-text" class="ip-proxy-runtime-details-text"></div>
|
||||
</details>
|
||||
<button id="btn-ip-proxy-check-ip" class="btn btn-outline btn-xs ip-proxy-check-ip-btn" type="button">检查IP</button>
|
||||
</div>
|
||||
<details id="ip-proxy-runtime-details" class="ip-proxy-runtime-details" hidden>
|
||||
<summary>查看详细信息</summary>
|
||||
<div id="ip-proxy-runtime-details-text" class="ip-proxy-runtime-details-text"></div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-codex2api-url" style="display:none;">
|
||||
<span class="data-label">Codex2API</span>
|
||||
<input type="text" id="input-codex2api-url" class="data-input"
|
||||
placeholder="http://localhost:8080/admin/accounts" />
|
||||
</div>
|
||||
<div class="data-row" id="row-codex2api-admin-key" style="display:none;">
|
||||
<span class="data-label">管理密钥</span>
|
||||
<input type="password" id="input-codex2api-admin-key" class="data-input"
|
||||
placeholder="请输入 Codex2API Admin Secret" />
|
||||
</div>
|
||||
<div class="data-row" id="row-custom-password">
|
||||
<span class="data-label">账户密码</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-password" class="data-input data-input-with-icon"
|
||||
placeholder="codex密码,留空则自动生成" />
|
||||
<button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码" title="显示密码"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-plus-mode">
|
||||
<span class="data-label">Plus 模式</span>
|
||||
<div class="data-inline setting-pair">
|
||||
<div class="setting-group setting-group-primary">
|
||||
<label class="toggle-switch" for="input-plus-mode-enabled" title="开启后使用 Plus Checkout + PayPal 授权流程">
|
||||
<input type="checkbox" id="input-plus-mode-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="setting-caption">PayPal 订阅链路</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-paypal-email" style="display:none;">
|
||||
<span class="data-label">PayPal 账号</span>
|
||||
<input type="text" id="input-paypal-email" class="data-input" placeholder="请输入 PayPal 登录邮箱" />
|
||||
</div>
|
||||
<div class="data-row" id="row-paypal-password" style="display:none;">
|
||||
<span class="data-label">PayPal 密码</span>
|
||||
<input type="password" id="input-paypal-password" class="data-input" placeholder="请输入 PayPal 登录密码" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">邮箱服务</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-mail-provider" class="data-select">
|
||||
<option value="custom">自定义邮箱</option>
|
||||
<option value="hotmail-api">Hotmail(账号池)</option>
|
||||
<option value="luckmail-api">LuckMail(API 购邮)</option>
|
||||
<option value="icloud">iCloud 邮箱</option>
|
||||
<option value="163">163 邮箱 (mail.163.com)</option>
|
||||
<option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
|
||||
<option value="126">126 邮箱 (mail.126.com)</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
|
||||
<option value="inbucket">Inbucket(自定义主机)</option>
|
||||
<option value="2925">2925 邮箱 (2925.com)</option>
|
||||
<option value="gmail">Gmail 邮箱 (mail.google.com)</option>
|
||||
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
|
||||
</select>
|
||||
<button id="btn-mail-login" class="btn btn-outline btn-sm data-inline-btn" type="button" disabled>登录</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row data-check-row" id="row-custom-mail-provider-pool" style="display:none;">
|
||||
<span class="data-label">自定义号池</span>
|
||||
<textarea id="input-custom-mail-provider-pool" class="data-textarea"
|
||||
placeholder="每行一个注册邮箱,例如 alias001@example.com alias002@example.com"></textarea>
|
||||
</div>
|
||||
<div class="data-row" id="row-mail-2925-mode" style="display:none;">
|
||||
<span class="data-label">2925 模式</span>
|
||||
<div id="mail-2925-mode-group" class="choice-group" role="group" aria-label="2925 邮箱模式">
|
||||
<button type="button" class="choice-btn" data-mail2925-mode="provide">提供邮箱</button>
|
||||
<button type="button" class="choice-btn" data-mail2925-mode="receive">接收邮箱</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-email-generator">
|
||||
<span class="data-label">邮箱生成</span>
|
||||
<select id="select-email-generator" class="data-select">
|
||||
<option value="gmail-alias">Gmail +tag</option>
|
||||
<option value="duck">DuckDuckGo</option>
|
||||
<option value="custom-pool">自定义邮箱池</option>
|
||||
<option value="cloudflare">Cloudflare</option>
|
||||
<option value="icloud">iCloud 隐私邮箱</option>
|
||||
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row data-check-row" id="row-custom-email-pool" style="display:none;">
|
||||
<span class="data-label">邮箱池</span>
|
||||
<textarea id="input-custom-email-pool" class="data-textarea"
|
||||
placeholder="每行一个邮箱,例如 alias001@gmail.com alias002@gmail.com"></textarea>
|
||||
</div>
|
||||
<div class="data-row" id="row-cf-domain" style="display:none;">
|
||||
<span class="data-label">CF 域名</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-cf-domain" class="data-select"></select>
|
||||
<input type="text" id="input-cf-domain" class="data-input" placeholder="例如 yourdomain.xyz"
|
||||
style="display:none;" />
|
||||
<button id="btn-cf-domain-mode" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-mail2925-pool-settings" style="display:none;">
|
||||
<span class="data-label">2925 号池</span>
|
||||
<div class="data-inline mail2925-base-inline">
|
||||
<label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool"
|
||||
for="input-mail2925-use-account-pool" style="display:none;" title="开启后启用 2925 账号池">
|
||||
<input type="checkbox" id="input-mail2925-use-account-pool" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
<span>号池</span>
|
||||
</label>
|
||||
<select id="select-mail2925-pool-account" class="data-select mail2925-pool-select" style="display:none;">
|
||||
<option value="">请选择号池邮箱</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-email-prefix" style="display:none;">
|
||||
<span class="data-label" id="label-email-prefix">别名基邮箱</span>
|
||||
<div class="data-inline">
|
||||
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input"
|
||||
placeholder="例如 yourname@example.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-inbucket-host" style="display:none;">
|
||||
<span class="data-label">Inbucket</span>
|
||||
<input type="text" id="input-inbucket-host" class="data-input" placeholder="填写主机或 https://主机地址" />
|
||||
</div>
|
||||
<div class="data-row" id="row-inbucket-mailbox" style="display:none;">
|
||||
<span class="data-label">邮箱名</span>
|
||||
<input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="例如 zju2001" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">注册邮箱</span>
|
||||
<div class="data-inline">
|
||||
<input type="text" id="input-email" class="data-input" placeholder="自动生成或手动粘贴邮箱" />
|
||||
<button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">延迟</span>
|
||||
<div class="data-inline setting-pair">
|
||||
<div class="setting-group setting-group-primary">
|
||||
<label class="toggle-switch" for="input-auto-delay-enabled">
|
||||
<input type="checkbox" id="input-auto-delay-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-auto-delay-minutes" class="data-input auto-delay-input" value="30" min="1"
|
||||
max="1440" step="1" title="启动前倒计时分钟数" />
|
||||
<span class="data-unit">分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-group setting-group-secondary">
|
||||
<span class="setting-caption">步间间隔</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-auto-step-delay-seconds" class="data-input auto-delay-input" value=""
|
||||
min="0" max="600" step="1" title="步间延迟秒数,0 或留空则不延迟" />
|
||||
<span class="data-unit">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">自动重试</span>
|
||||
<div class="data-inline setting-pair">
|
||||
<div class="setting-group setting-group-primary">
|
||||
<label class="toggle-switch" for="input-auto-skip-failures">
|
||||
<input type="checkbox" id="input-auto-skip-failures" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-group setting-group-secondary">
|
||||
<span class="setting-caption">线程间隔</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-auto-skip-failures-thread-interval-minutes"
|
||||
class="data-input auto-delay-input" value="0" min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
|
||||
<span class="data-unit">分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-phone-verification-enabled">
|
||||
<span class="data-label">接码</span>
|
||||
<div class="data-inline setting-pair">
|
||||
<div class="setting-group setting-group-primary">
|
||||
<label class="toggle-switch" for="input-phone-verification-enabled">
|
||||
<input type="checkbox" id="input-phone-verification-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-group setting-group-secondary">
|
||||
<span class="setting-caption">验证码重发</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
|
||||
min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
|
||||
<span class="data-unit">次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
|
||||
<span class="data-label">同步服务</span>
|
||||
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono"
|
||||
placeholder="http://127.0.0.1:17373" />
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-platform" style="display:none;">
|
||||
<span class="data-label">接码平台</span>
|
||||
<span id="display-hero-sms-platform" class="data-value mono">HeroSMS / OpenAI / Thailand</span>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-country" style="display:none;">
|
||||
<span class="data-label">接码国家</span>
|
||||
<select id="select-hero-sms-country" class="data-input mono">
|
||||
<option value="52" selected>Thailand</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
|
||||
<span class="data-label">接码 API</span>
|
||||
<input type="password" id="input-hero-sms-api-key" class="data-input mono" placeholder="请输入 HeroSMS API Key" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">OAuth</span>
|
||||
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">回调</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<span id="display-localhost-url" class="data-value data-value-fill mono truncate">等待中...</span>
|
||||
<button id="btn-save-settings" class="btn btn-outline btn-sm data-inline-btn" type="button">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cloudflare-temp-email-section" class="data-card hotmail-card" style="display:none;">
|
||||
<div class="section-mini-header">
|
||||
@@ -602,13 +743,23 @@
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
||||
<span class="data-label">Admin Auth</span>
|
||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon"
|
||||
placeholder="Cloudflare Temp Email admin password" />
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon"
|
||||
placeholder="Cloudflare Temp Email admin password" />
|
||||
<button id="btn-toggle-temp-email-admin-auth" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-temp-email-admin-auth" data-show-label="显示 Admin Auth"
|
||||
data-hide-label="隐藏 Admin Auth" aria-label="显示 Admin Auth" title="显示 Admin Auth"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
||||
<span class="data-label">Custom Auth</span>
|
||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon"
|
||||
placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon"
|
||||
placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
||||
<button id="btn-toggle-temp-email-custom-auth" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-temp-email-custom-auth" data-show-label="显示 Custom Auth"
|
||||
data-hide-label="隐藏 Custom Auth" aria-label="显示 Custom Auth" title="显示 Custom Auth"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
|
||||
<span class="data-label">邮件接收</span>
|
||||
@@ -682,12 +833,24 @@
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">邮箱密码</span>
|
||||
<input type="password" id="input-hotmail-password" class="data-input" placeholder="可选,仅用于记录" />
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-hotmail-password" class="data-input data-input-with-icon"
|
||||
placeholder="可选,仅用于记录" />
|
||||
<button id="btn-toggle-hotmail-password" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-hotmail-password" data-show-label="显示 Hotmail 密码"
|
||||
data-hide-label="隐藏 Hotmail 密码" aria-label="显示 Hotmail 密码" title="显示 Hotmail 密码"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">刷新令牌</span>
|
||||
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
|
||||
placeholder="必填,粘贴刷新令牌(refresh token)" />
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-hotmail-refresh-token" class="data-input data-input-with-icon mono"
|
||||
placeholder="必填,粘贴刷新令牌(refresh token)" />
|
||||
<button id="btn-toggle-hotmail-refresh-token" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-hotmail-refresh-token" data-show-label="显示 Hotmail 刷新令牌"
|
||||
data-hide-label="隐藏 Hotmail 刷新令牌" aria-label="显示 Hotmail 刷新令牌"
|
||||
title="显示 Hotmail 刷新令牌"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row hotmail-actions-row">
|
||||
<span class="data-label"></span>
|
||||
@@ -729,7 +892,13 @@
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">密码</span>
|
||||
<input type="password" id="input-mail2925-password" class="data-input" placeholder="2925 登录密码" />
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-mail2925-password" class="data-input data-input-with-icon"
|
||||
placeholder="2925 登录密码" />
|
||||
<button id="btn-toggle-mail2925-password" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-mail2925-password" data-show-label="显示 2925 密码"
|
||||
data-hide-label="隐藏 2925 密码" aria-label="显示 2925 密码" title="显示 2925 密码"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row hotmail-actions-row">
|
||||
<span class="data-label"></span>
|
||||
@@ -759,7 +928,13 @@
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">API Key</span>
|
||||
<input type="password" id="input-luckmail-api-key" class="data-input mono" placeholder="请输入 LuckMail API Key" />
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-luckmail-api-key" class="data-input data-input-with-icon mono"
|
||||
placeholder="请输入 LuckMail API Key" />
|
||||
<button id="btn-toggle-luckmail-api-key" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-luckmail-api-key" data-show-label="显示 LuckMail API Key"
|
||||
data-hide-label="隐藏 LuckMail API Key" aria-label="显示 LuckMail API Key" title="显示 LuckMail API Key"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Base URL</span>
|
||||
@@ -986,6 +1161,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shared-form-modal" class="modal-overlay" hidden>
|
||||
<div class="modal-card modal-card-form">
|
||||
<div class="modal-header">
|
||||
<span id="shared-form-modal-title" class="modal-title">添加账号</span>
|
||||
<button id="btn-shared-form-modal-close" class="modal-close" type="button" aria-label="关闭">×</button>
|
||||
</div>
|
||||
<p id="shared-form-modal-message" class="modal-message" hidden></p>
|
||||
<p id="shared-form-modal-alert" class="modal-alert" hidden></p>
|
||||
<div id="shared-form-modal-fields" class="modal-form-fields"></div>
|
||||
<div class="modal-actions">
|
||||
<button id="btn-shared-form-modal-cancel" class="btn btn-ghost btn-sm" type="button">取消</button>
|
||||
<button id="btn-shared-form-modal-confirm" class="btn btn-primary btn-sm" type="button">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="auto-start-modal" class="modal-overlay" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-header">
|
||||
@@ -1012,6 +1203,7 @@
|
||||
<input id="input-import-settings-file" type="file" accept=".json,application/json" hidden />
|
||||
<script src="../managed-alias-utils.js"></script>
|
||||
<script src="../mail2925-utils.js"></script>
|
||||
<script src="../paypal-utils.js"></script>
|
||||
<script src="../icloud-utils.js"></script>
|
||||
<script src="../mail-provider-utils.js"></script>
|
||||
<script src="../hotmail-utils.js"></script>
|
||||
@@ -1020,8 +1212,10 @@
|
||||
<script src="update-service.js"></script>
|
||||
<script src="contribution-content-update-service.js"></script>
|
||||
<script src="account-pool-ui.js"></script>
|
||||
<script src="form-dialog.js"></script>
|
||||
<script src="hotmail-manager.js"></script>
|
||||
<script src="mail-2925-manager.js"></script>
|
||||
<script src="paypal-manager.js"></script>
|
||||
<script src="icloud-manager.js"></script>
|
||||
<script src="luckmail-manager.js"></script>
|
||||
<script src="ip-proxy-provider-711proxy.js"></script>
|
||||
|
||||
+1567
-83
File diff suppressed because it is too large
Load Diff
@@ -114,6 +114,12 @@ let currentState = {
|
||||
inbucketMailbox: '',
|
||||
cloudflareDomain: '',
|
||||
cloudflareDomains: [],
|
||||
reusablePhoneActivation: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
successfulUses: 1,
|
||||
maxUses: 3,
|
||||
},
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
};
|
||||
@@ -329,6 +335,16 @@ return {
|
||||
assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion');
|
||||
assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset');
|
||||
assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset');
|
||||
assert.deepStrictEqual(
|
||||
snapshot.currentState.reusablePhoneActivation,
|
||||
{
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
successfulUses: 1,
|
||||
maxUses: 3,
|
||||
},
|
||||
'reusable phone activation should survive fresh-attempt reset'
|
||||
);
|
||||
|
||||
console.log('auto-run fresh attempt reset tests passed');
|
||||
})().catch((error) => {
|
||||
|
||||
@@ -215,18 +215,20 @@ test('auto-run stops restarting on generic phone-page failure messages even with
|
||||
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts from step 7 when phone verification cannot receive SMS after resend', async () => {
|
||||
test('auto-run does not restart step 7 when phone verification exhausted replacement attempts in add-phone flow', async () => {
|
||||
const harness = createHarness({
|
||||
failureStep: 9,
|
||||
failureBudget: 1,
|
||||
failureMessage: 'Phone verification could not receive an SMS after resend. Restart step 7 with a new number. Current number: 66959916439.',
|
||||
failureMessage: 'Step 9: phone verification did not succeed after 3 number replacements. Last reason: sms_timeout_after_resend.',
|
||||
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
const result = await harness.runAndCaptureError();
|
||||
|
||||
assert.equal(events.invalidations.length, 1);
|
||||
assert.deepStrictEqual(events.steps, [7, 8, 9, 7, 8, 9, 10]);
|
||||
assert.ok(result?.error);
|
||||
assert.equal(result.events.invalidations.length, 0);
|
||||
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
|
||||
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
|
||||
|
||||
@@ -54,6 +54,13 @@ test('background account history settings are normalized independently from hotm
|
||||
extractFunction('normalizeHotmailLocalBaseUrl'),
|
||||
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
|
||||
extractFunction('normalizeVerificationResendCount'),
|
||||
extractFunction('normalizePhoneVerificationReplacementLimit'),
|
||||
extractFunction('normalizePhoneCodeWaitSeconds'),
|
||||
extractFunction('normalizePhoneCodeTimeoutWindows'),
|
||||
extractFunction('normalizePhoneCodePollIntervalSeconds'),
|
||||
extractFunction('normalizePhoneCodePollMaxRounds'),
|
||||
extractFunction('normalizeHeroSmsMaxPrice'),
|
||||
extractFunction('normalizeHeroSmsCountryFallback'),
|
||||
extractFunction('normalizePersistentSettingValue'),
|
||||
].join('\n');
|
||||
|
||||
@@ -63,11 +70,28 @@ const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_U
|
||||
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
|
||||
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const PHONE_CODE_POLL_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_ROUNDS_MAX = 120;
|
||||
const DEFAULT_PHONE_CODE_POLL_ROUNDS = 4;
|
||||
const DEFAULT_SUB2API_PROXY_NAME = '';
|
||||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||||
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
|
||||
const VERIFICATION_RESEND_COUNT_MIN = 0;
|
||||
const VERIFICATION_RESEND_COUNT_MAX = 20;
|
||||
const HERO_SMS_COUNTRY_ID = 52;
|
||||
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PERSISTED_SETTING_DEFAULTS = {
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: '163',
|
||||
@@ -101,6 +125,18 @@ return {
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneVerificationEnabled', 1), true);
|
||||
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
|
||||
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '9'), 9);
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '-1'), 1);
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneCodeWaitSeconds', '75'), 75);
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneCodeTimeoutWindows', '3'), 3);
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneCodePollIntervalSeconds', '6'), 6);
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
|
||||
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
|
||||
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
|
||||
assert.deepStrictEqual(
|
||||
api.normalizePersistentSettingValue('heroSmsCountryFallback', [{ id: 16, label: 'United Kingdom' }, { id: 52 }]),
|
||||
[{ id: 16, label: 'United Kingdom' }, { id: 52, label: 'Country #52' }]
|
||||
);
|
||||
assert.equal(
|
||||
api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
|
||||
'http://127.0.0.1:17373'
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
assert.match(source, /cpaOAuthState:\s*null/);
|
||||
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,/);
|
||||
});
|
||||
|
||||
test('message router step-1 handler stores cpa oauth runtime keys', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
|
||||
const updates = [];
|
||||
const router = api.createMessageRouter({
|
||||
broadcastDataUpdate: () => {},
|
||||
setState: async (payload) => {
|
||||
updates.push(payload);
|
||||
},
|
||||
});
|
||||
|
||||
await router.handleStepData(1, {
|
||||
cpaOAuthState: 'oauth-state-1',
|
||||
cpaManagementOrigin: 'http://localhost:8317',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(updates, [
|
||||
{
|
||||
cpaOAuthState: 'oauth-state-1',
|
||||
cpaManagementOrigin: 'http://localhost:8317',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -625,6 +625,8 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
|
||||
" luckmailPreserveTagName: '保留',",
|
||||
" currentLuckmailPurchase: { token: 'stale' },",
|
||||
" currentLuckmailMailCursor: { messageId: 'stale' },",
|
||||
' currentPhoneActivation: null,',
|
||||
' reusablePhoneActivation: null,',
|
||||
' email: null,',
|
||||
'};',
|
||||
'const CONTRIBUTION_RUNTIME_DEFAULTS = {',
|
||||
@@ -670,6 +672,7 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
|
||||
" accounts: [{ email: 'saved@example.com' }],",
|
||||
" tabRegistry: { foo: { tabId: 1 } },",
|
||||
" sourceLastUrls: { foo: 'https://example.com' },",
|
||||
" reusablePhoneActivation: { activationId: 'rx-001', phoneNumber: '66951112222', provider: 'hero-sms', serviceCode: 'dr', countryId: 52, successfulUses: 1, maxUses: 3 },",
|
||||
" luckmailApiKey: 'sk-session',",
|
||||
" luckmailBaseUrl: 'https://demo.example.com/',",
|
||||
" luckmailEmailType: 'ms_imap',",
|
||||
@@ -711,6 +714,16 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
|
||||
assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
|
||||
assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
|
||||
assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
|
||||
assert.deepStrictEqual(snapshot.storedPayload.reusablePhoneActivation, {
|
||||
activationId: 'rx-001',
|
||||
phoneNumber: '66951112222',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
successfulUses: 1,
|
||||
maxUses: 3,
|
||||
});
|
||||
assert.equal(snapshot.storedPayload.currentPhoneActivation, null);
|
||||
});
|
||||
|
||||
test('handleStepData step 10 marks current LuckMail purchase as used and clears runtime state', async () => {
|
||||
@@ -765,6 +778,7 @@ function broadcastDataUpdate() {}
|
||||
function isLocalhostOAuthCallbackUrl() {
|
||||
return true;
|
||||
}
|
||||
async function finalizePhoneActivationAfterSuccessfulFlow() {}
|
||||
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
|
||||
test('message router appends success record on Plus final step instead of hard-coded step 10', async () => {
|
||||
const appendCalls = [];
|
||||
const router = api.createMessageRouter({
|
||||
addLog: async () => {},
|
||||
appendAccountRunRecord: async (...args) => {
|
||||
appendCalls.push(args);
|
||||
},
|
||||
batchUpdateLuckmailPurchases: async () => {},
|
||||
buildLocalhostCleanupPrefix: () => '',
|
||||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||||
buildPersistentSettingsPayload: () => ({}),
|
||||
broadcastDataUpdate: () => {},
|
||||
cancelScheduledAutoRun: async () => {},
|
||||
checkIcloudSession: async () => {},
|
||||
clearAutoRunTimerAlarm: async () => {},
|
||||
clearLuckmailRuntimeState: async () => {},
|
||||
clearStopRequest: () => {},
|
||||
closeLocalhostCallbackTabs: async () => {},
|
||||
closeTabsByUrlPrefix: async () => {},
|
||||
deleteHotmailAccount: async () => {},
|
||||
deleteHotmailAccounts: async () => {},
|
||||
deleteIcloudAlias: async () => {},
|
||||
deleteUsedIcloudAliases: async () => {},
|
||||
disableUsedLuckmailPurchases: async () => {},
|
||||
doesStepUseCompletionSignal: () => false,
|
||||
ensureManualInteractionAllowed: async () => ({}),
|
||||
executeStep: async () => {},
|
||||
executeStepViaCompletionSignal: async () => {},
|
||||
exportSettingsBundle: async () => ({}),
|
||||
fetchGeneratedEmail: async () => '',
|
||||
finalizeStep3Completion: async () => {},
|
||||
finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
|
||||
findHotmailAccount: async () => null,
|
||||
flushCommand: async () => {},
|
||||
getCurrentLuckmailPurchase: () => null,
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getSourceLabel: () => '',
|
||||
getState: async () => ({ plusModeEnabled: true, stepStatuses: { 13: 'pending' } }),
|
||||
getLastStepIdForState: () => 13,
|
||||
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'oauth-login' : 'platform-verify' }),
|
||||
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
||||
getTabId: async () => null,
|
||||
getStopRequested: () => false,
|
||||
handleAutoRunLoopUnhandledError: async () => {},
|
||||
handleCloudflareSecurityBlocked: async () => '',
|
||||
importSettingsBundle: async () => {},
|
||||
invalidateDownstreamAfterStepRestart: async () => {},
|
||||
isCloudflareSecurityBlockedError: () => false,
|
||||
isAutoRunLockedState: () => false,
|
||||
isHotmailProvider: () => false,
|
||||
isLocalhostOAuthCallbackUrl: () => true,
|
||||
isLuckmailProvider: () => false,
|
||||
isStopError: () => false,
|
||||
isTabAlive: async () => false,
|
||||
launchAutoRunTimerPlan: async () => {},
|
||||
listIcloudAliases: async () => [],
|
||||
listLuckmailPurchasesForManagement: async () => [],
|
||||
normalizeHotmailAccounts: (items) => items,
|
||||
normalizeRunCount: (value) => value,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
|
||||
notifyStepComplete: () => {},
|
||||
notifyStepError: () => {},
|
||||
patchHotmailAccount: async () => {},
|
||||
patchMail2925Account: async () => {},
|
||||
registerTab: async () => {},
|
||||
requestStop: async () => {},
|
||||
resetState: async () => {},
|
||||
resumeAutoRun: async () => {},
|
||||
scheduleAutoRun: async () => {},
|
||||
selectLuckmailPurchase: async () => {},
|
||||
setCurrentHotmailAccount: async () => {},
|
||||
setCurrentMail2925Account: async () => {},
|
||||
setContributionMode: async () => {},
|
||||
setEmailState: async () => {},
|
||||
setEmailStateSilently: async () => {},
|
||||
setIcloudAliasPreservedState: async () => {},
|
||||
setIcloudAliasUsedState: async () => {},
|
||||
setLuckmailPurchaseDisabledState: async () => {},
|
||||
setLuckmailPurchasePreservedState: async () => {},
|
||||
setLuckmailPurchaseUsedState: async () => {},
|
||||
setPersistentSettings: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
skipAutoRunCountdown: async () => false,
|
||||
skipStep: async () => {},
|
||||
startAutoRunLoop: async () => {},
|
||||
syncHotmailAccounts: async () => {},
|
||||
testHotmailAccountMailAccess: async () => {},
|
||||
upsertHotmailAccount: async () => {},
|
||||
verifyHotmailAccount: async () => {},
|
||||
});
|
||||
|
||||
await router.handleMessage({ type: 'STEP_COMPLETE', step: 13, payload: {} }, {});
|
||||
|
||||
assert.equal(appendCalls.length, 1);
|
||||
assert.equal(appendCalls[0][0], 'success');
|
||||
});
|
||||
@@ -12,6 +12,7 @@ function createRouter(overrides = {}) {
|
||||
stepStatuses: [],
|
||||
emailStates: [],
|
||||
finalizePayloads: [],
|
||||
phoneFinalizations: [],
|
||||
notifyCompletions: [],
|
||||
notifyErrors: [],
|
||||
securityBlocks: [],
|
||||
@@ -49,10 +50,13 @@ function createRouter(overrides = {}) {
|
||||
executeStepViaCompletionSignal: async () => {},
|
||||
exportSettingsBundle: async () => ({}),
|
||||
fetchGeneratedEmail: async () => '',
|
||||
finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
|
||||
events.phoneFinalizations.push(state);
|
||||
}),
|
||||
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
|
||||
events.finalizePayloads.push(payload);
|
||||
}),
|
||||
finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
|
||||
finalizeIcloudAliasAfterSuccessfulFlow: overrides.finalizeIcloudAliasAfterSuccessfulFlow || (async () => {}),
|
||||
findHotmailAccount: async () => null,
|
||||
flushCommand: async () => {},
|
||||
getCurrentLuckmailPurchase: () => null,
|
||||
@@ -61,6 +65,7 @@ function createRouter(overrides = {}) {
|
||||
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
|
||||
getStepDefinitionForState: overrides.getStepDefinitionForState,
|
||||
getStepIdsForState: overrides.getStepIdsForState,
|
||||
getLastStepIdForState: overrides.getLastStepIdForState,
|
||||
getTabId: overrides.getTabId || (async () => null),
|
||||
getStopRequested: () => false,
|
||||
handleAutoRunLoopUnhandledError: async () => {},
|
||||
@@ -262,6 +267,68 @@ test('message router finalizes step 3 before marking it completed', async () =>
|
||||
assert.deepStrictEqual(response, { ok: true });
|
||||
});
|
||||
|
||||
test('message router finalizes pending phone activation on platform verify success', async () => {
|
||||
const state = {
|
||||
stepStatuses: { 10: 'pending' },
|
||||
reusablePhoneActivation: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
},
|
||||
pendingPhoneActivationConfirmation: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
},
|
||||
};
|
||||
const { router, events } = createRouter({
|
||||
state,
|
||||
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
|
||||
});
|
||||
|
||||
await router.handleStepData(10, {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.phoneFinalizations, [state]);
|
||||
});
|
||||
|
||||
test('message router does not finalize pending phone activation when icloud finalization fails', async () => {
|
||||
const state = {
|
||||
stepStatuses: { 10: 'pending' },
|
||||
reusablePhoneActivation: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
},
|
||||
pendingPhoneActivationConfirmation: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
},
|
||||
};
|
||||
const { router, events } = createRouter({
|
||||
state,
|
||||
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
|
||||
finalizeIcloudAliasAfterSuccessfulFlow: async () => {
|
||||
throw new Error('icloud finalize failed');
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => router.handleStepData(10, {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
|
||||
}),
|
||||
/icloud finalize failed/
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(events.phoneFinalizations, []);
|
||||
});
|
||||
|
||||
test('message router marks step 3 failed when post-submit finalize fails', async () => {
|
||||
const { router, events } = createRouter({
|
||||
finalizeStep3Completion: async () => {
|
||||
|
||||
@@ -71,3 +71,48 @@ test('panel bridge can request codex2api oauth url via protocol', async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('panel bridge can request cpa oauth url via management api', async () => {
|
||||
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
assert.equal(url, 'http://localhost:8317/v0/management/codex-auth-url');
|
||||
assert.equal(options.method, 'GET');
|
||||
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
|
||||
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: 'ok',
|
||||
url: 'https://auth.openai.com/authorize?state=cpa-oauth-state',
|
||||
state: 'cpa-oauth-state',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
|
||||
const bridge = api.createPanelBridge({
|
||||
addLog: async () => {},
|
||||
getPanelMode: () => 'cpa',
|
||||
normalizeCodex2ApiUrl: (value) => value,
|
||||
normalizeSub2ApiUrl: (value) => value,
|
||||
DEFAULT_SUB2API_GROUP_NAME: 'codex',
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
|
||||
});
|
||||
|
||||
const result = await bridge.requestOAuthUrlFromPanel({
|
||||
panelMode: 'cpa',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
}, { logLabel: '步骤 7' });
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
oauthUrl: 'https://auth.openai.com/authorize?state=cpa-oauth-state',
|
||||
cpaOAuthState: 'cpa-oauth-state',
|
||||
cpaManagementOrigin: 'http://localhost:8317',
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports paypal account store module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/paypal-account-store\.js/);
|
||||
assert.match(source, /paypal-utils\.js/);
|
||||
});
|
||||
|
||||
test('paypal account store module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/paypal-account-store.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPayPalAccountStore;`)(globalScope);
|
||||
|
||||
assert.equal(typeof api?.createPayPalAccountStore, 'function');
|
||||
});
|
||||
|
||||
test('paypal account store selects account and keeps legacy paypal credentials in sync', async () => {
|
||||
const source = fs.readFileSync('background/paypal-account-store.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPayPalAccountStore;`)(globalScope);
|
||||
|
||||
let latestState = {
|
||||
paypalAccounts: [],
|
||||
currentPayPalAccountId: '',
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
};
|
||||
const broadcasts = [];
|
||||
|
||||
const store = api.createPayPalAccountStore({
|
||||
broadcastDataUpdate(payload) {
|
||||
broadcasts.push(payload);
|
||||
},
|
||||
findPayPalAccount(accounts, accountId) {
|
||||
return (Array.isArray(accounts) ? accounts : []).find((account) => account.id === accountId) || null;
|
||||
},
|
||||
getState: async () => latestState,
|
||||
normalizePayPalAccount(account = {}) {
|
||||
return {
|
||||
id: String(account.id || 'generated'),
|
||||
email: String(account.email || '').trim().toLowerCase(),
|
||||
password: String(account.password || ''),
|
||||
createdAt: Number(account.createdAt) || 1,
|
||||
updatedAt: Number(account.updatedAt) || 1,
|
||||
lastUsedAt: Number(account.lastUsedAt) || 0,
|
||||
};
|
||||
},
|
||||
normalizePayPalAccounts(accounts) {
|
||||
return Array.isArray(accounts) ? accounts.slice() : [];
|
||||
},
|
||||
setPersistentSettings: async (updates) => {
|
||||
latestState = { ...latestState, ...updates };
|
||||
},
|
||||
setState: async (updates) => {
|
||||
latestState = { ...latestState, ...updates };
|
||||
},
|
||||
upsertPayPalAccountInList(accounts, nextAccount) {
|
||||
const list = Array.isArray(accounts) ? accounts.slice() : [];
|
||||
const existingIndex = list.findIndex((account) => account.id === nextAccount.id);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = nextAccount;
|
||||
return list;
|
||||
}
|
||||
list.push(nextAccount);
|
||||
return list;
|
||||
},
|
||||
});
|
||||
|
||||
const account = await store.upsertPayPalAccount({
|
||||
id: 'pp-1',
|
||||
email: 'User@Example.com',
|
||||
password: 'secret',
|
||||
});
|
||||
assert.equal(account.email, 'user@example.com');
|
||||
|
||||
const selected = await store.setCurrentPayPalAccount('pp-1');
|
||||
assert.equal(selected.id, 'pp-1');
|
||||
assert.equal(latestState.currentPayPalAccountId, 'pp-1');
|
||||
assert.equal(latestState.paypalEmail, 'user@example.com');
|
||||
assert.equal(latestState.paypalPassword, 'secret');
|
||||
assert.deepStrictEqual(
|
||||
broadcasts.at(-1),
|
||||
{
|
||||
currentPayPalAccountId: 'pp-1',
|
||||
paypalEmail: 'user@example.com',
|
||||
paypalPassword: 'secret',
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
function createDeps(overrides = {}) {
|
||||
const logs = [];
|
||||
const completed = [];
|
||||
const uiCalls = [];
|
||||
|
||||
const deps = {
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completed.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'cpa',
|
||||
getTabId: async () => 0,
|
||||
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
|
||||
isTabAlive: async () => false,
|
||||
normalizeCodex2ApiUrl: (value) => value,
|
||||
normalizeSub2ApiUrl: (value) => value,
|
||||
rememberSourceLastUrl: async () => {},
|
||||
reuseOrCreateTab: async () => 91,
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToContentScriptResilient: async (source, message, options) => {
|
||||
uiCalls.push({ source, message, options });
|
||||
return {};
|
||||
},
|
||||
shouldBypassStep9ForLocalCpa: () => false,
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return { deps, logs, completed, uiCalls };
|
||||
}
|
||||
|
||||
test('platform verify module submits CPA callback via management API first', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
let uiCalled = false;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
assert.equal(url, 'http://localhost:8317/v0/management/oauth-callback');
|
||||
assert.equal(options.method, 'POST');
|
||||
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
|
||||
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
|
||||
assert.deepStrictEqual(JSON.parse(options.body), {
|
||||
provider: 'codex',
|
||||
redirect_url: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
message: 'CPA API 回调提交成功',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps, logs, completed } = createDeps({
|
||||
sendToContentScriptResilient: async () => {
|
||||
uiCalled = true;
|
||||
return {};
|
||||
},
|
||||
});
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
});
|
||||
|
||||
assert.equal(uiCalled, false);
|
||||
assert.deepStrictEqual(completed, [
|
||||
{
|
||||
step: 10,
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'CPA API 回调提交成功',
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(logs, [
|
||||
{ message: '步骤 10:正在通过 CPA 管理接口提交回调地址...', level: 'info' },
|
||||
{ message: '步骤 10:CPA API 回调提交成功', level: 'ok' },
|
||||
]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module prefers cpaManagementOrigin when provided', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
assert.equal(url, 'http://localhost:9999/v0/management/oauth-callback');
|
||||
assert.equal(options.method, 'POST');
|
||||
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
|
||||
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
message: 'CPA API 回调提交成功',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps, completed } = createDeps();
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
cpaManagementOrigin: 'http://localhost:9999',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
});
|
||||
|
||||
assert.equal(completed.length, 1);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module fails fast when CPA API submit fails', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => ({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: async () => ({ message: 'failed to persist oauth callback' }),
|
||||
});
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps, logs, completed, uiCalls } = createDeps();
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
}),
|
||||
/failed to persist oauth callback/
|
||||
);
|
||||
|
||||
assert.equal(uiCalls.length, 0);
|
||||
assert.equal(completed.length, 0);
|
||||
assert.equal(logs[0].message, '步骤 10:正在通过 CPA 管理接口提交回调地址...');
|
||||
assert.match(logs[1].message, /步骤 10:CPA 接口提交失败:failed to persist oauth callback/);
|
||||
assert.equal(logs[1].level, 'error');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module requires management key for CPA API-only flow', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalled = true;
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps, logs, completed, uiCalls } = createDeps();
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: ' ',
|
||||
}),
|
||||
/尚未配置 CPA 管理密钥/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalled, false);
|
||||
assert.equal(uiCalls.length, 0);
|
||||
assert.equal(completed.length, 0);
|
||||
assert.equal(logs.length, 0);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module rejects callback when cpa oauth state mismatches', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalled = true;
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ message: 'should not happen' }),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps } = createDeps();
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=callback-state',
|
||||
cpaOAuthState: 'expected-state',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
}),
|
||||
/CPA 回调 state 与当前授权会话不匹配/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalled, false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -310,6 +310,78 @@ test('step 8 retries in-place when polling fails but auth page still stays on ve
|
||||
assert.equal(events.ensureCalls >= 3, true);
|
||||
});
|
||||
|
||||
test('step 8 keeps resend cooldown timestamp across in-place retries to avoid repeated resend storms', async () => {
|
||||
const events = {
|
||||
resolveCalls: 0,
|
||||
resolveLastResendAts: [],
|
||||
sleepMs: [],
|
||||
};
|
||||
const realDateNow = Date.now;
|
||||
Date.now = () => 230000;
|
||||
|
||||
const executor = api.createStep8Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: 'user@example.com' }),
|
||||
rerunStep7ForStep8Recovery: async () => {},
|
||||
getOAuthFlowRemainingMs: async () => 9000,
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 9000),
|
||||
getMailConfig: () => ({
|
||||
provider: 'qq',
|
||||
label: 'QQ 邮箱',
|
||||
source: 'mail-qq',
|
||||
url: 'https://mail.qq.com',
|
||||
navigateOnReuse: false,
|
||||
}),
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret', loginVerificationRequestedAt: null }),
|
||||
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
isVerificationMailPollingError: (error) => /页面通信异常|did not respond/i.test(String(error?.message || error || '')),
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
resolveVerificationStep: async (_step, _state, _mail, options) => {
|
||||
events.resolveCalls += 1;
|
||||
events.resolveLastResendAts.push(Number(options?.lastResendAt) || 0);
|
||||
if (events.resolveCalls === 1) {
|
||||
await options.onResendRequestedAt(222000);
|
||||
throw new Error('步骤 8:页面通信异常 did not respond in 1s');
|
||||
}
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
sleepWithStop: async (ms) => {
|
||||
events.sleepMs.push(ms);
|
||||
},
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
try {
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
} finally {
|
||||
Date.now = realDateNow;
|
||||
}
|
||||
|
||||
assert.equal(events.resolveCalls, 2);
|
||||
assert.deepStrictEqual(events.resolveLastResendAts, [0, 222000]);
|
||||
assert.equal(events.sleepMs.length >= 1, true);
|
||||
assert.equal(events.sleepMs[0], 3000);
|
||||
});
|
||||
|
||||
test('step 8 completes when polling fails but recovery probe shows oauth consent page', async () => {
|
||||
const events = {
|
||||
ensureCalls: 0,
|
||||
|
||||
@@ -18,6 +18,107 @@ hotmail_helper = load_hotmail_helper()
|
||||
|
||||
|
||||
class HotmailHelperLoggingTest(unittest.TestCase):
|
||||
def test_select_latest_code_can_use_full_body_when_preview_is_truncated(self):
|
||||
css_prefix = (
|
||||
'Your temporary ChatGPT verification code '
|
||||
'@font-face { font-family: "Söhne"; src: url(https://cdn.openai.com/common/fonts/soehne/soehne-buch.woff2) format("woff2"); } '
|
||||
'.ExternalClass { width: 100%; } '
|
||||
'#bodyTable { width: 560px; } '
|
||||
'body { min-width: 100% !important; } '
|
||||
) * 8
|
||||
full_body = (
|
||||
css_prefix
|
||||
+ 'Enter this temporary verification code to continue: 272964 '
|
||||
+ 'Please ignore this email if this was not you.'
|
||||
)
|
||||
message = {
|
||||
"id": "imap-1",
|
||||
"mailbox": "INBOX",
|
||||
"subject": "Your temporary ChatGPT verification code",
|
||||
"from": {
|
||||
"emailAddress": {
|
||||
"address": "otp@tm1.openai.com",
|
||||
"name": "OpenAI",
|
||||
}
|
||||
},
|
||||
"bodyPreview": full_body[:500],
|
||||
"body": {
|
||||
"content": full_body,
|
||||
},
|
||||
"receivedTimestamp": 200,
|
||||
}
|
||||
|
||||
result = hotmail_helper.select_latest_code(
|
||||
[message],
|
||||
['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||
['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
||||
[],
|
||||
0,
|
||||
)
|
||||
|
||||
self.assertEqual(result["code"], "272964")
|
||||
self.assertEqual(result["message"]["id"], "imap-1")
|
||||
|
||||
def test_log_openai_messages_logs_full_body_when_available(self):
|
||||
messages = [{
|
||||
"mailbox": "INBOX",
|
||||
"subject": "Your verification code",
|
||||
"from": {
|
||||
"emailAddress": {
|
||||
"address": "account-security@openai.com",
|
||||
"name": "OpenAI",
|
||||
}
|
||||
},
|
||||
"bodyPreview": "Use 123456 to continue.",
|
||||
"body": {
|
||||
"content": "Hello there\nUse 123456 to continue.",
|
||||
},
|
||||
}]
|
||||
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
hotmail_helper.log_openai_messages(messages, transport="imap")
|
||||
|
||||
rendered = output.getvalue()
|
||||
self.assertIn(
|
||||
"[HotmailHelper] openai mail received transport=imap mailbox=INBOX sender=account-security@openai.com senderName=OpenAI subject=Your verification code",
|
||||
rendered,
|
||||
)
|
||||
self.assertIn(
|
||||
"[HotmailHelper] openai mail full body start transport=imap mailbox=INBOX sender=account-security@openai.com senderName=OpenAI subject=Your verification code",
|
||||
rendered,
|
||||
)
|
||||
self.assertIn("Hello there\nUse 123456 to continue.", rendered)
|
||||
self.assertIn("[HotmailHelper] openai mail full body end", rendered)
|
||||
|
||||
def test_log_openai_messages_falls_back_to_preview_without_full_body(self):
|
||||
messages = [{
|
||||
"mailbox": "Junk",
|
||||
"subject": "Verify your sign in",
|
||||
"from": {
|
||||
"emailAddress": {
|
||||
"address": "noreply@tm.openai.com",
|
||||
"name": "ChatGPT",
|
||||
}
|
||||
},
|
||||
"bodyPreview": "Use 654321 to continue.",
|
||||
}]
|
||||
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
hotmail_helper.log_openai_messages(messages, transport="graph")
|
||||
|
||||
rendered = output.getvalue()
|
||||
self.assertIn(
|
||||
"[HotmailHelper] openai mail received transport=graph mailbox=Junk sender=noreply@tm.openai.com senderName=ChatGPT subject=Verify your sign in",
|
||||
rendered,
|
||||
)
|
||||
self.assertIn(
|
||||
"[HotmailHelper] openai mail preview transport=graph mailbox=Junk sender=noreply@tm.openai.com senderName=ChatGPT subject=Verify your sign in preview=Use 654321 to continue.",
|
||||
rendered,
|
||||
)
|
||||
self.assertNotIn("openai mail full body start", rendered)
|
||||
|
||||
def test_refresh_access_token_logs_invalid_grant_and_direct_connection_refused_separately(self):
|
||||
failures = [
|
||||
{
|
||||
@@ -38,7 +139,8 @@ class HotmailHelperLoggingTest(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
|
||||
with mock.patch.object(hotmail_helper, "try_refresh_access_token", side_effect=failures):
|
||||
with mock.patch.object(hotmail_helper, "try_refresh_access_token", side_effect=failures), \
|
||||
mock.patch.object(hotmail_helper, "get_proxy_debug_context", return_value="direct"):
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
with self.assertRaises(RuntimeError):
|
||||
|
||||
@@ -107,6 +107,32 @@ test('PayPal approve keeps original combined email and password login path', asy
|
||||
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
|
||||
});
|
||||
|
||||
test('PayPal approve prefers the selected paypal pool account over legacy fields', async () => {
|
||||
const { executor, events } = createExecutor({
|
||||
pageStates: [
|
||||
{ needsLogin: true, hasEmailInput: true, hasPasswordInput: true, loginPhase: 'login_combined' },
|
||||
{ needsLogin: false, approveReady: true },
|
||||
{ needsLogin: false, approveReady: true },
|
||||
],
|
||||
submitResults: [
|
||||
{ submitted: true, phase: 'password_submitted', awaiting: 'redirect_or_approval' },
|
||||
],
|
||||
});
|
||||
|
||||
await executor.executePayPalApprove({
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
currentPayPalAccountId: 'pp-1',
|
||||
paypalAccounts: [
|
||||
{ id: 'pp-1', email: 'pool@example.com', password: 'pool-secret' },
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.submittedPayloads, [
|
||||
{ email: 'pool@example.com', password: 'pool-secret' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('PayPal approve discovers an already open unregistered PayPal tab', async () => {
|
||||
const { executor, events } = createExecutor({
|
||||
pageStates: [
|
||||
|
||||
@@ -145,6 +145,57 @@ return {
|
||||
`)(document, window);
|
||||
}
|
||||
|
||||
function createSubmitApi(overrides = {}) {
|
||||
const bindings = {
|
||||
waitForDocumentComplete: async () => {},
|
||||
normalizeText: (text = '') => String(text || '').replace(/\s+/g, ' ').trim(),
|
||||
findPasswordInput: () => null,
|
||||
findEmailInput: () => null,
|
||||
findEmailNextButton: () => null,
|
||||
isEnabledControl: () => true,
|
||||
findPasswordLoginButton: () => null,
|
||||
fillInput: () => {},
|
||||
simulateClick: () => {},
|
||||
waitUntil: async (predicate) => predicate(),
|
||||
findLoginNextButton: () => null,
|
||||
sleep: async () => {},
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return new Function(
|
||||
'waitForDocumentComplete',
|
||||
'normalizeText',
|
||||
'findPasswordInput',
|
||||
'findEmailInput',
|
||||
'findEmailNextButton',
|
||||
'isEnabledControl',
|
||||
'findPasswordLoginButton',
|
||||
'fillInput',
|
||||
'simulateClick',
|
||||
'waitUntil',
|
||||
'findLoginNextButton',
|
||||
'sleep',
|
||||
`
|
||||
${extractFunction('refillPayPalEmailInput')}
|
||||
${extractFunction('submitPayPalLogin')}
|
||||
return { refillPayPalEmailInput, submitPayPalLogin };
|
||||
`
|
||||
)(
|
||||
bindings.waitForDocumentComplete,
|
||||
bindings.normalizeText,
|
||||
bindings.findPasswordInput,
|
||||
bindings.findEmailInput,
|
||||
bindings.findEmailNextButton,
|
||||
bindings.isEnabledControl,
|
||||
bindings.findPasswordLoginButton,
|
||||
bindings.fillInput,
|
||||
bindings.simulateClick,
|
||||
bindings.waitUntil,
|
||||
bindings.findLoginNextButton,
|
||||
bindings.sleep
|
||||
);
|
||||
}
|
||||
|
||||
test('PayPal email page ignores hidden pre-rendered password input', () => {
|
||||
const hiddenPanel = createElement({ attrs: { 'aria-hidden': 'true' } });
|
||||
const emailInput = createElement({
|
||||
@@ -203,3 +254,57 @@ test('PayPal combined login page still sees visible password input', () => {
|
||||
assert.equal(api.findPasswordLoginButton(), loginButton);
|
||||
assert.equal(api.getPayPalLoginPhase(emailInput, passwordInput), 'login_combined');
|
||||
});
|
||||
|
||||
test('PayPal email submit refills a prefilled email before clicking next', async () => {
|
||||
const emailInput = createElement({
|
||||
tag: 'input',
|
||||
type: 'text',
|
||||
id: 'login_email',
|
||||
name: 'login_email',
|
||||
value: 'user@example.com',
|
||||
placeholder: 'Email',
|
||||
});
|
||||
const nextButton = createElement({
|
||||
tag: 'button',
|
||||
id: 'btnNext',
|
||||
text: 'Next',
|
||||
});
|
||||
const fillValues = [];
|
||||
const clicked = [];
|
||||
let focusCount = 0;
|
||||
let blurCount = 0;
|
||||
|
||||
emailInput.focus = () => {
|
||||
focusCount += 1;
|
||||
};
|
||||
emailInput.blur = () => {
|
||||
blurCount += 1;
|
||||
};
|
||||
|
||||
const api = createSubmitApi({
|
||||
findEmailInput: () => emailInput,
|
||||
findEmailNextButton: () => nextButton,
|
||||
fillInput: (element, value) => {
|
||||
fillValues.push(value);
|
||||
element.value = value;
|
||||
},
|
||||
simulateClick: (element) => {
|
||||
clicked.push(element);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await api.submitPayPalLogin({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
});
|
||||
|
||||
assert.deepEqual(fillValues, ['', 'user@example.com']);
|
||||
assert.equal(focusCount, 1);
|
||||
assert.equal(blurCount, 1);
|
||||
assert.deepEqual(clicked, [nextButton]);
|
||||
assert.deepEqual(result, {
|
||||
submitted: false,
|
||||
phase: 'email_submitted',
|
||||
awaiting: 'password_page',
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -222,6 +222,7 @@ const inputAutoDelayMinutes = { value: '30' };
|
||||
const inputAutoStepDelaySeconds = { value: '10' };
|
||||
const inputVerificationResendCount = { value: '6' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
|
||||
function getCloudflareDomainsFromState() { return { domains: ['example.com'], activeDomain: 'example.com' }; }
|
||||
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
|
||||
|
||||
@@ -130,6 +130,64 @@ const inputAutoDelayMinutes = { value: '30' };
|
||||
const inputAutoStepDelaySeconds = { value: '' };
|
||||
const inputVerificationResendCount = { value: '4' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const inputHeroSmsApiKey = { value: '' };
|
||||
const inputHeroSmsReuseEnabled = { checked: true };
|
||||
const selectHeroSmsAcquirePriority = { value: 'country' };
|
||||
const inputHeroSmsMaxPrice = { value: '' };
|
||||
const inputPhoneReplacementLimit = { value: '3' };
|
||||
const inputPhoneCodeWaitSeconds = { value: '60' };
|
||||
const inputPhoneCodeTimeoutWindows = { value: '2' };
|
||||
const inputPhoneCodePollIntervalSeconds = { value: '5' };
|
||||
const inputPhoneCodePollMaxRounds = { value: '4' };
|
||||
const selectHeroSmsCountry = { value: '52', selectedIndex: 0, options: [{ value: '52', textContent: 'Thailand' }] };
|
||||
function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); }
|
||||
function normalizeHeroSmsReuseEnabledValue(value) { return value === undefined || value === null ? true : Boolean(value); }
|
||||
function normalizeHeroSmsAcquirePriority(value = '') { return String(value || '').trim().toLowerCase() === 'price' ? 'price' : 'country'; }
|
||||
function normalizeHeroSmsCountryId(value) { return Math.max(1, Math.floor(Number(value) || 52)); }
|
||||
function normalizeHeroSmsCountryLabel(value = '') { return String(value || '').trim() || 'Thailand'; }
|
||||
function normalizePhoneVerificationReplacementLimit(value, fallback = 3) {
|
||||
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
function normalizePhoneCodeWaitSecondsValue(value, fallback = 60) {
|
||||
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
function normalizePhoneCodeTimeoutWindowsValue(value, fallback = 2) {
|
||||
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
function normalizePhoneCodePollIntervalSecondsValue(value, fallback = 5) {
|
||||
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
function normalizePhoneCodePollMaxRoundsValue(value, fallback = 12) {
|
||||
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
function getSelectedHeroSmsCountryOption() { return { id: 52, label: 'Thailand' }; }
|
||||
function syncHeroSmsFallbackSelectionOrderFromSelect() { return [{ id: 52, label: 'Thailand' }]; }
|
||||
function getPayPalAccounts() { return []; }
|
||||
function getCurrentPayPalAccount() { return null; }
|
||||
function getCloudflareDomainsFromState() { return { domains: [], activeDomain: '' }; }
|
||||
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
|
||||
function getCloudflareTempEmailDomainsFromState() { return { domains: [], activeDomain: '' }; }
|
||||
@@ -321,9 +379,14 @@ const inputVerificationResendCount = { value: '' };
|
||||
const inputPhoneVerificationEnabled = { checked: false };
|
||||
const DEFAULT_PHONE_VERIFICATION_ENABLED = false;
|
||||
const inputHeroSmsApiKey = { value: '' };
|
||||
const inputHeroSmsMaxPrice = { value: '' };
|
||||
const inputPhoneReplacementLimit = { value: '' };
|
||||
const selectHeroSmsCountry = { value: '52', options: [{ value: '52' }] };
|
||||
const inputRunCount = { value: '' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
function syncLatestState(state) { latestState = { ...latestState, ...state }; }
|
||||
function syncAutoRunState() {}
|
||||
function syncPasswordField() {}
|
||||
@@ -347,6 +410,12 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
|
||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||
function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); }
|
||||
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
||||
function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); }
|
||||
function normalizePhoneVerificationReplacementLimit(value, fallback = 3) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.min(PHONE_REPLACEMENT_LIMIT_MAX, Math.floor(numeric)));
|
||||
}
|
||||
function normalizeHeroSmsCountryId() { return 52; }
|
||||
function getSelectedHeroSmsCountryOption() { return { label: 'Thailand' }; }
|
||||
function updateHeroSmsPlatformDisplay() {}
|
||||
|
||||
@@ -201,6 +201,7 @@ const inputAutoDelayMinutes = { value: '30' };
|
||||
const inputAutoStepDelaySeconds = { value: '' };
|
||||
const inputVerificationResendCount = { value: '4' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
function getCloudflareDomainsFromState() {
|
||||
return { domains: [], activeDomain: '' };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('sidepanel password inputs expose visibility toggles', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const passwordInputIds = Array.from(
|
||||
html.matchAll(/<input\b[^>]*type="password"[^>]*id="([^"]+)"/g),
|
||||
(match) => match[1]
|
||||
);
|
||||
const legacyToggleIds = new Map([
|
||||
['input-vps-url', 'btn-toggle-vps-url'],
|
||||
['input-vps-password', 'btn-toggle-vps-password'],
|
||||
['input-ip-proxy-username', 'btn-toggle-ip-proxy-username'],
|
||||
['input-ip-proxy-password', 'btn-toggle-ip-proxy-password'],
|
||||
['input-ip-proxy-api-url', 'btn-toggle-ip-proxy-api-url'],
|
||||
['input-password', 'btn-toggle-password'],
|
||||
]);
|
||||
|
||||
assert.ok(passwordInputIds.length > 0);
|
||||
for (const inputId of passwordInputIds) {
|
||||
const hasDataToggle = html.includes(`data-password-toggle="${inputId}"`);
|
||||
const legacyToggleId = legacyToggleIds.get(inputId);
|
||||
const hasLegacyToggle = legacyToggleId ? html.includes(`id="${legacyToggleId}"`) : false;
|
||||
assert.equal(
|
||||
hasDataToggle || hasLegacyToggle,
|
||||
true,
|
||||
`${inputId} should have a visibility toggle button`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('shared form dialog adds visibility toggles for password fields', () => {
|
||||
const source = fs.readFileSync('sidepanel/form-dialog.js', 'utf8');
|
||||
|
||||
assert.match(source, /field\.type === 'password'[\s\S]*data-input-with-icon/);
|
||||
assert.match(source, /syncPasswordToggleButton\(toggleButton,\s*input,\s*labels\)/);
|
||||
assert.match(source, /input\.type = input\.type === 'password' \? 'text' : 'password'/);
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('sidepanel loads reusable form dialog and paypal manager before sidepanel bootstrap', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const formDialogIndex = html.indexOf('<script src="form-dialog.js"></script>');
|
||||
const managerIndex = html.indexOf('<script src="paypal-manager.js"></script>');
|
||||
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||
|
||||
assert.notEqual(formDialogIndex, -1);
|
||||
assert.notEqual(managerIndex, -1);
|
||||
assert.notEqual(sidepanelIndex, -1);
|
||||
assert.ok(formDialogIndex < managerIndex);
|
||||
assert.ok(managerIndex < sidepanelIndex);
|
||||
});
|
||||
|
||||
test('sidepanel html contains paypal select and add button controls', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
|
||||
assert.match(html, /id="row-paypal-account"/);
|
||||
assert.match(html, /id="select-paypal-account"/);
|
||||
assert.match(html, /id="btn-add-paypal-account"/);
|
||||
assert.match(html, /id="shared-form-modal"/);
|
||||
});
|
||||
|
||||
test('paypal manager saves a paypal account and selects it immediately', async () => {
|
||||
const source = fs.readFileSync('sidepanel/paypal-manager.js', 'utf8');
|
||||
const windowObject = {};
|
||||
const api = new Function('window', `${source}; return window.SidepanelPayPalManager;`)(windowObject);
|
||||
|
||||
let latestState = {
|
||||
paypalAccounts: [],
|
||||
currentPayPalAccountId: null,
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
};
|
||||
const events = [];
|
||||
const clickHandlers = {};
|
||||
const changeHandlers = {};
|
||||
const selectNode = {
|
||||
innerHTML: '',
|
||||
value: '',
|
||||
disabled: false,
|
||||
addEventListener(type, handler) {
|
||||
changeHandlers[type] = handler;
|
||||
},
|
||||
};
|
||||
const addButton = {
|
||||
disabled: false,
|
||||
addEventListener(type, handler) {
|
||||
clickHandlers[type] = handler;
|
||||
},
|
||||
};
|
||||
|
||||
const manager = api.createPayPalManager({
|
||||
state: {
|
||||
getLatestState: () => latestState,
|
||||
syncLatestState(updates) {
|
||||
latestState = { ...latestState, ...updates };
|
||||
},
|
||||
},
|
||||
dom: {
|
||||
btnAddPayPalAccount: addButton,
|
||||
selectPayPalAccount: selectNode,
|
||||
},
|
||||
helpers: {
|
||||
escapeHtml: (value) => String(value || ''),
|
||||
getPayPalAccounts: (state) => Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : [],
|
||||
openFormDialog: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
showToast(message, tone) {
|
||||
events.push({ type: 'toast', message, tone });
|
||||
},
|
||||
},
|
||||
runtime: {
|
||||
sendMessage: async (message) => {
|
||||
events.push({ type: 'message', message });
|
||||
if (message.type === 'UPSERT_PAYPAL_ACCOUNT') {
|
||||
return {
|
||||
ok: true,
|
||||
account: {
|
||||
id: 'pp-1',
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (message.type === 'SELECT_PAYPAL_ACCOUNT') {
|
||||
return {
|
||||
ok: true,
|
||||
account: {
|
||||
id: 'pp-1',
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected message ${message.type}`);
|
||||
},
|
||||
},
|
||||
paypalUtils: {
|
||||
upsertPayPalAccountInList(accounts, nextAccount) {
|
||||
const list = Array.isArray(accounts) ? accounts.slice() : [];
|
||||
const existingIndex = list.findIndex((account) => account.id === nextAccount.id);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = nextAccount;
|
||||
return list;
|
||||
}
|
||||
list.push(nextAccount);
|
||||
return list;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
manager.bindPayPalEvents();
|
||||
manager.renderPayPalAccounts();
|
||||
|
||||
assert.match(selectNode.innerHTML, /请先添加 PayPal 账号/);
|
||||
clickHandlers.click();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepStrictEqual(
|
||||
events.filter((event) => event.type === 'message').map((event) => event.message.type),
|
||||
['UPSERT_PAYPAL_ACCOUNT', 'SELECT_PAYPAL_ACCOUNT']
|
||||
);
|
||||
assert.equal(latestState.currentPayPalAccountId, 'pp-1');
|
||||
assert.equal(latestState.paypalEmail, 'user@example.com');
|
||||
assert.equal(latestState.paypalPassword, 'secret');
|
||||
assert.equal(selectNode.value, 'pp-1');
|
||||
assert.equal(selectNode.disabled, false);
|
||||
assert.match(events.at(-1)?.message || '', /已保存 PayPal 账号/);
|
||||
});
|
||||
@@ -56,41 +56,125 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
|
||||
assert.match(html, /id="row-phone-verification-enabled"/);
|
||||
assert.match(html, /id="btn-toggle-phone-verification-section"/);
|
||||
assert.match(html, /id="row-phone-verification-fold"/);
|
||||
assert.match(html, /id="input-phone-verification-enabled"/);
|
||||
assert.match(html, /id="row-hero-sms-platform"/);
|
||||
assert.match(html, /id="row-hero-sms-country"/);
|
||||
assert.match(html, /id="row-hero-sms-country-fallback"/);
|
||||
assert.match(html, /id="row-hero-sms-acquire-priority"/);
|
||||
assert.match(html, /id="select-hero-sms-acquire-priority"/);
|
||||
assert.match(html, /id="select-hero-sms-country"[^>]*multiple/);
|
||||
assert.doesNotMatch(html, /id="select-hero-sms-country-fallback"/);
|
||||
assert.match(html, /id="row-hero-sms-api-key"/);
|
||||
assert.match(html, /id="row-hero-sms-max-price"/);
|
||||
assert.match(html, /id="row-hero-sms-current-number"/);
|
||||
assert.match(html, /id="row-hero-sms-price-tiers"/);
|
||||
assert.match(html, /id="row-hero-sms-current-code"/);
|
||||
assert.match(html, /id="row-phone-replacement-limit"/);
|
||||
assert.match(html, /id="row-phone-verification-resend-count"/);
|
||||
assert.match(html, /id="row-phone-code-wait-seconds"/);
|
||||
assert.match(html, /id="row-phone-code-timeout-windows"/);
|
||||
assert.match(html, /id="row-phone-code-poll-interval-seconds"/);
|
||||
assert.match(html, /id="row-phone-code-poll-max-rounds"/);
|
||||
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
|
||||
});
|
||||
|
||||
test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch', () => {
|
||||
const api = new Function(`
|
||||
const phoneVerificationSectionExpanded = true;
|
||||
const inputPhoneVerificationEnabled = { checked: false };
|
||||
const rowPhoneVerificationEnabled = { style: { display: 'none' } };
|
||||
const rowPhoneVerificationFold = { style: { display: 'none' } };
|
||||
const btnTogglePhoneVerificationSection = {
|
||||
disabled: false,
|
||||
textContent: '',
|
||||
title: '',
|
||||
setAttribute: () => {},
|
||||
};
|
||||
const rowHeroSmsPlatform = { style: { display: 'none' } };
|
||||
const rowHeroSmsCountry = { style: { display: 'none' } };
|
||||
const rowHeroSmsCountryFallback = { style: { display: 'none' } };
|
||||
const rowHeroSmsAcquirePriority = { style: { display: 'none' } };
|
||||
const rowHeroSmsApiKey = { style: { display: 'none' } };
|
||||
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
|
||||
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
|
||||
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
|
||||
const rowHeroSmsCurrentCode = { style: { display: 'none' } };
|
||||
const rowPhoneVerificationResendCount = { style: { display: 'none' } };
|
||||
const rowPhoneReplacementLimit = { style: { display: 'none' } };
|
||||
const rowPhoneCodeWaitSeconds = { style: { display: 'none' } };
|
||||
const rowPhoneCodeTimeoutWindows = { style: { display: 'none' } };
|
||||
const rowPhoneCodePollIntervalSeconds = { style: { display: 'none' } };
|
||||
const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
|
||||
|
||||
${extractFunction('updatePhoneVerificationSettingsUI')}
|
||||
|
||||
return {
|
||||
rowPhoneVerificationEnabled,
|
||||
rowPhoneVerificationFold,
|
||||
btnTogglePhoneVerificationSection,
|
||||
inputPhoneVerificationEnabled,
|
||||
rowHeroSmsPlatform,
|
||||
rowHeroSmsCountry,
|
||||
rowHeroSmsCountryFallback,
|
||||
rowHeroSmsAcquirePriority,
|
||||
rowHeroSmsApiKey,
|
||||
rowHeroSmsMaxPrice,
|
||||
rowHeroSmsCurrentNumber,
|
||||
rowHeroSmsPriceTiers,
|
||||
rowHeroSmsCurrentCode,
|
||||
rowPhoneVerificationResendCount,
|
||||
rowPhoneReplacementLimit,
|
||||
rowPhoneCodeWaitSeconds,
|
||||
rowPhoneCodeTimeoutWindows,
|
||||
rowPhoneCodePollIntervalSeconds,
|
||||
rowPhoneCodePollMaxRounds,
|
||||
updatePhoneVerificationSettingsUI,
|
||||
};
|
||||
`)();
|
||||
|
||||
api.updatePhoneVerificationSettingsUI();
|
||||
assert.equal(api.rowPhoneVerificationEnabled.style.display, '');
|
||||
assert.equal(api.rowPhoneVerificationFold.style.display, 'none');
|
||||
assert.equal(api.btnTogglePhoneVerificationSection.disabled, true);
|
||||
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '展开设置');
|
||||
assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCountryFallback.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsAcquirePriority.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCurrentNumber.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCurrentCode.style.display, 'none');
|
||||
assert.equal(api.rowPhoneVerificationResendCount.style.display, 'none');
|
||||
assert.equal(api.rowPhoneReplacementLimit.style.display, 'none');
|
||||
assert.equal(api.rowPhoneCodeWaitSeconds.style.display, 'none');
|
||||
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, 'none');
|
||||
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, 'none');
|
||||
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, 'none');
|
||||
|
||||
api.inputPhoneVerificationEnabled.checked = true;
|
||||
api.updatePhoneVerificationSettingsUI();
|
||||
assert.equal(api.rowPhoneVerificationFold.style.display, '');
|
||||
assert.equal(api.btnTogglePhoneVerificationSection.disabled, false);
|
||||
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '收起设置');
|
||||
assert.equal(api.rowHeroSmsPlatform.style.display, '');
|
||||
assert.equal(api.rowHeroSmsCountry.style.display, '');
|
||||
assert.equal(api.rowHeroSmsCountryFallback.style.display, '');
|
||||
assert.equal(api.rowHeroSmsAcquirePriority.style.display, '');
|
||||
assert.equal(api.rowHeroSmsApiKey.style.display, '');
|
||||
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
|
||||
assert.equal(api.rowHeroSmsCurrentNumber.style.display, '');
|
||||
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCurrentCode.style.display, '');
|
||||
assert.equal(api.rowPhoneVerificationResendCount.style.display, '');
|
||||
assert.equal(api.rowPhoneReplacementLimit.style.display, '');
|
||||
assert.equal(api.rowPhoneCodeWaitSeconds.style.display, '');
|
||||
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, '');
|
||||
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, '');
|
||||
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, '');
|
||||
});
|
||||
|
||||
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
|
||||
@@ -141,8 +225,35 @@ const inputAutoStepDelaySeconds = { value: '' };
|
||||
const inputPhoneVerificationEnabled = { checked: true };
|
||||
const inputVerificationResendCount = { value: '4' };
|
||||
const inputHeroSmsApiKey = { value: 'demo-key' };
|
||||
const inputHeroSmsReuseEnabled = { checked: true };
|
||||
const selectHeroSmsAcquirePriority = { value: 'price' };
|
||||
const inputHeroSmsMaxPrice = { value: '0.12' };
|
||||
const inputPhoneReplacementLimit = { value: '5' };
|
||||
const inputPhoneCodeWaitSeconds = { value: '75' };
|
||||
const inputPhoneCodeTimeoutWindows = { value: '3' };
|
||||
const inputPhoneCodePollIntervalSeconds = { value: '6' };
|
||||
const inputPhoneCodePollMaxRounds = { value: '18' };
|
||||
const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const selectHeroSmsCountry = {
|
||||
@@ -167,9 +278,20 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
|
||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
|
||||
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||
${extractFunction('normalizePhoneVerificationReplacementLimit')}
|
||||
${extractFunction('normalizePhoneCodeWaitSecondsValue')}
|
||||
${extractFunction('normalizePhoneCodeTimeoutWindowsValue')}
|
||||
${extractFunction('normalizePhoneCodePollIntervalSecondsValue')}
|
||||
${extractFunction('normalizePhoneCodePollMaxRoundsValue')}
|
||||
${extractFunction('normalizeHeroSmsReuseEnabledValue')}
|
||||
${extractFunction('normalizeHeroSmsAcquirePriority')}
|
||||
${extractFunction('normalizeHeroSmsCountryId')}
|
||||
${extractFunction('normalizeHeroSmsCountryLabel')}
|
||||
${extractFunction('getSelectedHeroSmsCountryOption')}
|
||||
function syncHeroSmsFallbackSelectionOrderFromSelect() {
|
||||
return [{ id: 52, label: 'Thailand' }, { id: 16, label: 'United Kingdom' }];
|
||||
}
|
||||
${extractFunction('collectSettingsPayload')}
|
||||
return { collectSettingsPayload };
|
||||
`)(normalizeIcloudTargetMailboxType, normalizeIcloudForwardMailProvider);
|
||||
@@ -180,6 +302,15 @@ return { collectSettingsPayload };
|
||||
assert.equal(payload.accountRunHistoryTextEnabled, true);
|
||||
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
|
||||
assert.equal(payload.heroSmsApiKey, 'demo-key');
|
||||
assert.equal(payload.heroSmsReuseEnabled, true);
|
||||
assert.equal(payload.heroSmsAcquirePriority, 'price');
|
||||
assert.equal(payload.heroSmsMaxPrice, '0.12');
|
||||
assert.equal(payload.phoneVerificationReplacementLimit, 5);
|
||||
assert.equal(payload.phoneCodeWaitSeconds, 75);
|
||||
assert.equal(payload.phoneCodeTimeoutWindows, 3);
|
||||
assert.equal(payload.phoneCodePollIntervalSeconds, 6);
|
||||
assert.equal(payload.phoneCodePollMaxRounds, 18);
|
||||
assert.equal(payload.heroSmsCountryId, 52);
|
||||
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
|
||||
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
|
||||
});
|
||||
|
||||
@@ -68,6 +68,7 @@ test('sidepanel html loads shared step definitions before sidepanel bootstrap',
|
||||
test('sidepanel html exposes Plus mode and PayPal settings', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
assert.match(html, /id="input-plus-mode-enabled"/);
|
||||
assert.match(html, /id="input-paypal-email"/);
|
||||
assert.match(html, /id="input-paypal-password"/);
|
||||
assert.match(html, /id="select-paypal-account"/);
|
||||
assert.match(html, /id="btn-add-paypal-account"/);
|
||||
assert.match(html, /id="shared-form-modal"/);
|
||||
});
|
||||
|
||||
@@ -114,6 +114,7 @@ function broadcastDataUpdate() {}
|
||||
async function addLog(message) {
|
||||
logMessages.push(message);
|
||||
}
|
||||
async function finalizePhoneActivationAfterSuccessfulFlow() {}
|
||||
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
|
||||
function matchesSourceUrlFamily() {
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
|
||||
const step9ModuleSource = fs.readFileSync('background/steps/confirm-oauth.js', 'utf8');
|
||||
|
||||
const api = new Function('step9ModuleSource', `
|
||||
const self = {};
|
||||
let webNavListener = null;
|
||||
let webNavCommittedListener = null;
|
||||
let step8TabUpdatedListener = null;
|
||||
let step8PendingReject = null;
|
||||
let cleanupCalls = 0;
|
||||
let timeoutCalls = 0;
|
||||
let recoveryCalls = 0;
|
||||
let completePayload = null;
|
||||
const logs = [];
|
||||
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
|
||||
|
||||
const chrome = {
|
||||
webNavigation: {
|
||||
onBeforeNavigate: {
|
||||
addListener(listener) {
|
||||
webNavListener = listener;
|
||||
setTimeout(() => {
|
||||
if (typeof webNavListener === 'function') {
|
||||
webNavListener({ tabId: 123, url: callbackUrl });
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
removeListener() {},
|
||||
},
|
||||
onCommitted: {
|
||||
addListener(listener) {
|
||||
webNavCommittedListener = listener;
|
||||
},
|
||||
removeListener() {},
|
||||
},
|
||||
},
|
||||
tabs: {
|
||||
onUpdated: {
|
||||
addListener(listener) {
|
||||
step8TabUpdatedListener = listener;
|
||||
},
|
||||
removeListener() {},
|
||||
},
|
||||
async update() {},
|
||||
},
|
||||
};
|
||||
|
||||
function cleanupStep8NavigationListeners() {
|
||||
cleanupCalls += 1;
|
||||
webNavListener = null;
|
||||
webNavCommittedListener = null;
|
||||
step8TabUpdatedListener = null;
|
||||
}
|
||||
|
||||
async function addLog(message) {
|
||||
logs.push(message);
|
||||
}
|
||||
|
||||
function throwIfStep8SettledOrStopped() {}
|
||||
|
||||
async function getTabId() {
|
||||
return 123;
|
||||
}
|
||||
|
||||
async function isTabAlive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function ensureStep8SignupPageReady() {}
|
||||
|
||||
async function waitForStep8Ready() {
|
||||
return {
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
};
|
||||
}
|
||||
|
||||
async function triggerStep8ContentStrategy() {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function waitForStep8ClickEffect() {
|
||||
return {
|
||||
progressed: true,
|
||||
reason: 'url_changed',
|
||||
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
};
|
||||
}
|
||||
|
||||
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
|
||||
if (options.actionLabel === 'OAuth localhost 回调') {
|
||||
timeoutCalls += 1;
|
||||
if (timeoutCalls === 1) {
|
||||
throw new Error('步骤 9:从拿到 OAuth 登录地址开始,5 分钟内未完成OAuth localhost 回调,结束当前链路,准备从步骤 7 重新开始。');
|
||||
}
|
||||
}
|
||||
return defaultTimeoutMs;
|
||||
}
|
||||
|
||||
async function recoverOAuthLocalhostTimeout(details = {}) {
|
||||
recoveryCalls += 1;
|
||||
return {
|
||||
...(details.state || {}),
|
||||
oauthUrl: 'https://auth.openai.com/recovered-oauth',
|
||||
};
|
||||
}
|
||||
|
||||
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
|
||||
if (
|
||||
Number(signupTabId) === Number(details?.tabId)
|
||||
&& String(details?.url || '').includes('http://localhost:1455/auth/callback')
|
||||
) {
|
||||
return details.url;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getStep8CallbackUrlFromTabUpdate() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getStep8EffectLabel() {
|
||||
return 'URL 已变化';
|
||||
}
|
||||
|
||||
async function prepareStep8DebuggerClick() {
|
||||
return { rect: { centerX: 10, centerY: 10 } };
|
||||
}
|
||||
|
||||
async function clickWithDebugger() {}
|
||||
async function reloadStep8ConsentPage() {}
|
||||
async function reuseOrCreateTab() { return 123; }
|
||||
async function sleepWithStop() {}
|
||||
|
||||
function setWebNavListener(listener) { webNavListener = listener; }
|
||||
function getWebNavListener() { return webNavListener; }
|
||||
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
|
||||
function getWebNavCommittedListener() { return webNavCommittedListener; }
|
||||
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
|
||||
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
|
||||
function setStep8PendingReject(handler) { step8PendingReject = handler; }
|
||||
|
||||
async function completeStepFromBackground(step, payload) {
|
||||
completePayload = { step, payload };
|
||||
}
|
||||
|
||||
const STEP8_CLICK_RETRY_DELAY_MS = 200;
|
||||
const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
|
||||
const STEP8_MAX_ROUNDS = 2;
|
||||
const STEP8_STRATEGIES = [
|
||||
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
|
||||
{ mode: 'debugger', label: 'debugger click' },
|
||||
];
|
||||
|
||||
${step9ModuleSource}
|
||||
|
||||
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
|
||||
addLog,
|
||||
chrome,
|
||||
cleanupStep8NavigationListeners,
|
||||
clickWithDebugger,
|
||||
completeStepFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getStep8CallbackUrlFromNavigation,
|
||||
getStep8CallbackUrlFromTabUpdate,
|
||||
getStep8EffectLabel,
|
||||
getTabId,
|
||||
getWebNavCommittedListener,
|
||||
getWebNavListener,
|
||||
getStep8TabUpdatedListener,
|
||||
isTabAlive,
|
||||
prepareStep8DebuggerClick,
|
||||
recoverOAuthLocalhostTimeout,
|
||||
reloadStep8ConsentPage,
|
||||
reuseOrCreateTab,
|
||||
setStep8PendingReject,
|
||||
setStep8TabUpdatedListener,
|
||||
setWebNavCommittedListener,
|
||||
setWebNavListener,
|
||||
sleepWithStop,
|
||||
STEP8_CLICK_RETRY_DELAY_MS,
|
||||
STEP8_MAX_ROUNDS,
|
||||
STEP8_READY_WAIT_TIMEOUT_MS,
|
||||
STEP8_STRATEGIES,
|
||||
throwIfStep8SettledOrStopped,
|
||||
triggerStep8ContentStrategy,
|
||||
waitForStep8ClickEffect,
|
||||
waitForStep8Ready,
|
||||
});
|
||||
|
||||
return {
|
||||
executeStep9: executor.executeStep9,
|
||||
snapshot() {
|
||||
return {
|
||||
cleanupCalls,
|
||||
timeoutCalls,
|
||||
recoveryCalls,
|
||||
completePayload,
|
||||
hasPendingReject: Boolean(step8PendingReject),
|
||||
logs,
|
||||
};
|
||||
},
|
||||
};
|
||||
`)(step9ModuleSource);
|
||||
|
||||
(async () => {
|
||||
await api.executeStep9({
|
||||
oauthUrl: 'https://auth.openai.com/original-oauth',
|
||||
visibleStep: 9,
|
||||
});
|
||||
|
||||
const snapshot = api.snapshot();
|
||||
assert.strictEqual(snapshot.timeoutCalls, 2, 'step9 should retry timeout budget check after recovery');
|
||||
assert.strictEqual(snapshot.recoveryCalls, 1, 'step9 should call timeout recovery hook exactly once');
|
||||
assert.strictEqual(snapshot.cleanupCalls >= 1, true, 'step9 should cleanup navigation listeners');
|
||||
assert.strictEqual(snapshot.hasPendingReject, false, 'step9 should clear pending reject after completion');
|
||||
assert.deepStrictEqual(snapshot.completePayload, {
|
||||
step: 9,
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('step9 timeout recovery tests passed');
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1099,6 +1099,77 @@ test('verification flow clicks resend before waiting for the next LuckMail /code
|
||||
]);
|
||||
});
|
||||
|
||||
test('verification flow notifies onResendRequestedAt when resend is triggered', async () => {
|
||||
const resendRequestedAtCalls = [];
|
||||
const stateUpdates = [];
|
||||
let pollCalls = 0;
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async (_source, message) => {
|
||||
if (message.type === 'RESEND_VERIFICATION_CODE') {
|
||||
return { resent: true };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
sendToMailContentScriptResilient: async (_mail, message) => {
|
||||
if (message.type !== 'POLL_EMAIL') {
|
||||
return {};
|
||||
}
|
||||
pollCalls += 1;
|
||||
return pollCalls === 1
|
||||
? {}
|
||||
: { code: '654321', emailTimestamp: 123 };
|
||||
},
|
||||
setState: async (payload) => {
|
||||
stateUpdates.push(payload);
|
||||
},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
await helpers.resolveVerificationStep(
|
||||
8,
|
||||
{
|
||||
email: 'user@example.com',
|
||||
lastLoginCode: null,
|
||||
},
|
||||
{ provider: 'qq', label: 'QQ 邮箱' },
|
||||
{
|
||||
maxResendRequests: 1,
|
||||
resendIntervalMs: 25000,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
resendRequestedAtCalls.push(Number(requestedAt) || 0);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(resendRequestedAtCalls.length >= 1, true);
|
||||
assert.equal(resendRequestedAtCalls[0] > 0, true);
|
||||
assert.equal(
|
||||
stateUpdates.some((payload) => Number(payload?.loginVerificationRequestedAt) > 0),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('verification flow uses resilient signup-page transport when submitting verification code', async () => {
|
||||
const resilientCalls = [];
|
||||
|
||||
|
||||
+5
-15
@@ -23,7 +23,6 @@
|
||||
- 轮询登录验证码
|
||||
- 自动确认 OAuth 同意页
|
||||
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
|
||||
- 可选开启 Plus 模式:先完成 Plus Checkout、账单地址、PayPal 登录授权与订阅回跳确认,再复用 OAuth 后半段链路
|
||||
|
||||
## 2. 核心运行参与者
|
||||
|
||||
@@ -45,7 +44,7 @@
|
||||
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
||||
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro` 或 `v` 版本误显示为比 `Ultra` 更新
|
||||
- 展示一个单独的“接码”开关;开启后才展示 HeroSMS 的接码国家与 API Key 设置,用于 OAuth 登录链路命中手机号验证页时直接续跑手机验证
|
||||
- 展示 `Plus 模式` 开关与 PayPal 账号/密码配置;开启后步骤列表切换为 Plus 模式 13 步定义,普通模式的 Cookie 清理步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
|
||||
- 展示 `Plus 模式` 开关与 PayPal 账号池配置;开启后 PayPal 配置行会切换为“账号下拉框 + 添加按钮”,添加按钮复用公共表单弹窗录入账号和密码;步骤列表切换为 Plus 模式 13 步定义,普通模式的 Cookie 清理步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
|
||||
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
|
||||
|
||||
### 2.2 Background Service Worker
|
||||
@@ -118,7 +117,6 @@
|
||||
- 步骤顺序靠 `order`
|
||||
- 步骤文件名靠语义
|
||||
- 新增步骤时不需要重命名后续文件
|
||||
- 普通模式使用 10 步定义;Plus 模式使用 13 步定义,其中 Plus 可见步骤 10/11/12/13 复用原 OAuth 登录、登录验证码、OAuth 同意页与平台回调验证执行器,但按 Plus 可见步骤编号记录状态
|
||||
|
||||
## 4. 状态与存储链路
|
||||
|
||||
@@ -141,7 +139,6 @@
|
||||
- 第 8 步固定的验证码页显示邮箱 `step8VerificationTargetEmail`
|
||||
- 当前手机号验证激活记录 `currentPhoneActivation`
|
||||
- 可复用的手机号验证激活记录 `reusablePhoneActivation`
|
||||
- Plus checkout / PayPal 运行态:`plusCheckoutTabId`、`plusCheckoutUrl`、`plusCheckoutCountry`、`plusCheckoutCurrency`、`plusBillingCountryText`、`plusBillingAddress`、`plusPaypalApprovedAt`、`plusReturnUrl`
|
||||
- localhost 回调地址
|
||||
- 自动运行轮次信息
|
||||
- 当前自动运行 session 标识 `autoRunSessionId`
|
||||
@@ -163,7 +160,7 @@
|
||||
- Codex2API 配置
|
||||
- IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值
|
||||
- Plus 模式开关 `plusModeEnabled`
|
||||
- PayPal 登录配置 `paypalEmail / paypalPassword`
|
||||
- PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword`
|
||||
- 邮箱 provider 配置
|
||||
- Hotmail 账号池
|
||||
- 2925 账号池
|
||||
@@ -171,8 +168,7 @@
|
||||
- 2925 当前选中的号池账号 ID `currentMail2925AccountId`
|
||||
- Cloudflare / Temp Email 设置
|
||||
- 接码开关,以及 HeroSMS 的 API Key 与默认国家设置
|
||||
- iCloud 相关偏好:Host、获取策略、成功后自动删除、目标邮箱类型与转发收码邮箱 provider
|
||||
- iCloud Hide My Email 别名缓存:`icloudAliasCache` / `icloudAliasCacheAt`,用于在 iCloud 会话或网络上下文短暂波动时回退展示最近可用列表
|
||||
- iCloud 相关偏好
|
||||
- LuckMail API 配置
|
||||
- 自动运行默认配置
|
||||
- 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止)
|
||||
@@ -458,7 +454,6 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
补充:
|
||||
|
||||
- HeroSMS 号码当前最多复用 3 次成功注册;超过上限后会清空可复用激活记录,下次重新申请新号码。
|
||||
- HeroSMS 新号码申请会先查询当前国家与 OpenAI 服务的最低价并携带固定价格参数;如果 `getNumber` 返回 `NO_NUMBERS`,会回退到 `getNumberV2`,后续用 `getStatusV2` 轮询验证码。
|
||||
- 如果同一个号码在重发短信后 60 秒仍收不到验证码,后台会抛出“回到步骤 7 重新拿新号码”的恢复错误,而不是把当前号码无限重试下去。
|
||||
|
||||
### Step 10
|
||||
@@ -501,7 +496,7 @@ Plus 模式可见步骤:
|
||||
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
|
||||
2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session,并打开 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`。
|
||||
3. 第 7 步 `填写账单并提交订阅`:选择 PayPal,生成账单全名,从 `data/address-sources.js` 读取同国家 seed query,触发 checkout 内置 Google 地址推荐,选择推荐项并校验地址第 1 行、城市、州/省、邮编等结构化字段,再点击“订阅”。运行时会按 Stripe iframe 拆分执行:付款方式在 `elements-inner-payment` frame,账单地址在 `elements-inner-address` frame,Google 推荐在 `elements-inner-autocompl` frame 时单独点击推荐项。
|
||||
4. 第 8 步 `PayPal 登录与授权`:在 PayPal 页面填写 `paypalEmail / paypalPassword`,登录前固定等待 1 秒,关闭可见通行密钥提示,点击“同意并继续”。
|
||||
4. 第 8 步 `PayPal 登录与授权`:后台优先读取侧边栏当前选中的 PayPal 账号;为兼容旧链路,也会把该账号同步回 `paypalEmail / paypalPassword`。当页面处于账号输入阶段时,内容脚本会固定对邮箱输入框执行 `focus -> clear -> fill -> blur -> click next`,即使文本框里已经预填了同一个账号,也会重新触发输入事件,避免 PayPal 因跳过重填而停在邮箱页;登录前固定等待 1 秒,关闭可见通行密钥提示,点击“同意并继续”。
|
||||
5. 第 9 步 `订阅回跳确认`:等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。
|
||||
6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。
|
||||
7. 第 11 步:复用原 Step 8 登录验证码执行器,但状态和日志按 Plus 可见第 11 步记录。
|
||||
@@ -647,7 +642,6 @@ Plus 模式可见步骤:
|
||||
- `163`、`163 VIP`、`126` 都走同一条“网易网页邮箱”验证码链路。
|
||||
- sidepanel 只负责切换 provider 与展示登录入口;后台根据 provider 选择对应网页邮箱首页。
|
||||
- 内容脚本来源统一归类到 `mail-163`,这样 Step 4 / Step 8 继续复用同一套验证码读取与邮件清理逻辑。
|
||||
- `mail-provider-utils.js` 同时承接 iCloud 转发收码可选 provider 的归一化与入口配置,避免 background / sidepanel 重新复制 QQ、网易和 Gmail 的收码地址、label 与注入脚本。
|
||||
- `manifest.json` 需要同时覆盖:
|
||||
- `https://mail.163.com/*`
|
||||
- `https://webmail.vip.163.com/*`
|
||||
@@ -742,7 +736,6 @@ Plus 模式可见步骤:
|
||||
组成:
|
||||
|
||||
- [icloud-utils.js](c:/Users/projectf/Downloads/codex注册扩展/icloud-utils.js)
|
||||
- [mail-provider-utils.js](c:/Users/projectf/Downloads/codex注册扩展/mail-provider-utils.js)
|
||||
- [content/icloud-mail.js](c:/Users/projectf/Downloads/codex注册扩展/content/icloud-mail.js)
|
||||
|
||||
配置:
|
||||
@@ -804,7 +797,6 @@ Hide My Email 获取与管理链路:
|
||||
- UI 层不能凭输入值直接判断“代理已接管”,只能展示后台返回的 `ipProxyApplied*` 状态。
|
||||
- 新增代理服务商时,应优先新增 provider 规则模块,并让共享解析/运行态继续走 `background/ip-proxy-core.js`。
|
||||
- 修改代理字段、权限或链路时,需要同步更新 [docs/ip-proxy-module.md](c:/Users/projectf/Downloads/codex注册扩展/docs/ip-proxy-module.md)、当前完整链路说明和结构说明。
|
||||
|
||||
## 8. 自动运行完整链路
|
||||
|
||||
文件:
|
||||
@@ -820,11 +812,9 @@ Hide My Email 获取与管理链路:
|
||||
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
|
||||
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
|
||||
5. 执行 `runAutoSequenceFromStep`
|
||||
- 自动运行会按当前 `plusModeEnabled` 选择普通 10 步或 Plus 13 步可见步骤;Plus 模式下第 6~9 步走 checkout / PayPal,第 10~13 步复用 OAuth 后半段
|
||||
- 步骤 7 内部仍保留登录态恢复的有限重试,但 `add-phone / 手机号页` 属于立即跳出的不可重试错误
|
||||
- 步骤 8 若在验证码提交后进入 `add-phone / 手机号页`,会直接抛出 fatal 错误,不再先标记步骤成功
|
||||
- 普通模式一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开
|
||||
- Plus 模式在 checkout / PayPal 结束后,如果 OAuth 后半段遇到普通报错且认证流程未进入 `add-phone`,则自动回到 Plus 可见步骤 10 重新开始授权链路
|
||||
- 一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开
|
||||
- 如果命中 `add-phone / 手机号页` 这类 fatal 错误,则不会再做当前轮的内部重试;当开启自动重试/跳过失败时,会直接结束当前轮并继续下一轮,而不是把整条自动流程暂停
|
||||
- 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 7 重开”
|
||||
6. 如果失败,根据设置决定:
|
||||
|
||||
+14
-5
@@ -29,6 +29,7 @@
|
||||
- `mail-provider-utils.js`:网页邮箱 provider 配置纯工具,负责 `163 / 163 VIP / 126 / QQ / Inbucket / Hotmail` 的基础归一化与页面入口配置,并统一承接 iCloud 转发收码目标邮箱 provider 的归一化、选项列表和收码入口配置。
|
||||
- `mail2925-utils.js`:2925 账号池相关的纯工具函数,负责账号归一化、冷却期判断、可用账号挑选、批量导入解析与列表更新。
|
||||
- `managed-alias-utils.js`:共享 Gmail / 2925 的别名邮箱规则,负责解析基邮箱、校验“当前完整注册邮箱”是否仍与基邮箱兼容、生成 Gmail `+tag` 与 2925 随机后缀邮箱,并输出 sidepanel 可复用的 UI 文案;当前还统一承接 `mail2925Mode` 的归一化与“2925 仅在 provide 模式下参与别名生成”的共享判定。
|
||||
- `paypal-utils.js`:PayPal 账号池相关的纯工具函数,负责账号归一化、按 ID 查找和列表 upsert。
|
||||
- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集;当前额外声明 `proxy / webRequest / webRequestAuthProvider`,用于 IP 代理 PAC 接管和代理鉴权回填。
|
||||
- `microsoft-email.js`:Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。
|
||||
- `package.json`:仓库最小 Node 包配置,目前主要提供测试脚本定义。
|
||||
@@ -50,8 +51,9 @@
|
||||
- `background/ip-proxy-provider-711proxy.js`:711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。
|
||||
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。
|
||||
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
|
||||
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息。
|
||||
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息。
|
||||
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
|
||||
- `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`。
|
||||
- `background/phone-verification-flow.js`:手机号验证共享流程模块,负责在 OAuth 链路命中 `add-phone / phone-verification` 页面后向 HeroSMS 申请或复用号码、轮询短信验证码、提交手机号码与短信验证码,并在号码长期收不到短信时把后续自动流拉回步骤 7 重新拿号。
|
||||
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
|
||||
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
|
||||
@@ -86,7 +88,7 @@
|
||||
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
|
||||
- `content/mail-163.js`:163 / 163 VIP / 126 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
|
||||
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱自动登录态确认、收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路;当后台显式开启 `mail2925MatchTargetEmail` 时,会对邮件里显式出现的目标邮箱做弱匹配,避免 `receive` 模式误捞别人的验证码。
|
||||
- `content/paypal-flow.js`:PayPal 页面脚本,负责识别登录表单、填写 PayPal 账号密码、处理页面内可见通行密钥提示,并点击 PayPal 授权页的“同意并继续”按钮。
|
||||
- `content/paypal-flow.js`:PayPal 页面脚本,负责识别登录表单、填写 PayPal 账号密码、在账号页固定执行 `focus -> clear -> fill -> blur` 重新触发邮箱输入事件、处理页面内可见通行密钥提示,并点击 PayPal 授权页的“同意并继续”按钮。
|
||||
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
|
||||
- `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。
|
||||
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
||||
@@ -130,6 +132,7 @@
|
||||
- `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
|
||||
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
|
||||
- `sidepanel/mail-2925-manager.js`:侧边栏 2925 账号池管理器,负责 2925 账号的新增、导入、切换、手动登录、启停、清冷却与删除。
|
||||
- `sidepanel/form-dialog.js`:侧边栏公共表单弹窗模块,负责渲染可复用的小型表单弹窗,支持动态字段、校验、确认与取消。
|
||||
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
|
||||
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
|
||||
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
|
||||
@@ -144,7 +147,8 @@
|
||||
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` 时
|
||||
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
|
||||
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
|
||||
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;设置卡片新增 `Plus 模式` 开关与 PayPal 账号/密码输入行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
|
||||
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
|
||||
- `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
|
||||
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
|
||||
动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接
|
||||
@@ -152,8 +156,9 @@
|
||||
示并同时服务于 provide / receive 两种模式;当 provider 为 iCloud 时,会保存并回显目标邮箱类型与转发邮箱 provider,相关 provider 选
|
||||
项和 label 复用 `mail-provider-utils.js`;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保
|
||||
存;`邮箱服务 = 自定义邮箱` 时,如果配置了 `customMailProviderPool`,也会按号池长度锁定自动轮数并在 Auto 中按轮次分配注册邮箱,同时
|
||||
在普通失败时继续复用当前邮箱,只有成功或出现手机号验证时才切换下一个邮箱;Plus 模式开启后,步骤列表会切换为 13 步,并显示 PayPal 凭
|
||||
据配置;Step 8 的自定义邮箱确认弹窗当前额外提供“出现手机号验证”按钮,用于直接走与真实 add-phone 一致的 fatal 分支;HeroSMS 国家列表
|
||||
在普通失败时继续复用当前邮箱,只有成功或出现手机号验证时才切换下一个邮箱;Plus 模式开启后,步骤列表会切换为 13 步,并显示 PayPal 账
|
||||
号下拉框与添加入口;当前选中的 PayPal 账号会同步回兼容字段 `paypalEmail / paypalPassword` 供后台步骤复用;Step 8 的自定义邮箱确认弹
|
||||
窗当前额外提供“出现手机号验证”按钮,用于直接走与真实 add-phone 一致的 fatal 分支;HeroSMS 国家列表
|
||||
会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只
|
||||
要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;
|
||||
Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;IP 代理配置保存、服务配置快照、开关应用和运行态回显由这里接入消息路由,具体解析/状态渲染下沉到 `sidepanel/ip-proxy-panel.js`;侧边栏初始化与点击“自动”前会刷新一次贡献站
|
||||
@@ -189,6 +194,7 @@
|
||||
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑。
|
||||
- `tests/background-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。
|
||||
- `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
|
||||
- `tests/background-paypal-account-store-module.test.js`:测试 PayPal 账号池后台模块已接入、导出工厂,并覆盖当前账号选择会同步回兼容字段 `paypalEmail / paypalPassword`。
|
||||
- `tests/background-platform-verify-codex2api.test.js`:测试 Codex2API 新来源在步骤 10 走协议式 callback 交换,不依赖后台页面注入。
|
||||
- `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
|
||||
- `tests/background-skip-step-linking.test.js`:测试手动跳过步骤 1 时,会级联跳过步骤 2~5,并仅跳过其中未完成且未运行的步骤。
|
||||
@@ -202,6 +208,8 @@
|
||||
- `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
|
||||
- `tests/phone-auth-country-match.test.js`:测试认证页手机号国家选择在页面本地化时,仍能用 HeroSMS 的英文国家名匹配对应国家选项。
|
||||
- `tests/phone-verification-flow.test.js`:测试手机号验证共享流程对 HeroSMS 的取号、复用、重发、换号与 Step 7 重开错误分支。
|
||||
- `tests/paypal-approve-detection.test.js`:测试 Plus 第 8 步后台执行器对 PayPal 标签页发现、分离式账号/密码页识别、联合登录页识别,以及登录后直接离开 PayPal 页的分支判断。
|
||||
- `tests/paypal-flow-content.test.js`:测试 PayPal 内容脚本对可见邮箱/密码输入框的识别,并覆盖邮箱页即使已预填相同账号也会先清空再重填后继续下一步。
|
||||
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe。
|
||||
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。
|
||||
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
|
||||
@@ -229,6 +237,7 @@
|
||||
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
|
||||
- `tests/sidepanel-mail2925-manager.test.js`:测试侧边栏 2925 管理器模块接线、共享号池表单脚本加载顺序、显隐交互与空态渲染。
|
||||
- `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。
|
||||
- `tests/sidepanel-paypal-manager.test.js`:测试侧边栏公共表单弹窗脚本与 PayPal manager 的加载顺序,以及 PayPal 账号保存后会立即选中当前账号。
|
||||
- `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。
|
||||
- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式,以及本地化邮箱输入框识别。
|
||||
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
|
||||
|
||||
Reference in New Issue
Block a user