fix: preserve oauth step metadata and resolve dev merge conflicts
This commit is contained in:
+534
-30
@@ -8,6 +8,8 @@ importScripts(
|
||||
'background/account-run-history.js',
|
||||
'background/contribution-oauth.js',
|
||||
'background/mail-2925-session.js',
|
||||
'background/ip-proxy-provider-711proxy.js',
|
||||
'background/ip-proxy-core.js',
|
||||
'background/panel-bridge.js',
|
||||
'background/generated-email-helpers.js',
|
||||
'background/signup-flow-helpers.js',
|
||||
@@ -180,6 +182,43 @@ const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
||||
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
|
||||
const DEFAULT_SUB2API_PROXY_NAME = '';
|
||||
const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||||
const DEFAULT_IP_PROXY_SERVICE = '711proxy';
|
||||
const IP_PROXY_SERVICE_VALUES = ['711proxy', 'lumiproxy', 'iproyal', 'omegaproxy'];
|
||||
const IP_PROXY_ENABLED_SERVICE_VALUES = ['711proxy'];
|
||||
const DEFAULT_IP_PROXY_MODE = 'account';
|
||||
const IP_PROXY_MODE_VALUES = ['api', 'account'];
|
||||
const DEFAULT_IP_PROXY_PROTOCOL = 'http';
|
||||
const IP_PROXY_PROTOCOL_VALUES = ['http', 'https', 'socks4', 'socks5'];
|
||||
const IP_PROXY_FETCH_TIMEOUT_MS = 20000;
|
||||
const IP_PROXY_SETTINGS_SCOPE = 'regular';
|
||||
const IP_PROXY_BYPASS_LIST = ['<local>', 'localhost', '127.0.0.1'];
|
||||
const IP_PROXY_ROUTE_ALL_TRAFFIC = true;
|
||||
const IP_PROXY_ACCOUNT_LIST_ENABLED = false;
|
||||
const IP_PROXY_INIT_ENABLE_EXIT_PROBE = false;
|
||||
const IP_PROXY_INIT_SUPPRESS_AUTH_REBIND = true;
|
||||
const IP_PROXY_INIT_AUTO_APPLY = false;
|
||||
const IP_PROXY_TARGET_HOST_PATTERNS = [
|
||||
'openai.com',
|
||||
'*.openai.com',
|
||||
'chatgpt.com',
|
||||
'*.chatgpt.com',
|
||||
'ipwho.is',
|
||||
'*.ipwho.is',
|
||||
'ipapi.co',
|
||||
'*.ipapi.co',
|
||||
'ipinfo.io',
|
||||
'*.ipinfo.io',
|
||||
'api.ipify.org',
|
||||
'api64.ipify.org',
|
||||
'api.ip.cc',
|
||||
'ifconfig.me',
|
||||
'checkip.amazonaws.com',
|
||||
'ipv4.icanhazip.com',
|
||||
'ident.me',
|
||||
'httpbin.org',
|
||||
'ip-api.com',
|
||||
'myip.ipip.net',
|
||||
];
|
||||
const AUTO_RUN_TIMER_ALARM_NAME = 'auto-run-timer';
|
||||
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
|
||||
@@ -278,6 +317,21 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
sub2apiPassword: '',
|
||||
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME,
|
||||
sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME,
|
||||
ipProxyEnabled: false,
|
||||
ipProxyService: DEFAULT_IP_PROXY_SERVICE,
|
||||
ipProxyMode: DEFAULT_IP_PROXY_MODE,
|
||||
ipProxyApiUrl: '',
|
||||
ipProxyServiceProfiles: {},
|
||||
ipProxyAccountList: '',
|
||||
ipProxyAccountSessionPrefix: '',
|
||||
ipProxyAccountLifeMinutes: '',
|
||||
ipProxyPoolTargetCount: '20',
|
||||
ipProxyHost: '',
|
||||
ipProxyPort: '',
|
||||
ipProxyProtocol: DEFAULT_IP_PROXY_PROTOCOL,
|
||||
ipProxyUsername: '',
|
||||
ipProxyPassword: '',
|
||||
ipProxyRegion: '',
|
||||
codex2apiUrl: DEFAULT_CODEX2API_URL,
|
||||
codex2apiAdminKey: '',
|
||||
customPassword: '',
|
||||
@@ -379,6 +433,15 @@ const DEFAULT_STATE = {
|
||||
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
|
||||
logs: [], // 侧边栏展示的运行日志。
|
||||
...PERSISTED_SETTING_DEFAULTS, // 合并 chrome.storage.local 中持久化保存的用户配置。
|
||||
ipProxyApiPool: [],
|
||||
ipProxyApiCurrentIndex: 0,
|
||||
ipProxyApiCurrent: null,
|
||||
ipProxyAccountPool: [],
|
||||
ipProxyAccountCurrentIndex: 0,
|
||||
ipProxyAccountCurrent: null,
|
||||
ipProxyPool: [],
|
||||
ipProxyCurrentIndex: 0,
|
||||
ipProxyCurrent: null,
|
||||
luckmailApiKey: '',
|
||||
luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL,
|
||||
luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE,
|
||||
@@ -409,6 +472,21 @@ const DEFAULT_STATE = {
|
||||
currentHotmailAccountId: null,
|
||||
currentMail2925AccountId: null,
|
||||
preferredIcloudHost: '',
|
||||
ipProxyApplied: false,
|
||||
ipProxyAppliedReason: 'disabled',
|
||||
ipProxyAppliedAt: 0,
|
||||
ipProxyAppliedHost: '',
|
||||
ipProxyAppliedPort: 0,
|
||||
ipProxyAppliedRegion: '',
|
||||
ipProxyAppliedHasAuth: false,
|
||||
ipProxyAppliedProvider: DEFAULT_IP_PROXY_SERVICE,
|
||||
ipProxyAppliedError: '',
|
||||
ipProxyAppliedWarning: '',
|
||||
ipProxyAppliedExitIp: '',
|
||||
ipProxyAppliedExitRegion: '',
|
||||
ipProxyAppliedExitDetecting: false,
|
||||
ipProxyAppliedExitError: '',
|
||||
ipProxyAppliedExitSource: '',
|
||||
};
|
||||
|
||||
function normalizeAutoRunDelayMinutes(value) {
|
||||
@@ -1024,6 +1102,63 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return String(value || '').trim();
|
||||
case 'sub2apiDefaultProxyName':
|
||||
return String(value || '').trim();
|
||||
case 'ipProxyEnabled':
|
||||
return Boolean(value);
|
||||
case 'ipProxyService':
|
||||
return normalizeIpProxyProviderValue(value);
|
||||
case 'ipProxyMode':
|
||||
return normalizeIpProxyMode(value);
|
||||
case 'ipProxyApiUrl':
|
||||
return String(value || '').trim();
|
||||
case 'ipProxyServiceProfiles':
|
||||
return normalizeIpProxyServiceProfiles(value || {}, PERSISTED_SETTING_DEFAULTS);
|
||||
case 'ipProxyAccountList':
|
||||
return normalizeIpProxyAccountList(value || '');
|
||||
case 'ipProxyAccountSessionPrefix':
|
||||
return normalizeIpProxyAccountSessionPrefix(value || '');
|
||||
case 'ipProxyAccountLifeMinutes':
|
||||
return normalizeIpProxyAccountLifeMinutes(value || '');
|
||||
case 'ipProxyPoolTargetCount':
|
||||
return normalizeIpProxyPoolTargetCount(value || '', 20);
|
||||
case 'ipProxyHost':
|
||||
return String(value || '').trim();
|
||||
case 'ipProxyPort':
|
||||
return String(normalizeIpProxyPort(value || '') || '');
|
||||
case 'ipProxyProtocol':
|
||||
return normalizeIpProxyProtocol(value);
|
||||
case 'ipProxyUsername':
|
||||
return String(value || '').trim();
|
||||
case 'ipProxyPassword':
|
||||
return String(value || '');
|
||||
case 'ipProxyRegion':
|
||||
return String(value || '').trim();
|
||||
case 'ipProxyApiPool':
|
||||
return normalizeProxyPoolEntries(
|
||||
value,
|
||||
normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE)
|
||||
);
|
||||
case 'ipProxyApiCurrentIndex':
|
||||
return normalizeIpProxyCurrentIndex(value, 0);
|
||||
case 'ipProxyApiCurrent':
|
||||
return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null;
|
||||
case 'ipProxyAccountPool':
|
||||
return normalizeProxyPoolEntries(
|
||||
value,
|
||||
normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE)
|
||||
);
|
||||
case 'ipProxyAccountCurrentIndex':
|
||||
return normalizeIpProxyCurrentIndex(value, 0);
|
||||
case 'ipProxyAccountCurrent':
|
||||
return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null;
|
||||
case 'ipProxyPool':
|
||||
return normalizeProxyPoolEntries(
|
||||
value,
|
||||
normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE)
|
||||
);
|
||||
case 'ipProxyCurrentIndex':
|
||||
return normalizeIpProxyCurrentIndex(value, 0);
|
||||
case 'ipProxyCurrent':
|
||||
return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null;
|
||||
case 'codex2apiUrl':
|
||||
return normalizeCodex2ApiUrl(value);
|
||||
case 'codex2apiAdminKey':
|
||||
@@ -1176,6 +1311,34 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
|
||||
}
|
||||
payload.cloudflareTempEmailDomains = domains;
|
||||
}
|
||||
if (payload.ipProxyServiceProfiles) {
|
||||
const selectedService = normalizeIpProxyProviderValue(
|
||||
payload.ipProxyService || PERSISTED_SETTING_DEFAULTS.ipProxyService
|
||||
);
|
||||
const normalizedProfiles = normalizeIpProxyServiceProfiles(payload.ipProxyServiceProfiles, {
|
||||
...PERSISTED_SETTING_DEFAULTS,
|
||||
...payload,
|
||||
});
|
||||
payload.ipProxyServiceProfiles = normalizedProfiles;
|
||||
const activeProfile = normalizedProfiles[selectedService]
|
||||
|| buildIpProxyServiceProfileFromState({
|
||||
...PERSISTED_SETTING_DEFAULTS,
|
||||
...payload,
|
||||
});
|
||||
payload.ipProxyService = selectedService;
|
||||
payload.ipProxyMode = normalizeIpProxyMode(activeProfile?.mode || payload.ipProxyMode);
|
||||
payload.ipProxyApiUrl = String(activeProfile?.apiUrl || payload.ipProxyApiUrl || '').trim();
|
||||
payload.ipProxyAccountList = normalizeIpProxyAccountList(activeProfile?.accountList || payload.ipProxyAccountList || '');
|
||||
payload.ipProxyAccountSessionPrefix = normalizeIpProxyAccountSessionPrefix(activeProfile?.accountSessionPrefix || payload.ipProxyAccountSessionPrefix || '');
|
||||
payload.ipProxyAccountLifeMinutes = normalizeIpProxyAccountLifeMinutes(activeProfile?.accountLifeMinutes || payload.ipProxyAccountLifeMinutes || '');
|
||||
payload.ipProxyPoolTargetCount = normalizeIpProxyPoolTargetCount(activeProfile?.poolTargetCount || payload.ipProxyPoolTargetCount || '', 20);
|
||||
payload.ipProxyHost = String(activeProfile?.host || payload.ipProxyHost || '').trim();
|
||||
payload.ipProxyPort = String(normalizeIpProxyPort(activeProfile?.port || payload.ipProxyPort || '') || '');
|
||||
payload.ipProxyProtocol = normalizeIpProxyProtocol(activeProfile?.protocol || payload.ipProxyProtocol);
|
||||
payload.ipProxyUsername = String(activeProfile?.username || payload.ipProxyUsername || '').trim();
|
||||
payload.ipProxyPassword = String(activeProfile?.password || payload.ipProxyPassword || '');
|
||||
payload.ipProxyRegion = String(activeProfile?.region || payload.ipProxyRegion || '').trim();
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
@@ -3240,21 +3403,16 @@ async function resolveLuckmailVerificationMail(client, token, filters = {}, toke
|
||||
return match || null;
|
||||
}
|
||||
|
||||
async function pollLuckmailVerificationCode(step, state, pollPayload = {}) {
|
||||
async function legacyPollLuckmailVerificationCode(step, state, pollPayload = {}) {
|
||||
const purchase = getCurrentLuckmailPurchase(state);
|
||||
if (!purchase?.token) {
|
||||
throw new Error('LuckMail 当前没有可用 token,请先执行步骤 3 购买邮箱。');
|
||||
}
|
||||
|
||||
const client = createLuckmailClient(state);
|
||||
const maxAttempts = Math.max(1, Number(pollPayload.maxAttempts) || 5);
|
||||
const intervalMs = Math.max(1000, Number(pollPayload.intervalMs) || 3000);
|
||||
const filters = {
|
||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
};
|
||||
const maxAttempts = Math.max(1, Number(pollPayload.maxAttempts) || 3);
|
||||
const intervalMs = Math.max(15000, Number(pollPayload.intervalMs) || 15000);
|
||||
const excludedCodes = new Set((pollPayload.excludeCodes || []).filter(Boolean));
|
||||
|
||||
const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||||
if (!initialCursor.messageId && !initialCursor.receivedAt) {
|
||||
@@ -3318,6 +3476,80 @@ async function pollLuckmailVerificationCode(step, state, pollPayload = {}) {
|
||||
throw lastError || new Error(`步骤 ${step}:未在 LuckMail 邮箱中找到新的匹配验证码。`);
|
||||
}
|
||||
|
||||
async function pollLuckmailVerificationCode(step, state, pollPayload = {}) {
|
||||
const purchase = getCurrentLuckmailPurchase(state);
|
||||
if (!purchase?.token) {
|
||||
throw new Error('LuckMail 当前没有可用 token,请先执行步骤 3 购买邮箱。');
|
||||
}
|
||||
|
||||
const client = createLuckmailClient(state);
|
||||
const maxAttempts = Math.max(1, Number(pollPayload.maxAttempts) || 3);
|
||||
const intervalMs = Math.max(15000, Number(pollPayload.intervalMs) || 15000);
|
||||
const excludedCodes = new Set((pollPayload.excludeCodes || []).filter(Boolean));
|
||||
|
||||
const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||||
if (!initialCursor.messageId && !initialCursor.receivedAt) {
|
||||
const mailList = await client.user.getTokenMails(purchase.token);
|
||||
const baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []);
|
||||
await setLuckmailMailCursorState(baselineCursor);
|
||||
if (baselineCursor?.messageId || baselineCursor?.receivedAt) {
|
||||
await addLog(`步骤 ${step}:LuckMail 已保存当前邮箱旧邮件快照,后续仅使用新收到的验证码。`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
throwIfStopped();
|
||||
await addLog(`步骤 ${step}:正在通过 LuckMail /code 接口轮询验证码(${attempt}/${maxAttempts})...`, 'info');
|
||||
|
||||
try {
|
||||
const tokenCode = await client.user.getTokenCode(purchase.token);
|
||||
const remoteEmail = String(tokenCode?.email_address || '').trim().toLowerCase();
|
||||
const expectedEmail = String(purchase.email_address || state?.email || '').trim().toLowerCase();
|
||||
if (remoteEmail && expectedEmail && remoteEmail !== expectedEmail) {
|
||||
throw new Error(`步骤 ${step}:LuckMail token 对应邮箱与当前邮箱不一致。当前邮箱:${expectedEmail};token 邮箱:${remoteEmail}`);
|
||||
}
|
||||
|
||||
const tokenMail = tokenCode.verification_code && tokenCode.mail && !tokenCode.mail.verification_code
|
||||
? {
|
||||
...tokenCode.mail,
|
||||
verification_code: tokenCode.verification_code,
|
||||
}
|
||||
: tokenCode.mail;
|
||||
const code = String(tokenCode?.verification_code || tokenMail?.verification_code || '').trim();
|
||||
const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||||
|
||||
if (!code || !tokenMail) {
|
||||
lastError = new Error(`步骤 ${step}:LuckMail /code 接口暂未返回新的验证码。`);
|
||||
} else if (excludedCodes.has(code)) {
|
||||
lastError = new Error(`步骤 ${step}:LuckMail 返回的验证码 ${code} 已试过,等待 15 秒后再次轮询。`);
|
||||
} else if (!isLuckmailMailNewerThanCursor(tokenMail, cursor)) {
|
||||
lastError = new Error(`步骤 ${step}:LuckMail /code 返回的最新邮件仍是旧验证码。`);
|
||||
} else {
|
||||
await setLuckmailMailCursorState(buildLuckmailMailCursor(tokenMail));
|
||||
return {
|
||||
ok: true,
|
||||
code,
|
||||
emailTimestamp: normalizeLuckmailTimestamp(tokenMail.received_at) || Date.now(),
|
||||
mailId: tokenMail.message_id,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
if (isStopError(err)) {
|
||||
throw err;
|
||||
}
|
||||
lastError = err;
|
||||
await addLog(`步骤 ${step}:LuckMail /code 轮询失败:${err.message}`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleepWithStop(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error(`步骤 ${step}:未在 LuckMail /code 接口中获取到新的验证码。`);
|
||||
}
|
||||
|
||||
function summarizeCloudflareTempEmailMessagesForLog(messages) {
|
||||
return (messages || [])
|
||||
.slice()
|
||||
@@ -5450,23 +5682,48 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
|
||||
const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10]);
|
||||
|
||||
function waitForStepComplete(step, timeoutMs = 120000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
throwIfStopped();
|
||||
if (stepWaiters.has(step)) {
|
||||
console.warn(LOG_PREFIX, `[waitForStepComplete] replacing existing waiter for step ${step}`);
|
||||
}
|
||||
console.log(LOG_PREFIX, `[waitForStepComplete] register step ${step}, timeout=${timeoutMs}ms`);
|
||||
throwIfStopped();
|
||||
const normalizedStep = Number(step);
|
||||
const existingWaiter = stepWaiters.get(normalizedStep);
|
||||
if (existingWaiter?.promise) {
|
||||
console.log(LOG_PREFIX, `[waitForStepComplete] reuse existing waiter for step ${normalizedStep}`);
|
||||
return existingWaiter.promise;
|
||||
}
|
||||
|
||||
console.log(LOG_PREFIX, `[waitForStepComplete] register step ${normalizedStep}, timeout=${timeoutMs}ms`);
|
||||
const waiter = {
|
||||
promise: null,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
};
|
||||
|
||||
waiter.promise = new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
stepWaiters.delete(step);
|
||||
console.warn(LOG_PREFIX, `[waitForStepComplete] timeout for step ${step} after ${timeoutMs}ms`);
|
||||
reject(new Error(`步骤 ${step} 等待超时(>${timeoutMs / 1000} 秒)`));
|
||||
if (stepWaiters.get(normalizedStep) === waiter) {
|
||||
stepWaiters.delete(normalizedStep);
|
||||
}
|
||||
console.warn(LOG_PREFIX, `[waitForStepComplete] timeout for step ${normalizedStep} after ${timeoutMs}ms`);
|
||||
reject(new Error(`步骤 ${normalizedStep} 等待超时(>${timeoutMs / 1000} 秒)`));
|
||||
}, timeoutMs);
|
||||
|
||||
stepWaiters.set(step, {
|
||||
resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
|
||||
reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
|
||||
});
|
||||
waiter.resolve = (data) => {
|
||||
clearTimeout(timer);
|
||||
if (stepWaiters.get(normalizedStep) === waiter) {
|
||||
stepWaiters.delete(normalizedStep);
|
||||
}
|
||||
resolve(data);
|
||||
};
|
||||
waiter.reject = (err) => {
|
||||
clearTimeout(timer);
|
||||
if (stepWaiters.get(normalizedStep) === waiter) {
|
||||
stepWaiters.delete(normalizedStep);
|
||||
}
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
|
||||
stepWaiters.set(normalizedStep, waiter);
|
||||
return waiter.promise;
|
||||
}
|
||||
|
||||
function doesStepUseCompletionSignal(step) {
|
||||
@@ -6104,6 +6361,85 @@ async function deleteAndBroadcastAccountRunHistoryRecords(recordIds = [], stateO
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolveIpProxyCandidateCountForAutoSwitch(state = {}, mode = 'account', provider = DEFAULT_IP_PROXY_SERVICE) {
|
||||
const normalizedMode = typeof normalizeIpProxyMode === 'function'
|
||||
? normalizeIpProxyMode(mode)
|
||||
: String(mode || 'account').trim().toLowerCase();
|
||||
const normalizedProvider = typeof normalizeIpProxyProviderValue === 'function'
|
||||
? normalizeIpProxyProviderValue(provider)
|
||||
: String(provider || DEFAULT_IP_PROXY_SERVICE).trim().toLowerCase();
|
||||
if (normalizedMode === 'account' && typeof getAccountModeProxyPoolFromState === 'function') {
|
||||
const pool = getAccountModeProxyPoolFromState(state, normalizedProvider);
|
||||
return Array.isArray(pool) ? pool.length : 0;
|
||||
}
|
||||
if (typeof getIpProxyRuntimeSnapshot === 'function') {
|
||||
const runtime = getIpProxyRuntimeSnapshot(state, normalizedMode, normalizedProvider);
|
||||
return Array.isArray(runtime?.pool) ? runtime.pool.length : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload = {}) {
|
||||
if (typeof switchIpProxy !== 'function') {
|
||||
return null;
|
||||
}
|
||||
const successfulRuns = Number(payload?.successfulRuns) || 0;
|
||||
if (successfulRuns <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const state = await getState();
|
||||
if (!state?.ipProxyEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mode = typeof normalizeIpProxyMode === 'function'
|
||||
? normalizeIpProxyMode(state?.ipProxyMode)
|
||||
: String(state?.ipProxyMode || 'account').trim().toLowerCase();
|
||||
const provider = typeof normalizeIpProxyProviderValue === 'function'
|
||||
? normalizeIpProxyProviderValue(state?.ipProxyService)
|
||||
: String(state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE).trim().toLowerCase();
|
||||
const threshold = typeof resolveIpProxyAutoSwitchThreshold === 'function'
|
||||
? resolveIpProxyAutoSwitchThreshold(state)
|
||||
: Math.max(1, Math.min(500, Number(state?.ipProxyPoolTargetCount) || 20));
|
||||
if (successfulRuns % threshold !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidateCount = resolveIpProxyCandidateCountForAutoSwitch(state, mode, provider);
|
||||
if (candidateCount <= 1) {
|
||||
await addLog(
|
||||
`任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),但当前仅 ${candidateCount} 条可切换代理,已跳过自动切换。`,
|
||||
'info'
|
||||
);
|
||||
return {
|
||||
skipped: true,
|
||||
reason: 'insufficient_candidates',
|
||||
candidateCount,
|
||||
threshold,
|
||||
successfulRuns,
|
||||
};
|
||||
}
|
||||
|
||||
const switchResult = await switchIpProxy('next', {
|
||||
mode,
|
||||
state,
|
||||
forceRefresh: mode === 'api',
|
||||
maxItems: typeof resolveIpProxyPoolTargetCountForMode === 'function'
|
||||
? resolveIpProxyPoolTargetCountForMode(state, mode)
|
||||
: undefined,
|
||||
});
|
||||
const display = String(switchResult?.display || '').trim();
|
||||
const routingApplied = Boolean(switchResult?.proxyRouting?.applied);
|
||||
await addLog(
|
||||
routingApplied
|
||||
? `任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),已自动切换代理:${display || '已切换到下一条'}。`
|
||||
: `任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),已尝试自动切换代理,但连通性仍异常。`,
|
||||
routingApplied ? 'ok' : 'warn'
|
||||
);
|
||||
return switchResult;
|
||||
}
|
||||
|
||||
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
|
||||
addLog,
|
||||
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
|
||||
@@ -6130,6 +6466,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR
|
||||
isStopError,
|
||||
launchAutoRunTimerPlan,
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes,
|
||||
onAutoRunRoundSuccess: (payload = {}) => maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload),
|
||||
persistAutoRunTimerPlan,
|
||||
resetState,
|
||||
runAutoSequenceFromStep: (...args) => runAutoSequenceFromStep(...args),
|
||||
@@ -6928,6 +7265,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
|
||||
addLog,
|
||||
chrome,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
completeStepFromBackground,
|
||||
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
|
||||
@@ -6995,6 +7333,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
buildLuckmailSessionSettingsPayload,
|
||||
buildPersistentSettingsPayload,
|
||||
broadcastDataUpdate,
|
||||
applyIpProxySettingsFromState,
|
||||
cancelScheduledAutoRun,
|
||||
checkIcloudSession,
|
||||
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
|
||||
@@ -7051,6 +7390,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
launchAutoRunTimerPlan,
|
||||
listIcloudAliases,
|
||||
listLuckmailPurchasesForManagement,
|
||||
refreshIpProxyPool,
|
||||
getCurrentMail2925Account,
|
||||
normalizeHotmailAccounts,
|
||||
normalizeMail2925Accounts,
|
||||
@@ -7062,10 +7402,13 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
patchMail2925Account,
|
||||
registerTab,
|
||||
requestStop,
|
||||
probeIpProxyExit,
|
||||
resetState,
|
||||
resumeAutoRun,
|
||||
scheduleAutoRun,
|
||||
selectLuckmailPurchase,
|
||||
switchIpProxy,
|
||||
changeIpProxyExit,
|
||||
setCurrentHotmailAccount,
|
||||
setCurrentMail2925Account,
|
||||
setContributionMode,
|
||||
@@ -7577,11 +7920,57 @@ function isAddPhoneAuthState(authState = {}) {
|
||||
}
|
||||
|
||||
async function getPostStep6AutoRestartDecision(step, error) {
|
||||
const currentState = await getState().catch(() => ({}));
|
||||
const resolveStepKey = (stepId, state) => {
|
||||
if (typeof getStepExecutionKeyForState === 'function') {
|
||||
return getStepExecutionKeyForState(stepId, state);
|
||||
}
|
||||
return String(
|
||||
typeof getStepDefinitionForState === 'function'
|
||||
? (getStepDefinitionForState(stepId, state)?.key || '')
|
||||
: ''
|
||||
).trim();
|
||||
};
|
||||
const findStepIdByKeyForState = (targetKey, state = {}) => {
|
||||
const normalizedKey = String(targetKey || '').trim();
|
||||
if (!normalizedKey) {
|
||||
return null;
|
||||
}
|
||||
const stepIds = typeof getStepIdsForState === 'function'
|
||||
? getStepIdsForState(state)
|
||||
: [];
|
||||
for (const stepId of stepIds) {
|
||||
if (resolveStepKey(stepId, state) === normalizedKey) {
|
||||
return Number(stepId);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const isPlatformVerifyTransientRetryError = (errorMessage = '') => {
|
||||
const normalizedMessage = String(errorMessage || '');
|
||||
const mentionsTokenExchange = /auth\.openai\.com\/oauth\/token/i.test(normalizedMessage);
|
||||
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 normalizedStep = Number(step);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const shouldForceRestartFromStep7 = /restart step 7 with a new number/i.test(errorMessage);
|
||||
if (!Number.isFinite(normalizedStep) || normalizedStep < 7 || normalizedStep > getLastStepIdForState(currentState)) {
|
||||
const latestState = await getState();
|
||||
const authChainStartStep = typeof getAuthChainStartStepId === 'function'
|
||||
? getAuthChainStartStepId(latestState)
|
||||
: FINAL_OAUTH_CHAIN_START_STEP;
|
||||
const lastStepId = typeof getLastStepIdForState === 'function'
|
||||
? getLastStepIdForState(latestState)
|
||||
: (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10);
|
||||
const currentStepKey = resolveStepKey(normalizedStep, latestState);
|
||||
const confirmOauthStep = findStepIdByKeyForState('confirm-oauth', latestState);
|
||||
const shouldRetryFromConfirmStep = currentStepKey === 'platform-verify'
|
||||
&& Number.isFinite(confirmOauthStep)
|
||||
&& confirmOauthStep > 0
|
||||
&& confirmOauthStep < normalizedStep
|
||||
&& isPlatformVerifyTransientRetryError(errorMessage);
|
||||
const restartAnchorStep = shouldRetryFromConfirmStep ? confirmOauthStep : authChainStartStep;
|
||||
if (!Number.isFinite(normalizedStep) || normalizedStep < authChainStartStep || normalizedStep > lastStepId) {
|
||||
return {
|
||||
shouldRestart: false,
|
||||
blockedByAddPhone: false,
|
||||
@@ -7614,7 +8003,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
let authState = null;
|
||||
try {
|
||||
authState = await getLoginAuthStateFromContent({
|
||||
logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 7 重开...`,
|
||||
logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 ${restartAnchorStep} 重开...`,
|
||||
});
|
||||
} catch (inspectError) {
|
||||
console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', {
|
||||
@@ -7638,6 +8027,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
shouldRestart: true,
|
||||
blockedByAddPhone: false,
|
||||
forcedByPhoneVerificationTimeout: false,
|
||||
restartStep: restartAnchorStep,
|
||||
errorMessage,
|
||||
authState,
|
||||
};
|
||||
@@ -7668,8 +8058,14 @@ async function getLoginAuthStateFromContent(options = {}) {
|
||||
}
|
||||
|
||||
async function ensureStep8VerificationPageReady(options = {}) {
|
||||
const pageState = await getLoginAuthStateFromContent(options);
|
||||
if (pageState.state === 'verification_page') {
|
||||
const visibleStep = Number(options.visibleStep) || 8;
|
||||
const authLoginStep = Number(options.authLoginStep) || (visibleStep >= 11 ? 10 : 7);
|
||||
const inspectState = async (overrides = {}) => getLoginAuthStateFromContent({
|
||||
...options,
|
||||
...overrides,
|
||||
});
|
||||
let pageState = await inspectState();
|
||||
if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') {
|
||||
return pageState;
|
||||
}
|
||||
|
||||
@@ -7678,11 +8074,79 @@ async function ensureStep8VerificationPageReady(options = {}) {
|
||||
}
|
||||
|
||||
if (pageState.state === 'login_timeout_error_page') {
|
||||
let recovered = false;
|
||||
try {
|
||||
const recoverPayload = {
|
||||
flow: 'login',
|
||||
logLabel: `步骤 ${visibleStep}:检测到登录超时报错,正在点击“重试”恢复当前页面`,
|
||||
step: visibleStep,
|
||||
timeoutMs: 12000,
|
||||
};
|
||||
const recoverMessage = {
|
||||
type: 'RECOVER_AUTH_RETRY_PAGE',
|
||||
source: 'background',
|
||||
payload: recoverPayload,
|
||||
};
|
||||
let recoverResult = null;
|
||||
const recoverTimeoutMs = 15000;
|
||||
if (typeof sendToContentScriptResilient === 'function') {
|
||||
recoverResult = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
recoverMessage,
|
||||
{
|
||||
timeoutMs: recoverTimeoutMs,
|
||||
responseTimeoutMs: recoverTimeoutMs,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在尝试点击“重试”恢复...`,
|
||||
}
|
||||
);
|
||||
} else if (typeof sendToContentScript === 'function') {
|
||||
recoverResult = await sendToContentScript('signup-page', recoverMessage, {
|
||||
responseTimeoutMs: recoverTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
if (recoverResult?.error) {
|
||||
throw new Error(recoverResult.error);
|
||||
}
|
||||
recovered = Boolean(recoverResult?.recovered || Number(recoverResult?.clickCount) > 0);
|
||||
if (recovered && typeof addLog === 'function') {
|
||||
await addLog(`步骤 ${visibleStep}:认证页已点击“重试”,正在重新确认验证码页状态...`, 'warn');
|
||||
}
|
||||
} catch (recoverError) {
|
||||
const recoverMessage = getErrorMessage(recoverError);
|
||||
if (/^CF_SECURITY_BLOCKED::/i.test(recoverMessage)) {
|
||||
throw recoverError;
|
||||
}
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`步骤 ${visibleStep}:认证页“重试”恢复失败:${recoverMessage}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
if (recovered) {
|
||||
pageState = await inspectState({
|
||||
timeoutMs: 10000,
|
||||
responseTimeoutMs: 10000,
|
||||
retryDelayMs: 500,
|
||||
logMessage: `步骤 ${visibleStep}:认证页恢复后,正在确认验证码页是否可继续...`,
|
||||
});
|
||||
if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') {
|
||||
return pageState;
|
||||
}
|
||||
if (pageState.maxCheckAttemptsBlocked) {
|
||||
throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`);
|
||||
}
|
||||
if (pageState.state === 'add_phone_page' || pageState.state === 'phone_verification_page') {
|
||||
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
|
||||
throw new Error(`步骤 ${visibleStep}:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
|
||||
}
|
||||
}
|
||||
|
||||
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
|
||||
throw new Error(`STEP8_RESTART_STEP7::步骤 8:当前认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim());
|
||||
}
|
||||
|
||||
if (pageState.state === 'add_phone_page') {
|
||||
if (pageState.state === 'add_phone_page' || pageState.state === 'phone_verification_page') {
|
||||
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
|
||||
throw new Error(`步骤 8:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
|
||||
}
|
||||
@@ -7883,7 +8347,15 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
|
||||
throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
|
||||
}
|
||||
if (pageState?.retryPage) {
|
||||
throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`);
|
||||
const retryUrl = String(pageState?.url || '').trim();
|
||||
const consentLikeRetry = Boolean(
|
||||
pageState?.consentReady
|
||||
|| pageState?.consentPage
|
||||
|| /\/sign-in-with-chatgpt\/[^/?#]+\/consent(?:[/?#]|$)/i.test(retryUrl)
|
||||
);
|
||||
if (!consentLikeRetry) {
|
||||
throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`);
|
||||
}
|
||||
}
|
||||
if (pageState?.consentReady) {
|
||||
return pageState;
|
||||
@@ -8050,7 +8522,15 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
|
||||
throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。');
|
||||
}
|
||||
if (pageState?.retryPage) {
|
||||
throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`);
|
||||
const retryUrl = String(pageState?.url || baselineUrl || '').trim();
|
||||
const consentLikeRetry = Boolean(
|
||||
pageState?.consentReady
|
||||
|| pageState?.consentPage
|
||||
|| /\/sign-in-with-chatgpt\/[^/?#]+\/consent(?:[/?#]|$)/i.test(retryUrl)
|
||||
);
|
||||
if (!consentLikeRetry) {
|
||||
throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`);
|
||||
}
|
||||
}
|
||||
if (pageState === null) {
|
||||
if (!recovered) {
|
||||
@@ -8227,14 +8707,38 @@ chrome.runtime.onStartup.addListener(() => {
|
||||
restoreAutoRunTimerIfNeeded().catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore auto run timer on startup:', err);
|
||||
});
|
||||
if (IP_PROXY_INIT_AUTO_APPLY) {
|
||||
ensureIpProxySettingsAppliedFromCurrentState({
|
||||
skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE,
|
||||
suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND,
|
||||
}).catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore IP proxy settings on startup:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
restoreAutoRunTimerIfNeeded().catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore auto run timer on install/update:', err);
|
||||
});
|
||||
if (IP_PROXY_INIT_AUTO_APPLY) {
|
||||
ensureIpProxySettingsAppliedFromCurrentState({
|
||||
skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE,
|
||||
suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND,
|
||||
}).catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore IP proxy settings on install/update:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
restoreAutoRunTimerIfNeeded().catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore auto run timer:', err);
|
||||
});
|
||||
if (IP_PROXY_INIT_AUTO_APPLY) {
|
||||
ensureIpProxySettingsAppliedFromCurrentState({
|
||||
skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE,
|
||||
suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND,
|
||||
}).catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore IP proxy settings:', err);
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
// background/ip-proxy-provider-711proxy.js — 711Proxy 参数与账号规则
|
||||
(function register711ProxyProvider(root) {
|
||||
function normalizeCountryCode(value = '') {
|
||||
const raw = String(value || '').trim().toUpperCase().replace(/[^A-Z]/g, '');
|
||||
return /^[A-Z]{2}$/.test(raw) ? raw : '';
|
||||
}
|
||||
|
||||
function normalize711SessionId(value = '') {
|
||||
return String(value || '').trim().replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64);
|
||||
}
|
||||
|
||||
function normalize711SessionMinutes(value = '') {
|
||||
const raw = String(value ?? '').trim();
|
||||
if (!raw) return '';
|
||||
const numeric = Number.parseInt(raw, 10);
|
||||
if (!Number.isInteger(numeric)) return '';
|
||||
return String(Math.max(1, Math.min(180, numeric)));
|
||||
}
|
||||
|
||||
function apply711SessionToUsername(username = '', options = {}) {
|
||||
const text = String(username || '').trim();
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const sessionId = normalize711SessionId(options?.sessionId || '');
|
||||
const sessTime = normalize711SessionMinutes(options?.sessTime || '');
|
||||
let next = text;
|
||||
|
||||
if (sessionId) {
|
||||
if (/(?:^|[-_])session[-_:][A-Za-z0-9_-]+?(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i.test(next)) {
|
||||
next = next.replace(
|
||||
/((?:^|[-_])session[-_:])([A-Za-z0-9_-]+?)(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i,
|
||||
`$1${sessionId}`
|
||||
);
|
||||
} else {
|
||||
next = `${next}-session-${sessionId}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (sessTime) {
|
||||
if (/(?:^|[-_])sessTime[-_:]?\d+\b/i.test(next)) {
|
||||
next = next.replace(/((?:^|[-_])sessTime[-_:]?)(\d+)\b/i, `$1${sessTime}`);
|
||||
} else {
|
||||
next = `${next}-sessTime-${sessTime}`;
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function apply711RegionToUsername(username = '', regionCode = '') {
|
||||
const text = String(username || '').trim();
|
||||
const normalizedRegion = normalizeCountryCode(regionCode);
|
||||
if (!text || !normalizedRegion) {
|
||||
return text;
|
||||
}
|
||||
if (/(?:^|[-_])region[-_:]?[A-Za-z]{2}\b/i.test(text)) {
|
||||
return text.replace(/((?:^|[-_])region[-_:]?)([A-Za-z]{2})\b/i, `$1${normalizedRegion}`);
|
||||
}
|
||||
return `${text}-region-${normalizedRegion}`;
|
||||
}
|
||||
|
||||
function transform711ProxyAccountEntry(entry = {}, context = {}) {
|
||||
const state = context?.state || {};
|
||||
const hasAccountList = Boolean(context?.hasAccountList);
|
||||
const nextEntry = { ...entry };
|
||||
const username = String(nextEntry.username || '').trim();
|
||||
if (!username) {
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
const configuredRegion = normalizeCountryCode(state?.ipProxyRegion || '');
|
||||
if (!hasAccountList && configuredRegion) {
|
||||
nextEntry.username = apply711RegionToUsername(username, configuredRegion);
|
||||
if (!String(nextEntry.region || '').trim()) {
|
||||
nextEntry.region = configuredRegion;
|
||||
}
|
||||
}
|
||||
|
||||
// 账号列表模式按每行原样生效,不叠加固定账号区的 session/sessTime。
|
||||
if (hasAccountList) {
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
const sessionId = normalize711SessionId(state?.ipProxyAccountSessionPrefix || '');
|
||||
const sessTime = normalize711SessionMinutes(state?.ipProxyAccountLifeMinutes || '');
|
||||
if (!sessionId && !sessTime) {
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
nextEntry.username = apply711SessionToUsername(nextEntry.username, {
|
||||
sessionId,
|
||||
sessTime,
|
||||
});
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
root.transformIpProxyAccountEntryByProvider = function transformIpProxyAccountEntryByProvider(
|
||||
provider = '',
|
||||
entry = {},
|
||||
context = {}
|
||||
) {
|
||||
const normalizedProvider = String(provider || '').trim().toLowerCase();
|
||||
if (normalizedProvider === '711proxy') {
|
||||
return transform711ProxyAccountEntry(entry, context);
|
||||
}
|
||||
return entry;
|
||||
};
|
||||
})(typeof self !== 'undefined' ? self : globalThis);
|
||||
@@ -10,6 +10,7 @@
|
||||
buildLuckmailSessionSettingsPayload,
|
||||
buildPersistentSettingsPayload,
|
||||
broadcastDataUpdate,
|
||||
applyIpProxySettingsFromState,
|
||||
cancelScheduledAutoRun,
|
||||
checkIcloudSession,
|
||||
clearAccountRunHistory,
|
||||
@@ -58,6 +59,7 @@
|
||||
launchAutoRunTimerPlan,
|
||||
listIcloudAliases,
|
||||
listLuckmailPurchasesForManagement,
|
||||
refreshIpProxyPool,
|
||||
normalizeHotmailAccounts,
|
||||
normalizeMail2925Accounts,
|
||||
normalizeRunCount,
|
||||
@@ -69,11 +71,14 @@
|
||||
pollContributionStatus,
|
||||
registerTab,
|
||||
requestStop,
|
||||
probeIpProxyExit,
|
||||
handleCloudflareSecurityBlocked,
|
||||
resetState,
|
||||
resumeAutoRun,
|
||||
scheduleAutoRun,
|
||||
selectLuckmailPurchase,
|
||||
switchIpProxy,
|
||||
changeIpProxyExit,
|
||||
setCurrentMail2925Account,
|
||||
setCurrentHotmailAccount,
|
||||
setContributionMode,
|
||||
@@ -609,14 +614,138 @@
|
||||
}
|
||||
|
||||
case 'SAVE_SETTING': {
|
||||
const currentState = await getState();
|
||||
const updates = buildPersistentSettingsPayload(message.payload || {});
|
||||
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
|
||||
const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|
||||
&& Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled);
|
||||
await setPersistentSettings(updates);
|
||||
await setState({
|
||||
const stateUpdates = {
|
||||
...updates,
|
||||
...sessionUpdates,
|
||||
};
|
||||
if (modeChanged && typeof getStepIdsForState === 'function') {
|
||||
const nextStateForSteps = { ...currentState, ...stateUpdates };
|
||||
stateUpdates.stepStatuses = Object.fromEntries(
|
||||
getStepIdsForState(nextStateForSteps).map((stepId) => [stepId, 'pending'])
|
||||
);
|
||||
stateUpdates.currentStep = 0;
|
||||
}
|
||||
await setState(stateUpdates);
|
||||
const mergedState = await getState();
|
||||
const hasIpProxyUpdates = Object.keys(updates).some((key) => key.startsWith('ipProxy'));
|
||||
const hasIpProxyEnabledUpdate = Object.prototype.hasOwnProperty.call(updates, 'ipProxyEnabled');
|
||||
const previousIpProxyEnabled = Boolean(currentState?.ipProxyEnabled);
|
||||
const nextIpProxyEnabled = hasIpProxyEnabledUpdate
|
||||
? Boolean(updates.ipProxyEnabled)
|
||||
: previousIpProxyEnabled;
|
||||
// 仅在“手动开关代理”时自动应用。
|
||||
// 其他字段改动(host/账号/地区/session 等)需由“同步/下一条/检测出口/Change”显式触发。
|
||||
const shouldApplyIpProxyOnSave = hasIpProxyUpdates
|
||||
&& hasIpProxyEnabledUpdate
|
||||
&& previousIpProxyEnabled !== nextIpProxyEnabled;
|
||||
let proxyRouting = null;
|
||||
if (shouldApplyIpProxyOnSave && typeof applyIpProxySettingsFromState === 'function') {
|
||||
const isEnablingProxy = !previousIpProxyEnabled && nextIpProxyEnabled;
|
||||
proxyRouting = await applyIpProxySettingsFromState(mergedState, {
|
||||
// 手动开启时自动应用一次代理,不做出口探测;
|
||||
// 出口探测由“同步/检测出口”按钮显式触发,避免开启即误判为失败。
|
||||
skipExitProbe: true,
|
||||
resetNetworkState: false,
|
||||
forceAuthRebind: false,
|
||||
suppressAuthRebind: !isEnablingProxy,
|
||||
}).catch((error) => ({
|
||||
applied: false,
|
||||
reason: 'apply_failed',
|
||||
error: error?.message || String(error || '代理应用失败'),
|
||||
}));
|
||||
}
|
||||
if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') {
|
||||
await setContributionMode(true);
|
||||
}
|
||||
if (modeChanged) {
|
||||
await addLog(
|
||||
Boolean(updates.plusModeEnabled)
|
||||
? 'Plus 模式已开启,已切换为 Plus Checkout + PayPal 步骤。'
|
||||
: 'Plus 模式已关闭,已恢复普通注册授权步骤。',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
return { ok: true, state: await getState(), proxyRouting };
|
||||
}
|
||||
|
||||
case 'REFRESH_IP_PROXY_POOL': {
|
||||
if (typeof refreshIpProxyPool !== 'function') {
|
||||
throw new Error('IP 代理池能力尚未接入。');
|
||||
}
|
||||
const result = await refreshIpProxyPool({
|
||||
maxItems: message.payload?.maxItems,
|
||||
mode: message.payload?.mode,
|
||||
});
|
||||
return { ok: true, state: await getState() };
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
|
||||
case 'SWITCH_IP_PROXY': {
|
||||
if (typeof switchIpProxy !== 'function') {
|
||||
throw new Error('IP 代理切换能力尚未接入。');
|
||||
}
|
||||
const result = await switchIpProxy(message.payload?.direction || 'next', {
|
||||
maxItems: message.payload?.maxItems,
|
||||
mode: message.payload?.mode,
|
||||
forceRefresh: message.payload?.forceRefresh,
|
||||
});
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
|
||||
case 'CHANGE_IP_PROXY_EXIT': {
|
||||
if (typeof changeIpProxyExit !== 'function') {
|
||||
throw new Error('IP 代理 Change 能力尚未接入。');
|
||||
}
|
||||
const result = await changeIpProxyExit({
|
||||
mode: message.payload?.mode,
|
||||
});
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
|
||||
case 'PROBE_IP_PROXY_EXIT': {
|
||||
if (typeof probeIpProxyExit !== 'function') {
|
||||
throw new Error('IP 代理出口检测能力尚未接入。');
|
||||
}
|
||||
const probeState = await getState();
|
||||
const mode = typeof normalizeIpProxyMode === 'function'
|
||||
? normalizeIpProxyMode(probeState?.ipProxyMode)
|
||||
: String(probeState?.ipProxyMode || 'account').trim().toLowerCase();
|
||||
const provider = typeof normalizeIpProxyProviderValue === 'function'
|
||||
? normalizeIpProxyProviderValue(probeState?.ipProxyService)
|
||||
: String(probeState?.ipProxyService || '').trim().toLowerCase();
|
||||
const is711AccountMode = mode === 'account' && provider === '711proxy';
|
||||
const previousReason = String(probeState?.ipProxyAppliedReason || '').trim().toLowerCase();
|
||||
const previousExitError = String(probeState?.ipProxyAppliedExitError || '').trim();
|
||||
const hadMissingAuthChallenge = /challenge=0|provided=0|未触发代理鉴权挑战|未收到 407/i.test(previousExitError);
|
||||
const shouldPreRebindBeforeProbe = Boolean(
|
||||
probeState?.ipProxyEnabled
|
||||
&& is711AccountMode
|
||||
&& (hadMissingAuthChallenge || previousReason === 'connectivity_failed')
|
||||
);
|
||||
const timeoutMs = Number(message.payload?.timeoutMs) > 0
|
||||
? Number(message.payload.timeoutMs)
|
||||
: (is711AccountMode ? (shouldPreRebindBeforeProbe ? 8000 : 6000) : undefined);
|
||||
|
||||
// 手动“检测出口”前先轻量应用当前配置,避免读取到旧代理链路状态。
|
||||
if (probeState?.ipProxyEnabled && typeof applyIpProxySettingsFromState === 'function') {
|
||||
await applyIpProxySettingsFromState(probeState, {
|
||||
skipExitProbe: true,
|
||||
resetNetworkState: shouldPreRebindBeforeProbe,
|
||||
forceAuthRebind: shouldPreRebindBeforeProbe,
|
||||
suppressAuthRebind: !shouldPreRebindBeforeProbe,
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
const result = await probeIpProxyExit({
|
||||
timeoutMs,
|
||||
authRebindMaxAttempts: is711AccountMode ? 1 : undefined,
|
||||
});
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
|
||||
case 'EXPORT_SETTINGS': {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
addLog,
|
||||
chrome,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
completeStepFromBackground,
|
||||
confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession,
|
||||
@@ -68,6 +69,68 @@
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
|
||||
await setState({
|
||||
step8VerificationTargetEmail: '',
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
const fromRecovery = Boolean(options.fromRecovery);
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页${fromRecovery ? '(轮询失败后复核)' : ''},跳过登录验证码拉取并继续后续流程。`,
|
||||
'warn'
|
||||
);
|
||||
if (typeof completeStepFromBackground === 'function') {
|
||||
await completeStepFromBackground(visibleStep, {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isStep8AddPhoneStateError(error) {
|
||||
const message = String(error?.message || error || '');
|
||||
return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message);
|
||||
}
|
||||
|
||||
async function recoverStep8PollingFailure(currentState, visibleStep) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
try {
|
||||
const pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
authLoginStep,
|
||||
timeoutMs: await getStep8ReadyTimeoutMs(
|
||||
'登录验证码轮询异常后复核认证页状态',
|
||||
currentState?.oauthUrl || '',
|
||||
visibleStep
|
||||
),
|
||||
});
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true });
|
||||
return { outcome: 'completed' };
|
||||
}
|
||||
if (pageState?.state === 'verification_page') {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在验证码页,先在当前链路重试,不回到步骤 ${authLoginStep}。`,
|
||||
'warn'
|
||||
);
|
||||
return { outcome: 'retry_without_step7' };
|
||||
}
|
||||
} catch (inspectError) {
|
||||
if (isStep8RestartStep7Error(inspectError)) {
|
||||
return { outcome: 'restart_step7', error: inspectError };
|
||||
}
|
||||
if (isStep8AddPhoneStateError(inspectError)) {
|
||||
throw inspectError;
|
||||
}
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:轮询失败后复核认证页状态异常:${inspectError?.message || inspectError},将回到步骤 ${authLoginStep} 重试。`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
return { outcome: 'restart_step7' };
|
||||
}
|
||||
|
||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
@@ -131,6 +194,10 @@
|
||||
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
});
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
|
||||
return;
|
||||
}
|
||||
const shouldCompareVerificationEmail = mail.provider !== '2925';
|
||||
const displayedVerificationEmail = shouldCompareVerificationEmail
|
||||
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
|
||||
@@ -201,9 +268,11 @@
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
|
||||
requestFreshCodeFirst: false,
|
||||
targetEmail: fixedTargetEmail,
|
||||
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
|
||||
? 15000
|
||||
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -224,24 +293,47 @@
|
||||
} catch (err) {
|
||||
const visibleStep = getVisibleStep(currentState, 8);
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
if (!isVerificationMailPollingError(err) && !isStep8RestartStep7Error(err)) {
|
||||
throw err;
|
||||
let currentError = err;
|
||||
let retryWithoutStep7 = false;
|
||||
const isMailPollingError = isVerificationMailPollingError(err);
|
||||
if (isMailPollingError && !isStep8RestartStep7Error(err)) {
|
||||
const recovery = await recoverStep8PollingFailure(currentState, visibleStep);
|
||||
if (recovery?.outcome === 'completed') {
|
||||
return;
|
||||
}
|
||||
if (recovery?.outcome === 'retry_without_step7') {
|
||||
retryWithoutStep7 = true;
|
||||
}
|
||||
if (recovery?.error) {
|
||||
currentError = recovery.error;
|
||||
}
|
||||
}
|
||||
if (!isVerificationMailPollingError(currentError) && !isStep8RestartStep7Error(currentError)) {
|
||||
throw currentError;
|
||||
}
|
||||
|
||||
lastMailPollingError = err;
|
||||
lastMailPollingError = currentError;
|
||||
if (mailPollingAttempt >= STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS) {
|
||||
break;
|
||||
}
|
||||
|
||||
mailPollingAttempt += 1;
|
||||
if (retryWithoutStep7) {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}。`,
|
||||
'warn'
|
||||
);
|
||||
currentState = await getState();
|
||||
continue;
|
||||
}
|
||||
await addLog(
|
||||
isStep8RestartStep7Error(err)
|
||||
isStep8RestartStep7Error(currentError)
|
||||
? `步骤 ${visibleStep}:检测到认证页进入重试/超时报错状态,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`
|
||||
: `步骤 ${visibleStep}:检测到邮箱轮询类失败,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`,
|
||||
'warn'
|
||||
);
|
||||
await rerunStep7ForStep8Recovery({
|
||||
logMessage: isStep8RestartStep7Error(err)
|
||||
logMessage: isStep8RestartStep7Error(currentError)
|
||||
? `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...`
|
||||
: `步骤 ${visibleStep}:正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`,
|
||||
});
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
|
||||
const shouldRequestFreshCodeFirst = ![
|
||||
HOTMAIL_PROVIDER,
|
||||
LUCKMAIL_PROVIDER,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
].includes(mail.provider);
|
||||
|
||||
@@ -157,9 +158,11 @@
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
|
||||
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
|
||||
? 15000
|
||||
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,86 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function detectStep8PostSubmitFallback(options = {}) {
|
||||
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 9000);
|
||||
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || 300);
|
||||
const step = Number(options.step) || 8;
|
||||
const startedAt = Date.now();
|
||||
let lastSnapshot = null;
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
const request = {
|
||||
type: 'GET_LOGIN_AUTH_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
};
|
||||
const requestTimeoutMs = Math.max(1200, Math.min(5000, timeoutMs));
|
||||
const result = typeof sendToContentScriptResilient === 'function'
|
||||
? await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
request,
|
||||
{
|
||||
timeoutMs: requestTimeoutMs,
|
||||
responseTimeoutMs: requestTimeoutMs,
|
||||
retryDelayMs: 400,
|
||||
logMessage: `步骤 ${step}:验证码提交后页面正在切换,等待页面恢复并确认授权状态...`,
|
||||
}
|
||||
)
|
||||
: await sendToContentScript('signup-page', request, {
|
||||
responseTimeoutMs: requestTimeoutMs,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const authState = String(result?.state || '').trim();
|
||||
const authUrl = String(result?.url || '').trim();
|
||||
lastSnapshot = {
|
||||
state: authState || 'unknown',
|
||||
url: authUrl,
|
||||
};
|
||||
|
||||
if (authState === 'oauth_consent_page') {
|
||||
return {
|
||||
success: true,
|
||||
reason: 'oauth_consent_page',
|
||||
addPhonePage: false,
|
||||
url: authUrl,
|
||||
};
|
||||
}
|
||||
if (authState === 'add_phone_page' || authState === 'phone_verification_page') {
|
||||
return {
|
||||
success: true,
|
||||
reason: 'add_phone_page',
|
||||
addPhonePage: true,
|
||||
url: authUrl || 'https://auth.openai.com/add-phone',
|
||||
};
|
||||
}
|
||||
if (authState === 'login_timeout_error_page') {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'login_timeout_error_page',
|
||||
restartStep7: true,
|
||||
url: authUrl,
|
||||
};
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore transient inspect failures and keep polling.
|
||||
}
|
||||
|
||||
await sleepWithStop(pollIntervalMs);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
reason: 'unknown',
|
||||
snapshot: lastSnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
function getVerificationResendStateKey() {
|
||||
return 'verificationResendCount';
|
||||
}
|
||||
@@ -772,6 +852,31 @@
|
||||
};
|
||||
}
|
||||
}
|
||||
if (step === 8 && isRetryableVerificationTransportError(err)) {
|
||||
const fallback = await detectStep8PostSubmitFallback({
|
||||
step,
|
||||
timeoutMs: 9000,
|
||||
pollIntervalMs: 300,
|
||||
});
|
||||
if (fallback.success) {
|
||||
if (fallback.addPhonePage) {
|
||||
await addLog('步骤 8:验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn');
|
||||
} else {
|
||||
await addLog('步骤 8:验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn');
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
assumed: true,
|
||||
transportRecovered: true,
|
||||
addPhonePage: Boolean(fallback.addPhonePage),
|
||||
url: fallback.url || '',
|
||||
};
|
||||
}
|
||||
if (fallback.restartStep7) {
|
||||
const urlPart = fallback.url ? ` URL: ${fallback.url}` : '';
|
||||
throw new Error(`STEP8_RESTART_STEP7::步骤 8:验证码提交后认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim());
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
@@ -812,7 +917,7 @@
|
||||
getLegacyVerificationResendCountDefault(step, { requestFreshCodeFirst })
|
||||
)
|
||||
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
|
||||
const maxSubmitAttempts = 15;
|
||||
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
|
||||
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
|
||||
let lastResendAt = Number(options.lastResendAt) || 0;
|
||||
|
||||
@@ -897,6 +1002,12 @@
|
||||
throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
|
||||
}
|
||||
|
||||
if (mail.provider === LUCKMAIL_PROVIDER) {
|
||||
await addLog(`步骤 ${step}:LuckMail 验证码提交失败,等待 15 秒后重新轮询 /code 接口(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
|
||||
await sleepWithStop(15000);
|
||||
continue;
|
||||
}
|
||||
|
||||
const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0
|
||||
? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
|
||||
: 0;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# IP Proxy 模块说明(Ultra1 集成版)
|
||||
|
||||
## 1. 模块定位
|
||||
|
||||
`IP Proxy` 是扩展内的网络出口接管模块,用于把自动化流程的页面访问链路切到指定代理节点,并在侧边栏内可视化展示当前代理与出口状态。
|
||||
|
||||
本模块目标是解决三件事:
|
||||
|
||||
1. 在扩展内统一配置和切换代理,而不是依赖浏览器外部全局代理。
|
||||
2. 对“代理已配置但未真正接管/未真正出站”的情况进行显式诊断。
|
||||
3. 在自动化流程中可控地切换出口,降低单节点波动影响。
|
||||
|
||||
## 2. 代码结构
|
||||
|
||||
### 2.1 Background(核心逻辑)
|
||||
|
||||
- `background/ip-proxy-core.js`
|
||||
- 代理池解析与运行态管理
|
||||
- 应用/清除代理配置
|
||||
- 出口探测与状态诊断
|
||||
- `SYNC/NEXT/CHANGE/PROBE` 主流程
|
||||
- `background/ip-proxy-provider-711proxy.js`
|
||||
- Provider 级别参数处理(当前重点是 711 账号串 token 规则)
|
||||
- `background/message-router.js`
|
||||
- 暴露消息接口:
|
||||
- `REFRESH_IP_PROXY_POOL`
|
||||
- `SWITCH_IP_PROXY`
|
||||
- `CHANGE_IP_PROXY_EXIT`
|
||||
- `PROBE_IP_PROXY_EXIT`
|
||||
- `background.js`
|
||||
- 持久化字段定义与默认值
|
||||
- 启动恢复时的代理状态接管
|
||||
- 自动运行成功后的代理切换钩子
|
||||
|
||||
### 2.2 Sidepanel(界面与交互)
|
||||
|
||||
- `sidepanel/ip-proxy-panel.js`
|
||||
- 代理 UI 状态渲染
|
||||
- 按钮行为(同步/下一条/Change/检测出口)
|
||||
- 运行态文案和诊断详情展示
|
||||
- 711 账号参数双向同步(`session/sessTime/region`)
|
||||
- `sidepanel/ip-proxy-provider-711proxy.js`
|
||||
- Provider 级输入辅助(地区推断)
|
||||
- `sidepanel/sidepanel.html`
|
||||
- 代理区块 UI
|
||||
- `sidepanel/sidepanel.css`
|
||||
- 代理区块样式
|
||||
- `sidepanel/sidepanel.js`
|
||||
- 与设置保存流整合、事件绑定
|
||||
|
||||
## 3. 当前功能范围(本 PR)
|
||||
|
||||
### 3.1 基础能力
|
||||
|
||||
1. 启用/禁用代理接管(PAC + 认证回填)。
|
||||
2. 固定账号模式(Host/Port/Username/Password/Region)。
|
||||
3. 出口探测与状态卡(当前代理、当前出口、诊断详情)。
|
||||
4. 四个动作按钮:
|
||||
- `同步`:应用当前配置并刷新运行态
|
||||
- `下一条`:切到下一个可用节点
|
||||
- `Change`:保持 session 的前提下重绑并换出口(711)
|
||||
- `检测出口`:只做出口复测,不改节点
|
||||
5. `检查IP`:打开 `https://ipinfo.io/what-is-my-ip`
|
||||
|
||||
### 3.2 711 账号串参数联动
|
||||
|
||||
支持从用户名中识别并回填:
|
||||
|
||||
- `session-xxxx`
|
||||
- `sessTime-xx`(兼容 `life-xx`)
|
||||
- `region-XX`
|
||||
|
||||
支持从表单回写到用户名:
|
||||
|
||||
- 修改会话值 -> 更新 `session-*`
|
||||
- 修改时长 -> 更新 `sessTime-*`
|
||||
- 修改地区 -> 更新 `region-*`
|
||||
|
||||
### 3.3 同步后的自动复测
|
||||
|
||||
为避免“同步后还要手动点检测出口”:
|
||||
|
||||
- `同步/下一条/Change` 执行完成后,自动追加一次静默 `检测出口`。
|
||||
|
||||
## 4. 当前发布策略(为了稳定)
|
||||
|
||||
本 PR 是“先可用,再扩展”的第一阶段:
|
||||
|
||||
1. 服务商主路径按 `711` 优先。
|
||||
2. `API 模式`在 UI 中保留但禁用(暂未开放)。
|
||||
3. `账号列表模式`目前关闭(防止多条目与鉴权缓存复用带来的不稳定)。
|
||||
|
||||
对应开关位于:
|
||||
|
||||
- `sidepanel/sidepanel.js`:`IP_PROXY_API_MODE_ENABLED = false`
|
||||
- `sidepanel/sidepanel.js`:`IP_PROXY_ACCOUNT_LIST_ENABLED = false`
|
||||
- `background.js`:`IP_PROXY_ACCOUNT_LIST_ENABLED = false`
|
||||
|
||||
## 5. 使用方式(操作步骤)
|
||||
|
||||
1. 打开侧边栏 `IP代理` 开关。
|
||||
2. 选择账号模式,填写:
|
||||
- Host
|
||||
- Port
|
||||
- Username
|
||||
- Password
|
||||
- Region(可选)
|
||||
3. 点击 `同步`。
|
||||
4. 查看状态卡:
|
||||
- 当前代理
|
||||
- 当前出口
|
||||
- 是否有校验提示
|
||||
5. 需要换出口时:
|
||||
- 711 + session 场景优先用 `Change`
|
||||
- 或使用 `下一条`
|
||||
6. 需要手动复核时点击 `检测出口` 或 `检查IP`。
|
||||
|
||||
## 6. 已知限制
|
||||
|
||||
1. 某些代理链路不会返回标准 `407`,扩展无法触发认证回填,这类链路可能不稳定。
|
||||
2. 地区“期望值”和“实际出口”不一致时,模块会保留接管并提示校验告警(不直接判定全失败)。
|
||||
3. 不同探测源(页面探测/后台兜底)在网络波动时可能短时出现差异,状态卡会显示来源与诊断。
|
||||
|
||||
## 7. 回归建议
|
||||
|
||||
建议至少覆盖以下场景:
|
||||
|
||||
1. 固定账号:启用 -> 同步 -> 出口检测成功。
|
||||
2. 固定账号:`session/sessTime/region` 双向同步正确。
|
||||
3. 固定账号:`同步/下一条/Change` 后无需手动点检测即可刷新出口状态。
|
||||
4. 非标准链路失败时,诊断信息可读且不误报“已接管成功”。
|
||||
@@ -9,6 +9,9 @@
|
||||
"alarms",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"webRequest",
|
||||
"webRequestAuthProvider",
|
||||
"proxy",
|
||||
"declarativeNetRequest",
|
||||
"debugger",
|
||||
"browsingData",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
// sidepanel/ip-proxy-provider-711proxy.js — 711Proxy 面板专属逻辑
|
||||
function normalizeIpProxyCountryCode(value = '') {
|
||||
const raw = String(value || '').trim().toUpperCase().replace(/[^A-Z]/g, '');
|
||||
return /^[A-Z]{2}$/.test(raw) ? raw : '';
|
||||
}
|
||||
|
||||
function infer711RegionFromHost(host = '') {
|
||||
const text = String(host || '').trim().toLowerCase().replace(/\.$/, '');
|
||||
if (!text || !text.includes('.')) {
|
||||
return '';
|
||||
}
|
||||
const firstLabel = String(text.split('.')[0] || '').trim();
|
||||
return /^[a-z]{2}$/.test(firstLabel) ? firstLabel.toUpperCase() : '';
|
||||
}
|
||||
|
||||
function infer711RegionFromUsername(username = '') {
|
||||
const text = String(username || '').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
const match = text.match(/(?:^|[-_])region[-_:]?([A-Za-z]{2})\b/i);
|
||||
return normalizeIpProxyCountryCode(match ? match[1] : '');
|
||||
}
|
||||
|
||||
function resolve711ProxyRegionFromInputs({ host = '', username = '', region = '' } = {}) {
|
||||
const fromUsername = infer711RegionFromUsername(username);
|
||||
if (fromUsername) {
|
||||
return fromUsername;
|
||||
}
|
||||
const fromHost = infer711RegionFromHost(host);
|
||||
if (fromHost) {
|
||||
return fromHost;
|
||||
}
|
||||
return normalizeIpProxyCountryCode(region);
|
||||
}
|
||||
@@ -781,6 +781,248 @@ header {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-fold-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ip-proxy-fold {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-fold-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-layout-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ip-proxy-layout {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ip-proxy-layout.is-account-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ip-proxy-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ip-proxy-column-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ip-proxy-column-hint {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ip-proxy-column-mode-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ip-proxy-column-api.is-disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.ip-proxy-column-api.is-disabled .data-input,
|
||||
.ip-proxy-column-api.is-disabled .data-select,
|
||||
.ip-proxy-column-api.is-disabled .data-textarea,
|
||||
.ip-proxy-column-api.is-disabled .input-icon-btn,
|
||||
.ip-proxy-column-api.is-disabled .btn,
|
||||
.ip-proxy-column-api.is-disabled .choice-btn {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ip-proxy-layout {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.ip-proxy-enabled-inline {
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ip-proxy-actions-inline {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
row-gap: 6px;
|
||||
}
|
||||
|
||||
.ip-proxy-action-grid {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ip-proxy-action-grid .data-inline-btn {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-action-hint {
|
||||
flex: 1 1 100%;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
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;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ip-proxy-check-ip-btn {
|
||||
min-width: 64px;
|
||||
padding-inline: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-current {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-current.has-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--text-muted);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text-muted) 28%, transparent);
|
||||
flex-shrink: 0;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-status.state-applied .ip-proxy-runtime-dot {
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--green) 28%, transparent);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-status.state-warning .ip-proxy-runtime-dot {
|
||||
background: var(--orange);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--orange) 28%, transparent);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-status.state-error .ip-proxy-runtime-dot {
|
||||
background: var(--red);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 28%, transparent);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details summary {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details[open] summary {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ip-proxy-runtime-details-text {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.mail2925-base-inline {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@@ -194,6 +194,163 @@
|
||||
<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">
|
||||
<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;">
|
||||
<div id="ip-proxy-fold" class="ip-proxy-fold">
|
||||
<div class="ip-proxy-fold-body">
|
||||
<div class="data-row" id="row-ip-proxy-service" style="display:none;">
|
||||
<span class="data-label">代理服务</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-ip-proxy-service" class="data-select" disabled>
|
||||
<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>
|
||||
<div class="data-row" id="row-ip-proxy-mode" style="display:none;">
|
||||
<span class="data-label">代理模式</span>
|
||||
<div id="ip-proxy-mode-group" class="choice-group" role="group" aria-label="IP代理模式">
|
||||
<button type="button" class="choice-btn" data-ip-proxy-mode="account">账号密码</button>
|
||||
<button type="button" class="choice-btn" data-ip-proxy-mode="api" disabled title="首版暂未开放">API 拉取(暂未开放)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-layout-row" id="row-ip-proxy-layout" style="display:none;">
|
||||
<div class="ip-proxy-layout" id="ip-proxy-layout">
|
||||
<div class="ip-proxy-column ip-proxy-column-account" id="ip-proxy-account-panel">
|
||||
<div class="ip-proxy-column-header">账号密码模式</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-list" style="display:none;">
|
||||
<span class="data-label">账号代理列表</span>
|
||||
<textarea id="input-ip-proxy-account-list" class="data-textarea mono"
|
||||
placeholder="每行一条:host:port 或 host:port:username:password 例如 global.rotgb.711proxy.com:10000:username:password"></textarea>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-session-prefix" style="display:none;">
|
||||
<span class="data-label">会话(session)</span>
|
||||
<input type="text" id="input-ip-proxy-account-session-prefix" class="data-input mono"
|
||||
placeholder="会话前缀,例如 ZC28qZ0KQL" />
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-life-minutes" style="display:none;">
|
||||
<span class="data-label">时长(life)</span>
|
||||
<div class="data-inline">
|
||||
<input type="number" id="input-ip-proxy-account-life-minutes" class="data-input" min="1" max="1440" step="1"
|
||||
placeholder="5-180(分钟)" title="711Proxy 会话时长范围:5-180 分钟" />
|
||||
<span class="data-unit">分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-host" style="display:none;">
|
||||
<span class="data-label">代理 Host</span>
|
||||
<input type="text" id="input-ip-proxy-host" class="data-input" placeholder="例如 global.rotgb.711proxy.com" />
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-port" style="display:none;">
|
||||
<span class="data-label">代理 Port</span>
|
||||
<input type="number" id="input-ip-proxy-port" class="data-input" min="1" max="65535" step="1"
|
||||
placeholder="例如 10000" />
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-protocol" style="display:none;">
|
||||
<span class="data-label">代理协议</span>
|
||||
<select id="select-ip-proxy-protocol" class="data-select">
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="socks5">SOCKS5</option>
|
||||
<option value="socks4">SOCKS4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-username" style="display:none;">
|
||||
<span class="data-label">代理账号</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-ip-proxy-username" class="data-input data-input-with-icon"
|
||||
placeholder="例如 USER047152-zone-custom-region-US-asn-ASN81" />
|
||||
<button id="btn-toggle-ip-proxy-username" class="input-icon-btn" type="button" aria-label="显示代理账号"
|
||||
title="显示代理账号"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-password" style="display:none;">
|
||||
<span class="data-label">代理密码</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-ip-proxy-password" class="data-input data-input-with-icon"
|
||||
placeholder="账号密码代理的密码" />
|
||||
<button id="btn-toggle-ip-proxy-password" class="input-icon-btn" type="button" aria-label="显示代理密码"
|
||||
title="显示代理密码"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-region" style="display:none;">
|
||||
<span class="data-label">地区参数</span>
|
||||
<input type="text" id="input-ip-proxy-region" class="data-input" placeholder="可选,例如 US / DE / HK" />
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-pool-target-count" style="display:none;">
|
||||
<span class="data-label">任务切换阈值</span>
|
||||
<div class="data-inline">
|
||||
<input type="number" id="input-ip-proxy-pool-target-count" class="data-input" min="1" max="500" step="1"
|
||||
placeholder="20(成功轮次)" title="每成功多少轮任务后自动切换一次代理;仅 1 条节点时会跳过自动切换" />
|
||||
<span class="data-unit">轮</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ip-proxy-column ip-proxy-column-api" id="ip-proxy-api-panel">
|
||||
<div class="ip-proxy-column-header">API 模式</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-api-url" style="display:none;">
|
||||
<span class="data-label">代理API</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-ip-proxy-api-url" class="data-input data-input-with-icon"
|
||||
placeholder="粘贴完整 API 链接" />
|
||||
<button id="btn-toggle-ip-proxy-api-url" class="input-icon-btn" type="button" aria-label="显示代理 API"
|
||||
title="显示代理 API"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-ip-proxy-actions" style="display:none;">
|
||||
<span class="data-label">切换代理</span>
|
||||
<div class="data-inline ip-proxy-actions-inline">
|
||||
<span id="ip-proxy-action-buttons" class="ip-proxy-action-grid">
|
||||
<button id="btn-ip-proxy-refresh" class="btn btn-outline btn-sm data-inline-btn" type="button">拉取</button>
|
||||
<button id="btn-ip-proxy-next" class="btn btn-outline btn-sm data-inline-btn" type="button">下一条</button>
|
||||
<button id="btn-ip-proxy-change" class="btn btn-outline btn-sm data-inline-btn" type="button" title="保持当前会话并刷新出口(仅 711 + session)">Change</button>
|
||||
<button id="btn-ip-proxy-probe" class="btn btn-outline btn-sm data-inline-btn" type="button">检测出口</button>
|
||||
</span>
|
||||
<span id="ip-proxy-action-hint" class="ip-proxy-action-hint">
|
||||
下一条:切到代理池下一条。Change:保持当前 session 重绑链路(仅 711 + session)。
|
||||
</span>
|
||||
</div>
|
||||
</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">
|
||||
<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 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>
|
||||
</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"
|
||||
@@ -867,6 +1024,8 @@
|
||||
<script src="mail-2925-manager.js"></script>
|
||||
<script src="icloud-manager.js"></script>
|
||||
<script src="luckmail-manager.js"></script>
|
||||
<script src="ip-proxy-provider-711proxy.js"></script>
|
||||
<script src="ip-proxy-panel.js"></script>
|
||||
<script src="contribution-mode.js"></script>
|
||||
<script src="account-records-manager.js"></script>
|
||||
<script src="sidepanel.js"></script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadIpProxyCore({ accountListEnabled = true } = {}) {
|
||||
const providerSource = fs.readFileSync('background/ip-proxy-provider-711proxy.js', 'utf8');
|
||||
const coreSource = fs.readFileSync('background/ip-proxy-core.js', 'utf8');
|
||||
return new Function(`
|
||||
const self = {};
|
||||
const chrome = {};
|
||||
const DEFAULT_IP_PROXY_SERVICE = '711proxy';
|
||||
const IP_PROXY_SERVICE_VALUES = ['711proxy', 'lumiproxy', 'iproyal', 'omegaproxy'];
|
||||
const IP_PROXY_ENABLED_SERVICE_VALUES = ['711proxy'];
|
||||
const DEFAULT_IP_PROXY_MODE = 'account';
|
||||
const IP_PROXY_MODE_VALUES = ['api', 'account'];
|
||||
const DEFAULT_IP_PROXY_PROTOCOL = 'http';
|
||||
const IP_PROXY_PROTOCOL_VALUES = ['http', 'https', 'socks4', 'socks5'];
|
||||
const IP_PROXY_FETCH_TIMEOUT_MS = 20000;
|
||||
const IP_PROXY_SETTINGS_SCOPE = 'regular';
|
||||
const IP_PROXY_BYPASS_LIST = ['<local>', 'localhost', '127.0.0.1'];
|
||||
const IP_PROXY_ROUTE_ALL_TRAFFIC = true;
|
||||
const IP_PROXY_ACCOUNT_LIST_ENABLED = ${accountListEnabled ? 'true' : 'false'};
|
||||
const IP_PROXY_TARGET_HOST_PATTERNS = [
|
||||
'openai.com',
|
||||
'*.openai.com',
|
||||
'chatgpt.com',
|
||||
'*.chatgpt.com',
|
||||
];
|
||||
${providerSource}
|
||||
const transformIpProxyAccountEntryByProvider = self.transformIpProxyAccountEntryByProvider;
|
||||
${coreSource}
|
||||
return {
|
||||
buildIpProxyPacScript,
|
||||
getAccountModeProxyPoolFromState,
|
||||
normalizeIpProxyAccountList,
|
||||
normalizeProxyPoolEntries,
|
||||
parseIpProxyLine,
|
||||
resolveIpProxyAutoSwitchThreshold,
|
||||
};
|
||||
`)();
|
||||
}
|
||||
|
||||
test('IP proxy parser ignores disabled lines and normalizes proxy entries', () => {
|
||||
const api = loadIpProxyCore();
|
||||
|
||||
assert.equal(
|
||||
api.normalizeIpProxyAccountList([
|
||||
'# disabled',
|
||||
' // disabled',
|
||||
'; disabled',
|
||||
'global.rotgb.711proxy.com:10000:user:pass',
|
||||
'',
|
||||
].join('\n')),
|
||||
'global.rotgb.711proxy.com:10000:user:pass'
|
||||
);
|
||||
|
||||
const pool = api.normalizeProxyPoolEntries([
|
||||
'http://global.rotgb.711proxy.com:10000:user:pa:ss',
|
||||
'http://global.rotgb.711proxy.com:10000:user:pa:ss',
|
||||
{ host: 'us.proxy.example', port: '8080', username: 'u2', password: 'p2' },
|
||||
]);
|
||||
|
||||
assert.equal(pool.length, 2);
|
||||
assert.deepStrictEqual(pool[0], {
|
||||
host: 'global.rotgb.711proxy.com',
|
||||
port: 10000,
|
||||
username: 'user',
|
||||
password: 'pa:ss',
|
||||
protocol: 'http',
|
||||
region: '',
|
||||
provider: '711proxy',
|
||||
});
|
||||
assert.equal(pool[1].host, 'us.proxy.example');
|
||||
assert.equal(pool[1].port, 8080);
|
||||
});
|
||||
|
||||
test('711 fixed-account mode applies region and sticky session parameters', () => {
|
||||
const api = loadIpProxyCore();
|
||||
const pool = api.getAccountModeProxyPoolFromState({
|
||||
ipProxyService: '711proxy',
|
||||
ipProxyMode: 'account',
|
||||
ipProxyHost: 'global.rotgb.711proxy.com',
|
||||
ipProxyPort: '10000',
|
||||
ipProxyProtocol: 'http',
|
||||
ipProxyUsername: 'USER047152-zone-custom',
|
||||
ipProxyPassword: 'secret',
|
||||
ipProxyRegion: 'US',
|
||||
ipProxyAccountSessionPrefix: 'sticky_001',
|
||||
ipProxyAccountLifeMinutes: '30',
|
||||
});
|
||||
|
||||
assert.equal(pool.length, 1);
|
||||
assert.equal(pool[0].host, 'global.rotgb.711proxy.com');
|
||||
assert.equal(pool[0].port, 10000);
|
||||
assert.equal(pool[0].region, 'US');
|
||||
assert.match(pool[0].username, /region-US/);
|
||||
assert.match(pool[0].username, /session-sticky_001/);
|
||||
assert.match(pool[0].username, /sessTime-30/);
|
||||
});
|
||||
|
||||
test('IP proxy PAC keeps local traffic direct and routes target traffic through proxy', () => {
|
||||
const api = loadIpProxyCore();
|
||||
const pac = api.buildIpProxyPacScript({
|
||||
host: 'global.rotgb.711proxy.com',
|
||||
port: 10000,
|
||||
protocol: 'http',
|
||||
});
|
||||
|
||||
assert.match(pac, /FindProxyForURL/);
|
||||
assert.match(pac, /localhost/);
|
||||
assert.match(pac, /PROXY global\.rotgb\.711proxy\.com:10000/);
|
||||
assert.match(pac, /chatgpt\.com/);
|
||||
assert.match(pac, /openai\.com/);
|
||||
});
|
||||
|
||||
test('sidepanel loads IP proxy scripts before sidepanel bootstrap', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const providerIndex = html.indexOf('<script src="ip-proxy-provider-711proxy.js"></script>');
|
||||
const panelIndex = html.indexOf('<script src="ip-proxy-panel.js"></script>');
|
||||
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||
|
||||
assert.notEqual(providerIndex, -1);
|
||||
assert.notEqual(panelIndex, -1);
|
||||
assert.notEqual(sidepanelIndex, -1);
|
||||
assert.ok(providerIndex < panelIndex);
|
||||
assert.ok(panelIndex < sidepanelIndex);
|
||||
});
|
||||
|
||||
test('IP proxy auto-switch threshold is clamped to the supported range', () => {
|
||||
const api = loadIpProxyCore();
|
||||
|
||||
assert.equal(api.resolveIpProxyAutoSwitchThreshold({ ipProxyPoolTargetCount: '0' }), 1);
|
||||
assert.equal(api.resolveIpProxyAutoSwitchThreshold({ ipProxyPoolTargetCount: '25' }), 25);
|
||||
assert.equal(api.resolveIpProxyAutoSwitchThreshold({ ipProxyPoolTargetCount: '9999' }), 500);
|
||||
});
|
||||
@@ -159,6 +159,236 @@ test('Plus login-code step reuses step 8 verification logic but completes visibl
|
||||
assert.deepStrictEqual(remainingStepCalls, [11, 11]);
|
||||
});
|
||||
|
||||
test('step 8 completes directly when auth page is already on OAuth consent page', async () => {
|
||||
const events = {
|
||||
resolveCalls: 0,
|
||||
completeCalls: [],
|
||||
setStates: [],
|
||||
rerunStep7: 0,
|
||||
};
|
||||
|
||||
const executor = api.createStep8Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
events.completeCalls.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => ({
|
||||
state: 'oauth_consent_page',
|
||||
}),
|
||||
rerunStep7ForStep8Recovery: async () => {
|
||||
events.rerunStep7 += 1;
|
||||
},
|
||||
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' }),
|
||||
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
isVerificationMailPollingError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
resolveVerificationStep: async () => {
|
||||
events.resolveCalls += 1;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
setState: async (payload) => {
|
||||
events.setStates.push(payload);
|
||||
},
|
||||
setStepStatus: async () => {},
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
|
||||
assert.equal(events.resolveCalls, 0);
|
||||
assert.equal(events.rerunStep7, 0);
|
||||
assert.deepStrictEqual(events.setStates, [
|
||||
{
|
||||
step8VerificationTargetEmail: '',
|
||||
loginVerificationRequestedAt: null,
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(events.completeCalls, [
|
||||
{
|
||||
step: 8,
|
||||
payload: {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 8 retries in-place when polling fails but auth page still stays on verification page', async () => {
|
||||
const events = {
|
||||
ensureCalls: 0,
|
||||
resolveCalls: 0,
|
||||
rerunStep7: 0,
|
||||
};
|
||||
const pageStates = [
|
||||
{ state: 'verification_page', displayedEmail: 'user@example.com' },
|
||||
{ state: 'verification_page', displayedEmail: 'user@example.com' },
|
||||
{ state: 'verification_page', displayedEmail: 'user@example.com' },
|
||||
];
|
||||
|
||||
const executor = api.createStep8Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => {
|
||||
events.ensureCalls += 1;
|
||||
return pageStates[Math.min(events.ensureCalls - 1, pageStates.length - 1)];
|
||||
},
|
||||
rerunStep7ForStep8Recovery: async () => {
|
||||
events.rerunStep7 += 1;
|
||||
},
|
||||
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' }),
|
||||
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 () => {
|
||||
events.resolveCalls += 1;
|
||||
if (events.resolveCalls === 1) {
|
||||
throw new Error('步骤 8:页面通信异常 did not respond in 30s');
|
||||
}
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
|
||||
assert.equal(events.resolveCalls, 2);
|
||||
assert.equal(events.rerunStep7, 0);
|
||||
assert.equal(events.ensureCalls >= 3, true);
|
||||
});
|
||||
|
||||
test('step 8 completes when polling fails but recovery probe shows oauth consent page', async () => {
|
||||
const events = {
|
||||
ensureCalls: 0,
|
||||
resolveCalls: 0,
|
||||
rerunStep7: 0,
|
||||
completeCalls: [],
|
||||
};
|
||||
const pageStates = [
|
||||
{ state: 'verification_page', displayedEmail: 'user@example.com' },
|
||||
{ state: 'oauth_consent_page' },
|
||||
];
|
||||
|
||||
const executor = api.createStep8Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
events.completeCalls.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => {
|
||||
events.ensureCalls += 1;
|
||||
return pageStates[Math.min(events.ensureCalls - 1, pageStates.length - 1)];
|
||||
},
|
||||
rerunStep7ForStep8Recovery: async () => {
|
||||
events.rerunStep7 += 1;
|
||||
},
|
||||
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' }),
|
||||
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 () => {
|
||||
events.resolveCalls += 1;
|
||||
throw new Error('步骤 8:页面通信异常 did not respond in 30s');
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
|
||||
assert.equal(events.resolveCalls, 1);
|
||||
assert.equal(events.rerunStep7, 0);
|
||||
assert.deepStrictEqual(events.completeCalls, [
|
||||
{
|
||||
step: 8,
|
||||
payload: {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
|
||||
let capturedOptions = null;
|
||||
let ensureCalls = 0;
|
||||
|
||||
@@ -132,6 +132,115 @@ return {
|
||||
);
|
||||
});
|
||||
|
||||
test('step 8 ready check keeps waiting when retryPage is reported on consent URL', async () => {
|
||||
const api = new Function(`
|
||||
let pollCount = 0;
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleepWithStop() {}
|
||||
async function ensureStep8SignupPageReady() {}
|
||||
async function getState() {
|
||||
return { phoneVerificationEnabled: true };
|
||||
}
|
||||
const phoneVerificationHelpers = {
|
||||
async completePhoneVerificationFlow() {
|
||||
throw new Error('should not trigger phone verification flow');
|
||||
},
|
||||
};
|
||||
async function getStep8PageState() {
|
||||
pollCount += 1;
|
||||
if (pollCount === 1) {
|
||||
return {
|
||||
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
retryPage: true,
|
||||
consentPage: true,
|
||||
consentReady: false,
|
||||
addPhonePage: false,
|
||||
phoneVerificationPage: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
retryPage: false,
|
||||
consentPage: true,
|
||||
consentReady: true,
|
||||
addPhonePage: false,
|
||||
phoneVerificationPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
${extractFunction('waitForStep8Ready')}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
return waitForStep8Ready(88, 1000);
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
assert.equal(result?.consentReady, true);
|
||||
assert.equal(result?.retryPage, false);
|
||||
});
|
||||
|
||||
test('step 8 click effect ignores consent-like retry snapshots and waits for real page progress', async () => {
|
||||
const api = new Function(`
|
||||
let pollCount = 0;
|
||||
const chrome = {
|
||||
tabs: {
|
||||
async get() {
|
||||
return {
|
||||
id: 88,
|
||||
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleepWithStop() {}
|
||||
async function ensureStep8SignupPageReady() {}
|
||||
async function getStep8PageState() {
|
||||
pollCount += 1;
|
||||
if (pollCount === 1) {
|
||||
return {
|
||||
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
retryPage: true,
|
||||
consentPage: true,
|
||||
consentReady: false,
|
||||
addPhonePage: false,
|
||||
verificationPage: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
url: 'https://chatgpt.com/',
|
||||
retryPage: false,
|
||||
consentPage: false,
|
||||
consentReady: false,
|
||||
addPhonePage: false,
|
||||
verificationPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
${extractFunction('waitForStep8ClickEffect')}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
return waitForStep8ClickEffect(
|
||||
88,
|
||||
'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
1000
|
||||
);
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
assert.equal(result?.progressed, true);
|
||||
assert.equal(result?.reason, 'left_consent_page');
|
||||
assert.match(String(result?.url || ''), /chatgpt\.com/);
|
||||
});
|
||||
|
||||
test('step 8 ready check completes phone verification flow before waiting for OAuth consent', async () => {
|
||||
const api = new Function(`
|
||||
let pollCount = 0;
|
||||
|
||||
@@ -419,6 +419,79 @@ test('verification flow completes step 8 and flags phone verification when add-p
|
||||
]);
|
||||
});
|
||||
|
||||
test('verification flow keeps step 8 successful when code submit transport fails but auth page already reaches oauth consent', async () => {
|
||||
const events = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async (message) => {
|
||||
events.push(['log', message]);
|
||||
},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (_step, payload) => {
|
||||
events.push(['complete', payload.code]);
|
||||
},
|
||||
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 () => ({}),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'FILL_CODE') {
|
||||
throw new Error('message channel is closed before a response was received');
|
||||
}
|
||||
if (message.type === 'GET_LOGIN_AUTH_STATE') {
|
||||
return {
|
||||
state: 'oauth_consent_page',
|
||||
url: 'https://auth.openai.com/authorize?client_id=test',
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
sendToMailContentScriptResilient: async () => ({
|
||||
code: '654321',
|
||||
emailTimestamp: 123,
|
||||
}),
|
||||
setState: async (payload) => {
|
||||
events.push(['state', payload.lastLoginCode || payload.lastSignupCode]);
|
||||
},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
const result = await helpers.resolveVerificationStep(
|
||||
8,
|
||||
{ email: 'user@example.com', lastLoginCode: null },
|
||||
{ provider: 'qq', label: 'QQ Mail' },
|
||||
{}
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
phoneVerificationRequired: false,
|
||||
url: 'https://auth.openai.com/authorize?client_id=test',
|
||||
});
|
||||
assert.deepStrictEqual(events.filter((entry) => entry[0] !== 'log'), [
|
||||
['state', '654321'],
|
||||
['complete', '654321'],
|
||||
]);
|
||||
assert.ok(events.some((entry) => entry[0] === 'log' && /通信中断/.test(entry[1])));
|
||||
});
|
||||
|
||||
test('verification flow treats manual step 8 add-phone confirmation as the same fatal add-phone error', async () => {
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
|
||||
+67
-1
@@ -141,6 +141,7 @@
|
||||
- localhost 回调地址
|
||||
- 自动运行轮次信息
|
||||
- 当前自动运行 session 标识 `autoRunSessionId`
|
||||
- IP 代理运行态:`ipProxyApiPool / ipProxyAccountPool / ipProxyCurrent / ipProxyApplied*`,用于记录当前代理池、当前代理节点、PAC 接管结果、出口检测结果和诊断信息
|
||||
- 标签注册表
|
||||
- 最近打开的来源地址
|
||||
- LuckMail 当前运行时选择
|
||||
@@ -156,6 +157,9 @@
|
||||
|
||||
- CPA / SUB2API 配置
|
||||
- Codex2API 配置
|
||||
- IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值
|
||||
- Plus 模式开关 `plusModeEnabled`
|
||||
- PayPal 登录配置 `paypalEmail / paypalPassword`
|
||||
- 邮箱 provider 配置
|
||||
- Hotmail 账号池
|
||||
- 2925 账号池
|
||||
@@ -188,6 +192,8 @@
|
||||
- `ICLOUD_LOGIN_REQUIRED`
|
||||
- `ICLOUD_ALIASES_CHANGED`
|
||||
|
||||
IP 代理模块在同步、切换、Change、出口探测和自动运行成功阈值切换后,会通过 `DATA_UPDATED` 广播 `ipProxyApplied*` 与当前代理池字段,sidepanel 只根据广播更新状态卡,不在 UI 层重新推断后台代理结果。
|
||||
|
||||
### 4.4 贡献模式链路
|
||||
|
||||
贡献模式的目标不是新增一个 provider,而是在 sidepanel 内开启一套“贡献账号”专用 UI,并把公开贡献服务并进原有 10 步主链:
|
||||
@@ -701,6 +707,65 @@ Codex2API 补充:
|
||||
- [icloud-utils.js](c:/Users/projectf/Downloads/codex注册扩展/icloud-utils.js)
|
||||
- [content/icloud-mail.js](c:/Users/projectf/Downloads/codex注册扩展/content/icloud-mail.js)
|
||||
|
||||
配置:
|
||||
|
||||
- `icloudHostPreference` 只决定 iCloud 登录、别名管理和 iCloud Mail 收件箱 Host。
|
||||
- `icloudFetchMode` 决定生成注册邮箱时复用已有 Hide My Email 别名,还是始终创建新别名。
|
||||
- `icloudTargetMailboxType = icloud-inbox` 时,Step 4 / Step 8 直接打开 iCloud Mail 收件箱轮询验证码。
|
||||
- `icloudTargetMailboxType = forward-mailbox` 时,注册邮箱仍由 iCloud Hide My Email 生成,但 Step 4 / Step 8 改为打开 `icloudForwardMailProvider` 指定的转发目标邮箱收码;当前支持 `qq / 163 / 163-vip / 126 / gmail`。
|
||||
|
||||
Hide My Email 获取与管理链路:
|
||||
|
||||
1. 后台优先通过 `setup.icloud.com*/setup/ws/1/validate` 解析 `premiummailsettings.url`,并记录最近可用服务节点。
|
||||
2. 如果后台请求受 401 / 403 / 409 / 421 / 429 / 5xx、CORS、超时或地址空间限制影响,会优先尝试在已打开或自动创建的 `www.icloud.com*/mail/` 页面主上下文中重放请求。
|
||||
3. `maildomainws` 请求会补充 `clientBuildNumber / clientMasteringNumber / clientId / dsid` 查询参数,写请求使用 `text/plain;charset=UTF-8`,以兼容 iCloud 网页端接口。
|
||||
4. 别名列表加载成功后会缓存到 `icloudAliasCache`;遇到短暂上下文波动时,列表展示会按“最近缓存 -> 历史缓存 -> 本地已用/保留记录”回退。
|
||||
5. 生成新别名后若 `reserve` 返回鉴权或网络异常,会先回查列表确认候选别名是否已经创建;必要时刷新服务节点重试一次,最后才进入可复用别名回退或暂停等待用户处理。
|
||||
6. 自动运行获取 iCloud 别名时,检测到会话或网络上下文异常后会停止重复消耗获取重试次数,转入等待邮箱状态,避免连续请求放大 iCloud 风控或上下文抖动。
|
||||
|
||||
维护约定:
|
||||
|
||||
- iCloud 转发目标邮箱 provider 的 label、URL、source、inject 配置统一由 `mail-provider-utils.js` 输出。
|
||||
- `background.js` 只根据 `icloudTargetMailboxType` 选择 iCloud Mail 收件箱或共享转发邮箱配置,不再在主文件内硬编码各 provider 地址。
|
||||
- `sidepanel/sidepanel.js` 只负责展示、保存和回显目标邮箱类型 / 转发邮箱 provider,不重复定义 provider 业务清单。
|
||||
- iCloud 登录态提示必须区分“真实未登录”和“已登录但请求上下文波动”;不能把所有 401 / 403 / 421 都直接提示为未登录。
|
||||
- iCloud 别名缓存只用于短暂失败回退,不应改变最终的已用 / 保留状态来源;状态仍以 `manualAliasUsage`、`preservedAliases` 与最新线上列表合并后的结果为准。
|
||||
|
||||
### 7.7 IP Proxy
|
||||
|
||||
组成:
|
||||
|
||||
- [background/ip-proxy-core.js](c:/Users/projectf/Downloads/codex注册扩展/background/ip-proxy-core.js)
|
||||
- [background/ip-proxy-provider-711proxy.js](c:/Users/projectf/Downloads/codex注册扩展/background/ip-proxy-provider-711proxy.js)
|
||||
- [sidepanel/ip-proxy-panel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/ip-proxy-panel.js)
|
||||
- [sidepanel/ip-proxy-provider-711proxy.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/ip-proxy-provider-711proxy.js)
|
||||
- [docs/ip-proxy-module.md](c:/Users/projectf/Downloads/codex注册扩展/docs/ip-proxy-module.md)
|
||||
|
||||
职责边界:
|
||||
|
||||
- `background/ip-proxy-core.js` 负责代理条目解析、运行态代理池、PAC 应用与清除、`webRequest.onAuthRequired` 代理鉴权回填、出口探测和失败时的目标站点 fail-close 规则。
|
||||
- `background/ip-proxy-provider-711proxy.js` 只承接 711Proxy 的账号串参数规则,负责把 `region / session / sessTime` 叠加到最终用户名。
|
||||
- `background/message-router.js` 只暴露 `REFRESH_IP_PROXY_POOL / SWITCH_IP_PROXY / CHANGE_IP_PROXY_EXIT / PROBE_IP_PROXY_EXIT` 消息,不在路由层复制代理解析逻辑。
|
||||
- `sidepanel/ip-proxy-panel.js` 负责 UI 状态渲染、表单快照、按钮动作和 711 参数输入联动;`sidepanel/sidepanel.js` 只保留配置保存、事件绑定和广播接线。
|
||||
|
||||
基础链路:
|
||||
|
||||
1. 用户在 sidepanel 打开 `IP代理`,选择服务和账号模式。
|
||||
2. 首版只开放 `711proxy + account` 主路径;API 模式和账号列表入口保留但默认禁用。
|
||||
3. 用户填写固定账号的 Host / Port / Protocol / Username / Password,可选填写地区、session 和时长。
|
||||
4. 点击 `同步` 后,sidepanel 发送 `REFRESH_IP_PROXY_POOL`,后台解析当前账号为代理池并调用 `chrome.proxy.settings.set` 写入 PAC。
|
||||
5. 如果代理需要账号密码,后台通过 `webRequest.onAuthRequired` 对代理 407 challenge 回填用户名和密码。
|
||||
6. PAC 成功应用后,后台关闭 fail-close 规则;如果配置缺失、应用失败或出口探测失败,则开启只覆盖 OpenAI / ChatGPT 目标域的阻断规则,避免误以为已经走代理。
|
||||
7. `检测出口` 会优先用页面上下文探测公网 IP,再用后台请求兜底,并把出口 IP、地区、探测来源和诊断摘要写回运行态。
|
||||
8. `下一条` 根据当前代理池 index 切换节点;`Change` 在 711 session 场景下强制重绑代理链路,用于刷新出口。
|
||||
9. 自动运行成功轮数命中 `ipProxyPoolTargetCount` 时,后台会尝试自动切到下一条代理;只有代理候选数量大于 1 时才执行切换。
|
||||
|
||||
维护约定:
|
||||
|
||||
- 代理解析、PAC、鉴权和出口检测必须留在后台模块,不应继续堆进 `background.js`。
|
||||
- UI 层不能凭输入值直接判断“代理已接管”,只能展示后台返回的 `ipProxyApplied*` 状态。
|
||||
- 新增代理服务商时,应优先新增 provider 规则模块,并让共享解析/运行态继续走 `background/ip-proxy-core.js`。
|
||||
- 修改代理字段、权限或链路时,需要同步更新 [docs/ip-proxy-module.md](c:/Users/projectf/Downloads/codex注册扩展/docs/ip-proxy-module.md)、当前完整链路说明和结构说明。
|
||||
## 8. 自动运行完整链路
|
||||
|
||||
文件:
|
||||
@@ -729,7 +794,8 @@ Codex2API 补充:
|
||||
- 手动停止与自动停止会写入“停止”状态(若后续同邮箱成功/失败,会被覆盖为最新状态)
|
||||
8. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId`
|
||||
9. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起”
|
||||
10. 所有轮次结束后输出汇总,并清空当前 session 标识
|
||||
10. 如果启用 IP 代理,后台会在每轮成功后按 `ipProxyPoolTargetCount` 检查是否需要切换出口;候选代理不足时只记录日志并跳过切换
|
||||
11. 所有轮次结束后输出汇总,并清空当前 session 标识
|
||||
|
||||
## 9. 新增功能时最容易漏掉的地方
|
||||
|
||||
|
||||
+14
-9
@@ -29,7 +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 模式下参与别名生成”的共享判定。
|
||||
- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。
|
||||
- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集;当前额外声明 `proxy / webRequest / webRequestAuthProvider`,用于 IP 代理 PAC 接管和代理鉴权回填。
|
||||
- `microsoft-email.js`:Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。
|
||||
- `package.json`:仓库最小 Node 包配置,目前主要提供测试脚本定义。
|
||||
- `rules.json`:静态 DNR 规则,主要处理 iCloud 相关请求头。
|
||||
@@ -46,15 +46,17 @@
|
||||
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail`、`mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失关键持久配置。
|
||||
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
|
||||
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
|
||||
- `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。
|
||||
- `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 账号池的新增、导入、切换、登录、禁用与删除消息。
|
||||
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息。
|
||||
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
|
||||
- `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 模式下的号池时,会先确保账号池中已选中可用账号。
|
||||
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
|
||||
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给步骤 9,而不是误判为普通失败;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
|
||||
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给步骤 9,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
|
||||
|
||||
## `background/steps/`
|
||||
|
||||
@@ -102,6 +104,7 @@
|
||||
## `docs/`
|
||||
|
||||
- `docs/仓库协作者AI分析PR与合并标准流程.md`:仓库协作者进行 AI 分析 PR 与合并时的流程说明。
|
||||
- `docs/ip-proxy-module.md`:IP Proxy 模块说明,记录模块定位、后台/侧栏代码结构、当前功能范围、711Proxy 参数联动、操作方式、发布策略与已知限制。
|
||||
- `docs/使用教程.md`:面向使用者的补充教程文档,当前说明扩展更新流程与 Clash Verge 的“非港轮询”配置步骤。
|
||||
- `docs/images/十轮自动.png`:README 中展示的自动运行效果图。
|
||||
- `docs/images/微信.png`:README 中展示的微信收款码图片。
|
||||
@@ -130,8 +133,10 @@
|
||||
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
|
||||
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
|
||||
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
|
||||
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。
|
||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡
|
||||
- `sidepanel/ip-proxy-panel.js`:侧边栏 IP 代理面板逻辑,负责代理配置显隐、服务/模式切换、账号串参数同步、状态卡渲染,以及同步、下一条、Change、检测出口、检查 IP 等按钮行为。
|
||||
- `sidepanel/ip-proxy-provider-711proxy.js`:侧边栏 711Proxy 输入辅助逻辑,负责从 host、username、region 输入中推断和归一化国家/地区参数。
|
||||
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行、统一的收起态列表高度样式,以及 IP 代理配置区、状态卡和操作按钮样式。
|
||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡
|
||||
献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内
|
||||
展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“接码”开关与“验证码重发”拆成同一行展示;只有开启接码后,下面的 HeroSMS 平
|
||||
台、国家与 API Key 行才会显示;页面继续加载 `managed-alias-utils.js` 与 `mail-provider-utils.js`,并把旧的“邮箱前缀”字段语义改为“别
|
||||
@@ -139,9 +144,8 @@
|
||||
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` 时
|
||||
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
|
||||
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
|
||||
钥配置行;设置卡片新增 `Plus 模式` 开关与 PayPal 账号/密码输入行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添
|
||||
加”按钮和共享表单容器。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
|
||||
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;设置卡片新增 `Plus 模式` 开关与 PayPal 账号/密码输入行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
|
||||
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
|
||||
动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接
|
||||
到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显
|
||||
@@ -152,7 +156,7 @@
|
||||
据配置;Step 8 的自定义邮箱确认弹窗当前额外提供“出现手机号验证”按钮,用于直接走与真实 add-phone 一致的 fatal 分支;HeroSMS 国家列表
|
||||
会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只
|
||||
要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;
|
||||
Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站
|
||||
Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;IP 代理配置保存、服务配置快照、开关应用和运行态回显由这里接入消息路由,具体解析/状态渲染下沉到 `sidepanel/ip-proxy-panel.js`;侧边栏初始化与点击“自动”前会刷新一次贡献站
|
||||
公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示。
|
||||
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Ultra` / 历史 `Pro` / legacy `v` 版本族排序、缓存读取与版本展示。
|
||||
|
||||
@@ -176,6 +180,7 @@
|
||||
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
|
||||
- `tests/background-icloud-login-help.test.js`:测试 iCloud 登录判定、页面上下文回退、别名缓存回退和保留异常恢复的关键源码约束。
|
||||
- `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析,并覆盖转发收码模式复用共享 provider 配置。
|
||||
- `tests/background-ip-proxy-core.test.js`:测试 IP 代理核心解析与接入约束,覆盖注释行过滤、代理条目归一化、711 固定账号参数叠加、PAC 生成、侧栏脚本加载顺序和自动切换阈值钳制。
|
||||
- `tests/background-logging-status-module.test.js`:测试日志 / 状态模块已接入且导出工厂。
|
||||
- `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。
|
||||
- `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。
|
||||
|
||||
Reference in New Issue
Block a user