Merge remote-tracking branch 'origin/dev' into master-sync
This commit is contained in:
@@ -132,7 +132,15 @@
|
||||
4. Step 1 会直接在 SUB2API 后台生成 OAuth 链接
|
||||
5. Step 10 会把 localhost 回调提交回 SUB2API,并直接创建 OpenAI 账号
|
||||
|
||||
### 方案 C:`Hotmail 账号池`
|
||||
### 方案 C:`Codex2API + QQ / 163 / 163 VIP`
|
||||
|
||||
1. `来源` 选择 `Codex2API`
|
||||
2. 填好 `Codex2API` 后台地址、管理密钥
|
||||
3. `Mail` 与 `邮箱生成` 的配置方式同方案 A
|
||||
4. Step 7 会直接通过 Codex2API 协议 `/api/admin/oauth/generate-auth-url` 生成 OAuth 链接
|
||||
5. Step 10 会把 localhost 回调中的 `code / state` 通过 `/api/admin/oauth/exchange-code` 直接提交给 Codex2API
|
||||
|
||||
### 方案 D:`Hotmail 账号池`
|
||||
|
||||
1. `Mail` 选择 `Hotmail`
|
||||
2. 在 `Hotmail 账号池` 中添加 `邮箱 / Client ID / Refresh Token`
|
||||
@@ -140,7 +148,7 @@
|
||||
4. 通过后再执行步骤或 `Auto`
|
||||
5. 当前项目中,`Mail = Hotmail` 时会直接使用账号池里的邮箱作为注册邮箱,不再走 `Duck / Cloudflare` 自动生成
|
||||
|
||||
### 方案 D:`2925 账号池`
|
||||
### 方案 E:`2925 账号池`
|
||||
|
||||
1. `Mail` 选择 `2925`
|
||||
2. 在 `2925 账号池` 中添加 `邮箱 / 密码`
|
||||
@@ -177,6 +185,20 @@ Step 1 和 Step 10 都依赖这个地址。
|
||||
|
||||
插件会在 Step 1 和 Step 10 自动从 `/api/v1/admin/proxies/all` 解析这个代理,并在 OAuth 链接生成、授权码交换和账号创建请求中附带 `proxy_id`。如果名称匹配到多个代理,请改填代理 ID;留空则不会发送 `proxy_id`。
|
||||
|
||||
### `Codex2API`
|
||||
|
||||
当 `来源 = Codex2API` 时,需要配置:
|
||||
|
||||
- `Codex2API`:后台账号管理页地址,默认 `http://localhost:8080/admin/accounts`
|
||||
- `管理密钥`:Codex2API 的 `Admin Secret`
|
||||
|
||||
插件会在:
|
||||
|
||||
- Step 7 调用 `POST /api/admin/oauth/generate-auth-url` 生成授权链接
|
||||
- Step 10 调用 `POST /api/admin/oauth/exchange-code` 完成 localhost callback 的授权码交换并创建账号
|
||||
|
||||
这条来源是协议直连,不依赖 Codex2API 后台页面的“添加账号 / OAuth 授权 / 生成授权链接”按钮 DOM。
|
||||
|
||||
### `Mail`
|
||||
|
||||
支持五种验证码来源:
|
||||
|
||||
+61
-6
@@ -156,6 +156,7 @@ const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
|
||||
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
|
||||
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
|
||||
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';
|
||||
@@ -252,6 +253,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
sub2apiPassword: '',
|
||||
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME,
|
||||
sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME,
|
||||
codex2apiUrl: DEFAULT_CODEX2API_URL,
|
||||
codex2apiAdminKey: '',
|
||||
customPassword: '',
|
||||
autoRunSkipFailures: false,
|
||||
autoRunFallbackThreadIntervalMinutes: 0,
|
||||
@@ -282,6 +285,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
cloudflareTempEmailAdminAuth: '',
|
||||
cloudflareTempEmailCustomAuth: '',
|
||||
cloudflareTempEmailReceiveMailbox: '',
|
||||
cloudflareTempEmailUseRandomSubdomain: false,
|
||||
cloudflareTempEmailDomain: '',
|
||||
cloudflareTempEmailDomains: [],
|
||||
hotmailAccounts: [],
|
||||
@@ -329,6 +333,8 @@ const DEFAULT_STATE = {
|
||||
sub2apiGroupId: null, // SUB2API 目标分组 ID。
|
||||
sub2apiDraftName: null, // SUB2API 本轮预生成的账号名称。
|
||||
sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。
|
||||
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
|
||||
codex2apiOAuthState: null, // Codex2API OAuth state。
|
||||
flowStartTime: null, // 当前流程开始时间。
|
||||
tabRegistry: {}, // 程序维护的标签页注册表。
|
||||
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
|
||||
@@ -653,7 +659,14 @@ function normalizeEmailGenerator(value = '') {
|
||||
}
|
||||
|
||||
function normalizePanelMode(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa';
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'sub2api') {
|
||||
return 'sub2api';
|
||||
}
|
||||
if (normalized === 'codex2api') {
|
||||
return 'codex2api';
|
||||
}
|
||||
return 'cpa';
|
||||
}
|
||||
|
||||
function normalizeMailProvider(value = '') {
|
||||
@@ -829,6 +842,7 @@ function getCloudflareTempEmailConfig(state = {}) {
|
||||
adminAuth: String(state.cloudflareTempEmailAdminAuth || ''),
|
||||
customAuth: String(state.cloudflareTempEmailCustomAuth || ''),
|
||||
receiveMailbox: normalizeCloudflareTempEmailReceiveMailbox(state.cloudflareTempEmailReceiveMailbox),
|
||||
useRandomSubdomain: Boolean(state.cloudflareTempEmailUseRandomSubdomain),
|
||||
domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain),
|
||||
domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains),
|
||||
};
|
||||
@@ -874,6 +888,10 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return String(value || '').trim();
|
||||
case 'sub2apiDefaultProxyName':
|
||||
return String(value || '').trim();
|
||||
case 'codex2apiUrl':
|
||||
return normalizeCodex2ApiUrl(value);
|
||||
case 'codex2apiAdminKey':
|
||||
return String(value || '').trim();
|
||||
case 'customPassword':
|
||||
return String(value || '');
|
||||
case 'autoRunSkipFailures':
|
||||
@@ -897,6 +915,7 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeEmailGenerator(value);
|
||||
case 'autoDeleteUsedIcloudAlias':
|
||||
case 'accountRunHistoryTextEnabled':
|
||||
case 'cloudflareTempEmailUseRandomSubdomain':
|
||||
return Boolean(value);
|
||||
case 'icloudHostPreference':
|
||||
return normalizeIcloudHost(value) || 'auto';
|
||||
@@ -3623,11 +3642,31 @@ function normalizeSub2ApiUrl(rawUrl) {
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function normalizeCodex2ApiUrl(rawUrl) {
|
||||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.normalizeCodex2ApiUrl) {
|
||||
return navigationUtils.normalizeCodex2ApiUrl(rawUrl);
|
||||
}
|
||||
const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL;
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') {
|
||||
parsed.pathname = '/admin/accounts';
|
||||
}
|
||||
parsed.hash = '';
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function getPanelMode(state = {}) {
|
||||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.getPanelMode) {
|
||||
return navigationUtils.getPanelMode(state);
|
||||
}
|
||||
return state.panelMode === 'sub2api' ? 'sub2api' : 'cpa';
|
||||
if (state.panelMode === 'sub2api') {
|
||||
return 'sub2api';
|
||||
}
|
||||
if (state.panelMode === 'codex2api') {
|
||||
return 'codex2api';
|
||||
}
|
||||
return 'cpa';
|
||||
}
|
||||
|
||||
function getPanelModeLabel(modeOrState) {
|
||||
@@ -3635,7 +3674,13 @@ function getPanelModeLabel(modeOrState) {
|
||||
return navigationUtils.getPanelModeLabel(modeOrState);
|
||||
}
|
||||
const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState);
|
||||
return mode === 'sub2api' ? 'SUB2API' : 'CPA';
|
||||
if (mode === 'sub2api') {
|
||||
return 'SUB2API';
|
||||
}
|
||||
if (mode === 'codex2api') {
|
||||
return 'Codex2API';
|
||||
}
|
||||
return 'CPA';
|
||||
}
|
||||
|
||||
function isSignupPageHost(hostname = '') {
|
||||
@@ -3939,6 +3984,7 @@ function isRetryableContentScriptTransportError(error) {
|
||||
}
|
||||
|
||||
const navigationUtils = self.MultiPageBackgroundNavigationUtils?.createNavigationUtils({
|
||||
DEFAULT_CODEX2API_URL,
|
||||
DEFAULT_SUB2API_URL,
|
||||
normalizeLocalCpaStep9Mode,
|
||||
});
|
||||
@@ -4132,6 +4178,8 @@ function getDownstreamStateResets(step) {
|
||||
sub2apiOAuthState: null,
|
||||
sub2apiGroupId: null,
|
||||
sub2apiDraftName: null,
|
||||
codex2apiSessionId: null,
|
||||
codex2apiOAuthState: null,
|
||||
flowStartTime: null,
|
||||
password: null,
|
||||
lastEmailTimestamp: null,
|
||||
@@ -4917,6 +4965,8 @@ async function handleStepData(step, payload) {
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||||
if (Object.keys(updates).length) {
|
||||
await setState(updates);
|
||||
}
|
||||
@@ -5448,6 +5498,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
upsertMail2925AccountInList,
|
||||
waitForTabComplete,
|
||||
waitForTabUrlMatch,
|
||||
});
|
||||
|
||||
@@ -6184,6 +6235,7 @@ const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
|
||||
closeConflictingTabsForSource,
|
||||
ensureContentScriptReadyOnTab,
|
||||
getPanelMode,
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
rememberSourceLastUrl,
|
||||
sendToContentScript,
|
||||
@@ -6359,6 +6411,7 @@ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
|
||||
getTabId,
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isTabAlive,
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
rememberSourceLastUrl,
|
||||
reuseOrCreateTab,
|
||||
@@ -6425,6 +6478,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
getPendingAutoRunTimerPlan,
|
||||
getSourceLabel,
|
||||
getState,
|
||||
getTabId,
|
||||
getStopRequested: () => stopRequested,
|
||||
handleCloudflareSecurityBlocked,
|
||||
handleAutoRunLoopUnhandledError,
|
||||
@@ -6436,6 +6490,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isLuckmailProvider,
|
||||
isStopError,
|
||||
isTabAlive,
|
||||
launchAutoRunTimerPlan,
|
||||
listIcloudAliases,
|
||||
listLuckmailPurchasesForManagement,
|
||||
@@ -6807,7 +6862,7 @@ async function runPreStep6CookieCleanup() {
|
||||
|
||||
async function refreshOAuthUrlBeforeStep6(state) {
|
||||
if (state?.contributionModeExpected && !state?.contributionMode) {
|
||||
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 链路。请重新进入贡献模式后再点击自动。');
|
||||
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。');
|
||||
}
|
||||
if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
|
||||
await addLog('步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info');
|
||||
@@ -6823,7 +6878,7 @@ async function refreshOAuthUrlBeforeStep6(state) {
|
||||
await handleStepData(1, { oauthUrl });
|
||||
return oauthUrl;
|
||||
}
|
||||
await addLog(`步骤 7:contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
|
||||
await addLog(`步骤 7:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
|
||||
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
|
||||
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' });
|
||||
await handleStepData(1, refreshResult);
|
||||
@@ -7524,7 +7579,7 @@ async function executeContributionStep10(state) {
|
||||
|
||||
async function executeStep10(state) {
|
||||
if (state?.contributionModeExpected && !state?.contributionMode) {
|
||||
throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 提交。请重新进入贡献模式后再点击自动。');
|
||||
throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入贡献模式后再点击自动。');
|
||||
}
|
||||
if (state?.contributionMode) {
|
||||
return executeContributionStep10(state);
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
|
||||
const payload = {
|
||||
enablePrefix: true,
|
||||
enableRandomSubdomain: Boolean(config.useRandomSubdomain),
|
||||
name: requestedName,
|
||||
domain: config.domain,
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
upsertMail2925AccountInList,
|
||||
waitForTabComplete,
|
||||
waitForTabUrlMatch,
|
||||
} = deps;
|
||||
|
||||
@@ -45,6 +46,9 @@
|
||||
];
|
||||
const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::';
|
||||
const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::';
|
||||
const MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS = 15000;
|
||||
const MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS = 120000;
|
||||
const MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS = 120000;
|
||||
|
||||
function getMail2925MailConfig() {
|
||||
return {
|
||||
@@ -95,6 +99,14 @@
|
||||
return getErrorMessage(error).startsWith(MAIL2925_THREAD_TERMINATED_ERROR_PREFIX);
|
||||
}
|
||||
|
||||
function isRetryableMail2925TransportError(error) {
|
||||
const message = getErrorMessage(error).toLowerCase();
|
||||
return message.includes('receiving end does not exist')
|
||||
|| message.includes('message port closed')
|
||||
|| message.includes('content script on')
|
||||
|| message.includes('did not respond');
|
||||
}
|
||||
|
||||
async function syncMail2925Accounts(accounts) {
|
||||
const normalized = normalizeMail2925Accounts(accounts);
|
||||
await setPersistentSettings({ mail2925Accounts: normalized });
|
||||
@@ -399,6 +411,43 @@
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
async function recoverMail2925LoginPageAfterTransportError(tabId) {
|
||||
const numericTabId = Number(tabId);
|
||||
if (!Number.isInteger(numericTabId) || numericTabId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl();
|
||||
await addLog(
|
||||
`2925:登录提交后页面发生跳转或重载,正在等待当前标签页恢复后继续确认登录态。当前地址:${currentUrl || 'unknown'}`,
|
||||
'warn'
|
||||
);
|
||||
|
||||
if (typeof waitForTabComplete === 'function') {
|
||||
const completedTab = await waitForTabComplete(numericTabId, {
|
||||
timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS,
|
||||
retryDelayMs: 300,
|
||||
});
|
||||
await addLog(
|
||||
`2925:登录跳转等待结束,当前标签地址:${String(completedTab?.url || '').trim() || 'unknown'}`,
|
||||
completedTab?.url ? 'info' : 'warn'
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof ensureContentScriptReadyOnTab === 'function') {
|
||||
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, numericTabId, {
|
||||
inject: MAIL2925_INJECT,
|
||||
injectSource: MAIL2925_INJECT_SOURCE,
|
||||
timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS,
|
||||
retryDelayMs: 800,
|
||||
logMessage: '步骤 0:2925 登录后页面仍在跳转,正在等待邮箱页重新就绪...',
|
||||
});
|
||||
}
|
||||
|
||||
const recoveredUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl();
|
||||
await addLog(`2925:登录跳转恢复后当前标签地址:${recoveredUrl || 'unknown'}`, 'info');
|
||||
}
|
||||
|
||||
async function ensureMail2925MailboxSession(options = {}) {
|
||||
const {
|
||||
accountId = null,
|
||||
@@ -520,10 +569,10 @@
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
const sendEnsureSessionRequest = async () => {
|
||||
const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
|
||||
await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info');
|
||||
result = await sendLoginMessage(
|
||||
return sendLoginMessage(
|
||||
MAIL2925_SOURCE,
|
||||
{
|
||||
type: 'ENSURE_MAIL2925_SESSION',
|
||||
@@ -537,19 +586,34 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
timeoutMs: 50000,
|
||||
timeoutMs: MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS,
|
||||
retryDelayMs: 800,
|
||||
responseTimeoutMs: 50000,
|
||||
responseTimeoutMs: MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS,
|
||||
logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...',
|
||||
}
|
||||
);
|
||||
};
|
||||
try {
|
||||
result = await sendEnsureSessionRequest();
|
||||
} catch (err) {
|
||||
const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '40 秒内未进入收件箱'})。`;
|
||||
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
|
||||
if (stopped) {
|
||||
throw new Error('流程已被用户停止。');
|
||||
if (isRetryableMail2925TransportError(err)) {
|
||||
try {
|
||||
await recoverMail2925LoginPageAfterTransportError(tabId);
|
||||
await addLog('2925:页面恢复完成,正在重新确认登录态...', 'info');
|
||||
result = await sendEnsureSessionRequest();
|
||||
} catch (recoveryErr) {
|
||||
err = recoveryErr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '登录结果确认超时'})。`;
|
||||
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
|
||||
if (stopped) {
|
||||
throw new Error('流程已被用户停止。');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (result?.error) {
|
||||
@@ -584,6 +648,18 @@
|
||||
await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
await addLog('2925:未触发自动登录,继续复用当前已登录会话。', 'info');
|
||||
return {
|
||||
account: null,
|
||||
mail: getMail2925MailConfig(),
|
||||
result: {
|
||||
...result,
|
||||
usedExistingSession: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await patchMail2925Account(account.id, {
|
||||
lastLoginAt: Date.now(),
|
||||
lastError: '',
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
getPendingAutoRunTimerPlan,
|
||||
getSourceLabel,
|
||||
getState,
|
||||
getTabId,
|
||||
getStopRequested,
|
||||
handleAutoRunLoopUnhandledError,
|
||||
importSettingsBundle,
|
||||
@@ -50,6 +51,7 @@
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isLuckmailProvider,
|
||||
isStopError,
|
||||
isTabAlive,
|
||||
launchAutoRunTimerPlan,
|
||||
listIcloudAliases,
|
||||
listLuckmailPurchasesForManagement,
|
||||
@@ -108,6 +110,23 @@
|
||||
return appendAccountRunRecord(status, state, reason);
|
||||
}
|
||||
|
||||
async function ensureManualStepPrerequisites(step) {
|
||||
if (step !== 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signupTabId = typeof getTabId === 'function'
|
||||
? await getTabId('signup-page')
|
||||
: null;
|
||||
const signupTabAlive = signupTabId && typeof isTabAlive === 'function'
|
||||
? await isTabAlive('signup-page')
|
||||
: Boolean(signupTabId);
|
||||
|
||||
if (!signupTabId || !signupTabAlive) {
|
||||
throw new Error('手动执行步骤 4 前,请先执行步骤 1 或步骤 2,确保认证页仍然打开并停留在验证码页。');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStepData(step, payload) {
|
||||
switch (step) {
|
||||
case 1: {
|
||||
@@ -121,6 +140,8 @@
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||||
if (Object.keys(updates).length) {
|
||||
await setState(updates);
|
||||
}
|
||||
@@ -402,6 +423,9 @@
|
||||
await ensureManualInteractionAllowed('手动执行步骤');
|
||||
}
|
||||
const step = message.payload.step;
|
||||
if (message.source === 'sidepanel') {
|
||||
await ensureManualStepPrerequisites(step);
|
||||
}
|
||||
if (message.source === 'sidepanel') {
|
||||
await invalidateDownstreamAfterStepRestart(step, { logLabel: `步骤 ${step} 重新执行` });
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundNavigationUtilsModule() {
|
||||
function createNavigationUtils(deps = {}) {
|
||||
const {
|
||||
DEFAULT_CODEX2API_URL,
|
||||
DEFAULT_SUB2API_URL,
|
||||
normalizeLocalCpaStep9Mode,
|
||||
} = deps;
|
||||
@@ -27,13 +28,36 @@
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function normalizeCodex2ApiUrl(rawUrl) {
|
||||
const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL;
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') {
|
||||
parsed.pathname = '/admin/accounts';
|
||||
}
|
||||
parsed.hash = '';
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function getPanelMode(state = {}) {
|
||||
return state.panelMode === 'sub2api' ? 'sub2api' : 'cpa';
|
||||
if (state.panelMode === 'sub2api') {
|
||||
return 'sub2api';
|
||||
}
|
||||
if (state.panelMode === 'codex2api') {
|
||||
return 'codex2api';
|
||||
}
|
||||
return 'cpa';
|
||||
}
|
||||
|
||||
function getPanelModeLabel(modeOrState) {
|
||||
const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState);
|
||||
return mode === 'sub2api' ? 'SUB2API' : 'CPA';
|
||||
if (mode === 'sub2api') {
|
||||
return 'SUB2API';
|
||||
}
|
||||
if (mode === 'codex2api') {
|
||||
return 'Codex2API';
|
||||
}
|
||||
return 'CPA';
|
||||
}
|
||||
|
||||
function isSignupPageHost(hostname = '') {
|
||||
@@ -124,6 +148,14 @@
|
||||
|| candidate.pathname.startsWith('/login')
|
||||
|| candidate.pathname === '/'
|
||||
);
|
||||
case 'codex2api-panel':
|
||||
return Boolean(reference)
|
||||
&& candidate.origin === reference.origin
|
||||
&& (
|
||||
candidate.pathname.startsWith('/admin/accounts')
|
||||
|| candidate.pathname === '/admin'
|
||||
|| candidate.pathname === '/'
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -160,6 +192,7 @@
|
||||
isSignupPageHost,
|
||||
isSignupPasswordPageUrl,
|
||||
matchesSourceUrlFamily,
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
parseUrlSafely,
|
||||
shouldBypassStep9ForLocalCpa,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
closeConflictingTabsForSource,
|
||||
ensureContentScriptReadyOnTab,
|
||||
getPanelMode,
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
rememberSourceLastUrl,
|
||||
sendToContentScript,
|
||||
@@ -17,7 +18,74 @@
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
} = deps;
|
||||
|
||||
function normalizeAdminKey(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function extractStateFromAuthUrl(authUrl = '') {
|
||||
try {
|
||||
return new URL(authUrl).searchParams.get('state') || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getCodex2ApiErrorMessage(payload, responseStatus = 500) {
|
||||
const candidates = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
];
|
||||
const message = candidates
|
||||
.map((value) => String(value || '').trim())
|
||||
.find(Boolean);
|
||||
return message || `Codex2API 请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchCodex2ApiJson(origin, path, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Key': normalizeAdminKey(options.adminKey),
|
||||
},
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCodex2ApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('Codex2API 请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestOAuthUrlFromPanel(state, options = {}) {
|
||||
if (getPanelMode(state) === 'codex2api') {
|
||||
return requestCodex2ApiOAuthUrl(state, options);
|
||||
}
|
||||
if (getPanelMode(state) === 'sub2api') {
|
||||
return requestSub2ApiOAuthUrl(state, options);
|
||||
}
|
||||
@@ -74,6 +142,39 @@
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function requestCodex2ApiOAuthUrl(state, options = {}) {
|
||||
const { logLabel = 'OAuth 刷新' } = options;
|
||||
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
||||
const adminKey = normalizeAdminKey(state.codex2apiAdminKey);
|
||||
|
||||
if (!adminKey) {
|
||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const origin = new URL(codex2apiUrl).origin;
|
||||
await addLog(`${logLabel}:正在通过 Codex2API 协议生成 OAuth 授权链接...`);
|
||||
|
||||
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/generate-auth-url', {
|
||||
adminKey,
|
||||
method: 'POST',
|
||||
body: {},
|
||||
});
|
||||
|
||||
const oauthUrl = String(result?.auth_url || result?.authUrl || '').trim();
|
||||
const sessionId = String(result?.session_id || result?.sessionId || '').trim();
|
||||
const oauthState = extractStateFromAuthUrl(oauthUrl);
|
||||
|
||||
if (!oauthUrl || !sessionId) {
|
||||
throw new Error('Codex2API 未返回有效的 auth_url 或 session_id。');
|
||||
}
|
||||
|
||||
return {
|
||||
oauthUrl,
|
||||
codex2apiSessionId: sessionId,
|
||||
codex2apiOAuthState: oauthState || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestSub2ApiOAuthUrl(state, options = {}) {
|
||||
const { logLabel = 'OAuth 刷新' } = options;
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
@@ -135,6 +236,7 @@
|
||||
|
||||
return {
|
||||
requestOAuthUrlFromPanel,
|
||||
requestCodex2ApiOAuthUrl,
|
||||
requestCpaOAuthUrl,
|
||||
requestSub2ApiOAuthUrl,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
chrome,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureStep8VerificationPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
@@ -57,6 +58,20 @@
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
|
||||
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
|
||||
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
|
||||
if (accountEmail) {
|
||||
return accountEmail;
|
||||
}
|
||||
}
|
||||
|
||||
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function focusOrOpenMailTab(mail) {
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
@@ -134,7 +149,17 @@
|
||||
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
|
||||
} else {
|
||||
await addLog(`步骤 8:正在打开${mail.label}...`);
|
||||
await focusOrOpenMailTab(mail);
|
||||
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
|
||||
await ensureMail2925MailboxSession({
|
||||
accountId: state.currentMail2925AccountId || null,
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
|
||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
|
||||
actionLabel: 'Step 8: ensure 2925 mailbox session',
|
||||
});
|
||||
} else {
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
if (mail.provider === '2925') {
|
||||
await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
|
||||
if (!signupTabId) {
|
||||
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
|
||||
throw new Error('认证页面标签页已关闭,无法继续步骤 4。请先执行步骤 1 或步骤 2,重新打开认证页后再试。');
|
||||
}
|
||||
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
|
||||
function isManagementSecretConfigError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '').trim();
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mentionsSecret = /管理密钥|Admin Secret|X-Admin-Key|CPA Key/i.test(message);
|
||||
if (!mentionsSecret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
|
||||
}
|
||||
|
||||
async function executeStep7(state) {
|
||||
if (!state.email) {
|
||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||
@@ -99,6 +113,13 @@
|
||||
if (isAddPhoneAuthFailure(err)) {
|
||||
throw err;
|
||||
}
|
||||
if (isManagementSecretConfigError(err)) {
|
||||
await addLog(
|
||||
`步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||
'error'
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
lastError = err;
|
||||
if (attempt >= STEP6_MAX_ATTEMPTS) {
|
||||
break;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
getTabId,
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isTabAlive,
|
||||
normalizeCodex2ApiUrl,
|
||||
normalizeSub2ApiUrl,
|
||||
rememberSourceLastUrl,
|
||||
reuseOrCreateTab,
|
||||
@@ -21,7 +22,86 @@
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
} = deps;
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。');
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const state = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !state) {
|
||||
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。');
|
||||
}
|
||||
|
||||
return {
|
||||
url: parsed.toString(),
|
||||
code,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
function getCodex2ApiErrorMessage(payload, responseStatus = 500) {
|
||||
const details = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
]
|
||||
.map((value) => normalizeString(value))
|
||||
.find(Boolean);
|
||||
return details || `Codex2API 请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchCodex2ApiJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Key': normalizeString(options.adminKey),
|
||||
},
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCodex2ApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('Codex2API 请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeStep10(state) {
|
||||
if (getPanelMode(state) === 'codex2api') {
|
||||
return executeCodex2ApiStep10(state);
|
||||
}
|
||||
if (getPanelMode(state) === 'sub2api') {
|
||||
return executeSub2ApiStep10(state);
|
||||
}
|
||||
@@ -90,6 +170,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCodex2ApiStep10(state) {
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
}
|
||||
if (!state.codex2apiSessionId) {
|
||||
throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。');
|
||||
}
|
||||
if (!normalizeString(state.codex2apiAdminKey)) {
|
||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const callback = parseLocalhostCallback(state.localhostUrl);
|
||||
const expectedState = normalizeString(state.codex2apiOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
|
||||
}
|
||||
|
||||
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
||||
const origin = new URL(codex2apiUrl).origin;
|
||||
|
||||
await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...');
|
||||
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
|
||||
adminKey: state.codex2apiAdminKey,
|
||||
method: 'POST',
|
||||
body: {
|
||||
session_id: state.codex2apiSessionId,
|
||||
code: callback.code,
|
||||
state: callback.state,
|
||||
},
|
||||
});
|
||||
|
||||
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
|
||||
await addLog(`步骤 10:${verifiedStatus}`, 'ok');
|
||||
await completeStepFromBackground(10, {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSub2ApiStep10(state) {
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
@@ -161,6 +283,7 @@
|
||||
|
||||
return {
|
||||
executeCpaStep10,
|
||||
executeCodex2ApiStep10,
|
||||
executeStep10,
|
||||
executeSub2ApiStep10,
|
||||
};
|
||||
|
||||
+366
-31
@@ -69,15 +69,141 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
// Find mail items
|
||||
// ============================================================
|
||||
|
||||
function findMailItems() {
|
||||
return document.querySelectorAll('div[sign="letter"]');
|
||||
function normalizeText(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getCurrentMailIds() {
|
||||
function isVisibleNode(node) {
|
||||
if (!node) return false;
|
||||
if (node.hidden) return false;
|
||||
|
||||
const style = typeof window.getComputedStyle === 'function'
|
||||
? window.getComputedStyle(node)
|
||||
: null;
|
||||
if (style && (style.display === 'none' || style.visibility === 'hidden')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = typeof node.getBoundingClientRect === 'function'
|
||||
? node.getBoundingClientRect()
|
||||
: null;
|
||||
if (rect && rect.width <= 0 && rect.height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isLikelyMailItemNode(node) {
|
||||
if (!isVisibleNode(node)) {
|
||||
return false;
|
||||
}
|
||||
if (node.matches?.('div[sign="letter"]')) {
|
||||
return true;
|
||||
}
|
||||
if (node.querySelector?.('.nui-user, span.da0, [sign="trash"], [title="删除邮件"], [class*="subject"], [class*="sender"]')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const summaryText = normalizeText(
|
||||
node.getAttribute?.('aria-label')
|
||||
|| node.getAttribute?.('title')
|
||||
|| node.textContent
|
||||
);
|
||||
if (!summaryText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /发件人|验证码|verification|chatgpt|openai|code/i.test(summaryText);
|
||||
}
|
||||
|
||||
function findMailItems() {
|
||||
const selectorGroups = [
|
||||
'div[sign="letter"]',
|
||||
'[role="option"][aria-label]',
|
||||
'[role="listitem"][aria-label]',
|
||||
'tr[aria-label]',
|
||||
'li[aria-label]',
|
||||
'div[aria-label]',
|
||||
];
|
||||
|
||||
for (const selector of selectorGroups) {
|
||||
const matches = Array.from(document.querySelectorAll(selector)).filter(isLikelyMailItemNode);
|
||||
if (matches.length > 0) {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getMailTextBySelectors(item, selectors = []) {
|
||||
for (const selector of selectors) {
|
||||
const candidates = item.querySelectorAll(selector);
|
||||
for (const candidate of candidates) {
|
||||
const texts = [
|
||||
candidate.getAttribute?.('title'),
|
||||
candidate.getAttribute?.('aria-label'),
|
||||
candidate.textContent,
|
||||
];
|
||||
for (const text of texts) {
|
||||
const normalized = normalizeText(text);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getMailSenderText(item) {
|
||||
return getMailTextBySelectors(item, [
|
||||
'.nui-user',
|
||||
'[class*="sender"]',
|
||||
'[class*="from"]',
|
||||
'[data-sender]',
|
||||
]);
|
||||
}
|
||||
|
||||
function getMailSubjectText(item) {
|
||||
return getMailTextBySelectors(item, [
|
||||
'span.da0',
|
||||
'[class*="subject"]',
|
||||
'[data-subject]',
|
||||
'strong',
|
||||
]);
|
||||
}
|
||||
|
||||
function getMailRowText(item) {
|
||||
const ariaLabel = normalizeText(item.getAttribute('aria-label'));
|
||||
const sender = getMailSenderText(item);
|
||||
const subject = getMailSubjectText(item);
|
||||
const fullText = normalizeText(item.textContent || '');
|
||||
return normalizeText([ariaLabel, sender, subject, fullText].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getMailItemId(item, index = 0) {
|
||||
const candidates = [
|
||||
item.getAttribute('id'),
|
||||
item.getAttribute('data-id'),
|
||||
item.dataset?.id,
|
||||
item.getAttribute('data-key'),
|
||||
item.getAttribute('key'),
|
||||
].filter(Boolean);
|
||||
|
||||
if (candidates.length > 0) {
|
||||
return String(candidates[0]);
|
||||
}
|
||||
|
||||
return `${index}|${getMailRowText(item).slice(0, 240)}`;
|
||||
}
|
||||
|
||||
function getCurrentMailIds(items = []) {
|
||||
const ids = new Set();
|
||||
findMailItems().forEach(item => {
|
||||
const id = item.getAttribute('id') || '';
|
||||
if (id) ids.add(id);
|
||||
const sourceItems = items.length > 0 ? items : findMailItems();
|
||||
sourceItems.forEach((item, index) => {
|
||||
ids.add(getMailItemId(item, index));
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
@@ -90,7 +216,7 @@ function normalizeMinuteTimestamp(timestamp) {
|
||||
}
|
||||
|
||||
function parseMail163Timestamp(rawText) {
|
||||
const text = (rawText || '').replace(/\s+/g, ' ').trim();
|
||||
const text = normalizeText(rawText);
|
||||
if (!text) return null;
|
||||
|
||||
let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/);
|
||||
@@ -107,6 +233,37 @@ function parseMail163Timestamp(rawText) {
|
||||
).getTime();
|
||||
}
|
||||
|
||||
match = text.match(/今天\s*(\d{1,2}):(\d{2})/);
|
||||
if (match) {
|
||||
const [, hour, minute] = match;
|
||||
const now = new Date();
|
||||
return new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
Number(hour),
|
||||
Number(minute),
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
}
|
||||
|
||||
match = text.match(/昨天\s*(\d{1,2}):(\d{2})/);
|
||||
if (match) {
|
||||
const [, hour, minute] = match;
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() - 1);
|
||||
return new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
Number(hour),
|
||||
Number(minute),
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
}
|
||||
|
||||
match = text.match(/\b(\d{1,2}):(\d{2})\b/);
|
||||
if (match) {
|
||||
const [, hour, minute] = match;
|
||||
@@ -125,18 +282,48 @@ function parseMail163Timestamp(rawText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMailTimestamp(item) {
|
||||
const candidates = [];
|
||||
const timeCell = item.querySelector('.e00[title], [title*="年"][title*=":"]');
|
||||
if (timeCell?.getAttribute('title')) candidates.push(timeCell.getAttribute('title'));
|
||||
if (timeCell?.textContent) candidates.push(timeCell.textContent);
|
||||
function isLikelyMailTimestampText(value) {
|
||||
const text = normalizeText(value);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return /(\d{4}年\d{1,2}月\d{1,2}日\s+\d{1,2}:\d{2})|今天\s*\d{1,2}:\d{2}|昨天\s*\d{1,2}:\d{2}|\b\d{1,2}:\d{2}\b/.test(text);
|
||||
}
|
||||
|
||||
const titledNodes = item.querySelectorAll('[title]');
|
||||
titledNodes.forEach((node) => {
|
||||
const title = node.getAttribute('title');
|
||||
if (title) candidates.push(title);
|
||||
function collectMailTimestampCandidates(item) {
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const pushCandidate = (value) => {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized || !isLikelyMailTimestampText(normalized) || seen.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
seen.add(normalized);
|
||||
candidates.push(normalized);
|
||||
};
|
||||
|
||||
const priorityNodes = item.querySelectorAll('.e00, [title], [aria-label], time, [class*="time"], [class*="date"]');
|
||||
priorityNodes.forEach((node) => {
|
||||
pushCandidate(node.getAttribute?.('title'));
|
||||
pushCandidate(node.getAttribute?.('aria-label'));
|
||||
pushCandidate(node.textContent);
|
||||
});
|
||||
|
||||
const textNodes = item.querySelectorAll('span, div, td, strong, b');
|
||||
textNodes.forEach((node) => {
|
||||
const text = normalizeText(node.textContent);
|
||||
if (text && text.length <= 24) {
|
||||
pushCandidate(text);
|
||||
}
|
||||
});
|
||||
|
||||
pushCandidate(item.getAttribute('aria-label'));
|
||||
pushCandidate(item.getAttribute('title'));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function getMailTimestamp(item) {
|
||||
const candidates = collectMailTimestampCandidates(item);
|
||||
for (const candidate of candidates) {
|
||||
const parsed = parseMail163Timestamp(candidate);
|
||||
if (parsed) return parsed;
|
||||
@@ -145,6 +332,134 @@ function getMailTimestamp(item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectOpenedMailTextCandidates() {
|
||||
const texts = [];
|
||||
const seen = new Set();
|
||||
const pushText = (value) => {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
seen.add(normalized);
|
||||
texts.push(normalized);
|
||||
};
|
||||
|
||||
const selectors = [
|
||||
'.readHtml',
|
||||
'[class*="readmail"]',
|
||||
'[class*="mailread"]',
|
||||
'[class*="mailBody"]',
|
||||
'[class*="mailbody"]',
|
||||
'[class*="mail-content"]',
|
||||
'[class*="mailContent"]',
|
||||
'[class*="mail-detail"]',
|
||||
'[class*="mailDetail"]',
|
||||
'[class*="detail"] [class*="content"]',
|
||||
'[class*="read"] [class*="content"]',
|
||||
'[role="main"]',
|
||||
];
|
||||
|
||||
selectors.forEach((selector) => {
|
||||
document.querySelectorAll(selector).forEach((node) => {
|
||||
pushText(node.innerText || node.textContent);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('iframe').forEach((frame) => {
|
||||
try {
|
||||
pushText(frame.contentDocument?.body?.innerText || frame.contentDocument?.body?.textContent);
|
||||
} catch {
|
||||
// Ignore cross-frame access errors and keep trying other candidates.
|
||||
}
|
||||
});
|
||||
|
||||
pushText(document.body?.innerText || document.body?.textContent);
|
||||
return texts.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
function selectOpenedMailTextCandidate(item, candidates = [], options = {}) {
|
||||
const subject = normalizeText(getMailSubjectText(item)).toLowerCase();
|
||||
const sender = normalizeText(getMailSenderText(item)).toLowerCase();
|
||||
const excludedSet = new Set((options.excludedTexts || []).map((value) => normalizeText(value)));
|
||||
const allowExcludedFallback = options.allowExcludedFallback !== false;
|
||||
|
||||
const pickCandidate = (source) => source.find((candidate) => {
|
||||
const lower = candidate.toLowerCase();
|
||||
if (subject && lower.includes(subject)) {
|
||||
return true;
|
||||
}
|
||||
if (sender && lower.includes(sender)) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(extractVerificationCode(candidate) && /chatgpt|openai|verification|验证码|login code/i.test(lower));
|
||||
}) || source[0] || '';
|
||||
|
||||
const filteredCandidates = candidates.filter((candidate) => !excludedSet.has(normalizeText(candidate)));
|
||||
const preferred = pickCandidate(filteredCandidates);
|
||||
if (preferred || !allowExcludedFallback) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
return pickCandidate(candidates);
|
||||
}
|
||||
|
||||
function readOpenedMailText(item, options = {}) {
|
||||
const candidates = collectOpenedMailTextCandidates();
|
||||
return selectOpenedMailTextCandidate(item, candidates, options);
|
||||
}
|
||||
|
||||
async function returnToInbox() {
|
||||
const inboxLink = document.querySelector('.nui-tree-item-text[title="收件箱"], [title="收件箱"]');
|
||||
if (inboxLink) {
|
||||
if (typeof simulateClick === 'function') {
|
||||
simulateClick(inboxLink);
|
||||
} else {
|
||||
inboxLink.click();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
if (findMailItems().length > 0) {
|
||||
return true;
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function openMailAndGetMessageText(item) {
|
||||
const beforeCandidates = collectOpenedMailTextCandidates();
|
||||
const beforeText = selectOpenedMailTextCandidate(item, beforeCandidates);
|
||||
if (typeof simulateClick === 'function') {
|
||||
simulateClick(item);
|
||||
} else {
|
||||
item.click();
|
||||
}
|
||||
|
||||
let openedText = '';
|
||||
for (let i = 0; i < 24; i += 1) {
|
||||
await sleep(250);
|
||||
const candidate = readOpenedMailText(item, {
|
||||
excludedTexts: beforeCandidates,
|
||||
allowExcludedFallback: false,
|
||||
});
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
openedText = candidate;
|
||||
if (extractVerificationCode(candidate)) {
|
||||
break;
|
||||
}
|
||||
if (candidate !== beforeText && candidate.length > beforeText.length + 24) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await returnToInbox();
|
||||
return openedText;
|
||||
}
|
||||
|
||||
function scheduleEmailCleanup(item, step) {
|
||||
setTimeout(() => {
|
||||
Promise.resolve(deleteEmail(item, step)).catch(() => {
|
||||
@@ -199,7 +514,7 @@ async function handlePollEmail(step, payload) {
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
||||
|
||||
// Snapshot existing mail IDs
|
||||
const existingMailIds = getCurrentMailIds();
|
||||
const existingMailIds = getCurrentMailIds(items);
|
||||
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
@@ -215,38 +530,58 @@ async function handlePollEmail(step, payload) {
|
||||
const allItems = findMailItems();
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
|
||||
for (const item of allItems) {
|
||||
const id = item.getAttribute('id') || '';
|
||||
for (let index = 0; index < allItems.length; index++) {
|
||||
const item = allItems[index];
|
||||
const id = getMailItemId(item, index);
|
||||
const mailTimestamp = getMailTimestamp(item);
|
||||
const mailMinute = normalizeMinuteTimestamp(mailTimestamp || 0);
|
||||
const passesTimeFilter = !filterAfterMinute || (mailMinute && mailMinute >= filterAfterMinute);
|
||||
const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && mailMinute > 0);
|
||||
|
||||
if (!passesTimeFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(id)) continue;
|
||||
if (!useFallback && existingMailIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const senderEl = item.querySelector('.nui-user');
|
||||
const sender = senderEl ? senderEl.textContent.toLowerCase() : '';
|
||||
const sender = getMailSenderText(item).toLowerCase();
|
||||
const subject = getMailSubjectText(item);
|
||||
const rowText = getMailRowText(item);
|
||||
const ariaLabel = normalizeText(item.getAttribute('aria-label')).toLowerCase();
|
||||
const combinedText = normalizeText([subject, ariaLabel, rowText].filter(Boolean).join(' '));
|
||||
|
||||
const subjectEl = item.querySelector('span.da0');
|
||||
const subject = subjectEl ? subjectEl.textContent : '';
|
||||
if (!mailTimestamp) {
|
||||
log(`步骤 ${step}:邮件 ${id.slice(0, 60)} 未读取到时间,已跳过时间窗口校验后的文本匹配阶段。`, 'info');
|
||||
}
|
||||
|
||||
const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase();
|
||||
|
||||
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
||||
const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
||||
const senderMatch = senderFilters.some((filter) => {
|
||||
const normalizedFilter = String(filter || '').toLowerCase();
|
||||
return normalizedFilter && (sender.includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter));
|
||||
});
|
||||
const subjectMatch = subjectFilters.some((filter) => {
|
||||
const normalizedFilter = String(filter || '').toLowerCase();
|
||||
return normalizedFilter && (subject.toLowerCase().includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter));
|
||||
});
|
||||
|
||||
if (senderMatch || subjectMatch) {
|
||||
const code = extractVerificationCode(subject + ' ' + ariaLabel);
|
||||
let code = extractVerificationCode(combinedText);
|
||||
let codeSource = '邮件列表';
|
||||
|
||||
if (!code) {
|
||||
const openedText = await openMailAndGetMessageText(item);
|
||||
code = extractVerificationCode(openedText);
|
||||
if (code) {
|
||||
codeSource = '邮件正文';
|
||||
}
|
||||
}
|
||||
|
||||
if (code && excludedCodeSet.has(code)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||
} else if (code && !seenCodes.has(code)) {
|
||||
seenCodes.add(code);
|
||||
persistSeenCodes();
|
||||
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
|
||||
const source = useFallback && existingMailIds.has(id) ? `回退匹配${codeSource}` : `新邮件${codeSource}`;
|
||||
const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
||||
log(`步骤 ${step}:已找到验证码:${code}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)})`, 'ok');
|
||||
|
||||
|
||||
@@ -534,6 +534,9 @@ function detectMail2925ViewState() {
|
||||
|
||||
function getMail2925DisplayedMailboxEmail() {
|
||||
const directSelectors = [
|
||||
'.right-header',
|
||||
'[class~="right-header"]',
|
||||
'[class*="right-header"]',
|
||||
'[class*="user"] [class*="mail"]',
|
||||
'[class*="user"] [class*="email"]',
|
||||
'[class*="account"] [class*="mail"]',
|
||||
@@ -548,7 +551,8 @@ function getMail2925DisplayedMailboxEmail() {
|
||||
if (!isVisibleNode(candidate) || isMailItemNode(candidate)) {
|
||||
continue;
|
||||
}
|
||||
const email = extractEmails(candidate.textContent || candidate.innerText || '')[0] || '';
|
||||
const email = extractEmails(candidate.textContent || candidate.innerText || '')
|
||||
.find((value) => /@2925\.com$/i.test(String(value || '').trim())) || '';
|
||||
if (email) {
|
||||
return email;
|
||||
}
|
||||
@@ -567,7 +571,8 @@ function getMail2925DisplayedMailboxEmail() {
|
||||
return rect.top >= 0 && rect.top <= Math.max(window.innerHeight * 0.35, 280);
|
||||
})
|
||||
.map((node) => {
|
||||
const email = extractEmails(node.textContent || node.innerText || '')[0] || '';
|
||||
const email = extractEmails(node.textContent || node.innerText || '')
|
||||
.find((value) => /@2925\.com$/i.test(String(value || '').trim())) || '';
|
||||
return { node, email };
|
||||
})
|
||||
.filter((entry) => entry.email);
|
||||
|
||||
+207
-38
@@ -1621,8 +1621,13 @@ function createStep6RecoverableResult(reason, snapshot, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) {
|
||||
const resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
||||
async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message, options = {}) {
|
||||
const {
|
||||
loginVerificationRequestedAt = null,
|
||||
via = 'login_timeout_recovered',
|
||||
} = options;
|
||||
let resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
||||
let recovered = false;
|
||||
if (resolvedSnapshot?.state === 'login_timeout_error_page') {
|
||||
try {
|
||||
const recoveryResult = await recoverCurrentAuthRetryPage({
|
||||
@@ -1631,8 +1636,9 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag
|
||||
step: 7,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
if (recoveryResult?.recovered) {
|
||||
log('步骤 7:登录超时报错页已点击“重试”,准备重新执行当前步骤。', 'warn');
|
||||
recovered = Boolean(recoveryResult?.recovered);
|
||||
if (recovered) {
|
||||
log('步骤 7:登录超时报错页已点击“重试”,正在按恢复后的页面状态继续当前流程。', 'warn');
|
||||
}
|
||||
} catch (error) {
|
||||
if (/CF_SECURITY_BLOCKED::/i.test(String(error?.message || error || ''))) {
|
||||
@@ -1642,7 +1648,46 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag
|
||||
}
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult(reason, resolvedSnapshot, {
|
||||
resolvedSnapshot = recovered
|
||||
? normalizeStep6Snapshot(await waitForKnownLoginAuthState(4000))
|
||||
: normalizeStep6Snapshot(inspectLoginAuthState());
|
||||
|
||||
if (resolvedSnapshot.state === 'verification_page') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: createStep6SuccessResult(resolvedSnapshot, {
|
||||
via,
|
||||
loginVerificationRequestedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (resolvedSnapshot.state === 'password_page') {
|
||||
log('步骤 7:登录超时报错页恢复后已进入密码页,继续当前登录流程。', 'warn');
|
||||
return { action: 'password', snapshot: resolvedSnapshot };
|
||||
}
|
||||
|
||||
if (resolvedSnapshot.state === 'email_page') {
|
||||
log('步骤 7:登录超时报错页恢复后已回到邮箱输入页,继续当前登录流程。', 'warn');
|
||||
return { action: 'email', snapshot: resolvedSnapshot };
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult(reason, resolvedSnapshot, {
|
||||
message,
|
||||
loginVerificationRequestedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) {
|
||||
const transition = await createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message);
|
||||
if (transition?.action === 'done' || transition?.action === 'recoverable') {
|
||||
return transition.result;
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult(reason, transition?.snapshot || normalizeStep6Snapshot(inspectLoginAuthState()), {
|
||||
message,
|
||||
});
|
||||
}
|
||||
@@ -2222,13 +2267,30 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
const transition = await createStep6LoginTimeoutRecoveryTransition(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交邮箱后进入登录超时报错页。',
|
||||
{
|
||||
loginVerificationRequestedAt: emailSubmittedAt,
|
||||
via: 'email_submit_timeout_recovered',
|
||||
}
|
||||
);
|
||||
if (transition.action === 'done') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: transition.result,
|
||||
};
|
||||
}
|
||||
if (transition.action === 'password') {
|
||||
return { action: 'password', snapshot: transition.snapshot };
|
||||
}
|
||||
if (transition.action === 'email') {
|
||||
return { action: 'email', snapshot: transition.snapshot };
|
||||
}
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交邮箱后进入登录超时报错页。'
|
||||
),
|
||||
result: transition.result,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2257,13 +2319,30 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120
|
||||
return { action: 'password', snapshot };
|
||||
}
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
const transition = await createStep6LoginTimeoutRecoveryTransition(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交邮箱后进入登录超时报错页。',
|
||||
{
|
||||
loginVerificationRequestedAt: emailSubmittedAt,
|
||||
via: 'email_submit_timeout_recovered',
|
||||
}
|
||||
);
|
||||
if (transition.action === 'done') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: transition.result,
|
||||
};
|
||||
}
|
||||
if (transition.action === 'password') {
|
||||
return { action: 'password', snapshot: transition.snapshot };
|
||||
}
|
||||
if (transition.action === 'email') {
|
||||
return { action: 'email', snapshot: transition.snapshot };
|
||||
}
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交邮箱后进入登录超时报错页。'
|
||||
),
|
||||
result: transition.result,
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
@@ -2300,13 +2379,30 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
const transition = await createStep6LoginTimeoutRecoveryTransition(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交密码后进入登录超时报错页。',
|
||||
{
|
||||
loginVerificationRequestedAt: passwordSubmittedAt,
|
||||
via: 'password_submit_timeout_recovered',
|
||||
}
|
||||
);
|
||||
if (transition.action === 'done') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: transition.result,
|
||||
};
|
||||
}
|
||||
if (transition.action === 'password') {
|
||||
return { action: 'password', snapshot: transition.snapshot };
|
||||
}
|
||||
if (transition.action === 'email') {
|
||||
return { action: 'email', snapshot: transition.snapshot };
|
||||
}
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交密码后进入登录超时报错页。'
|
||||
),
|
||||
result: transition.result,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2332,13 +2428,30 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
const transition = await createStep6LoginTimeoutRecoveryTransition(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交密码后进入登录超时报错页。',
|
||||
{
|
||||
loginVerificationRequestedAt: passwordSubmittedAt,
|
||||
via: 'password_submit_timeout_recovered',
|
||||
}
|
||||
);
|
||||
if (transition.action === 'done') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: transition.result,
|
||||
};
|
||||
}
|
||||
if (transition.action === 'password') {
|
||||
return { action: 'password', snapshot: transition.snapshot };
|
||||
}
|
||||
if (transition.action === 'email') {
|
||||
return { action: 'email', snapshot: transition.snapshot };
|
||||
}
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交密码后进入登录超时报错页。'
|
||||
),
|
||||
result: transition.result,
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
@@ -2375,11 +2488,22 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return await createStep6LoginTimeoutRecoverableResult(
|
||||
const transition = await createStep6LoginTimeoutRecoveryTransition(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'切换到一次性验证码登录后进入登录超时报错页。'
|
||||
'切换到一次性验证码登录后进入登录超时报错页。',
|
||||
{
|
||||
loginVerificationRequestedAt,
|
||||
via: 'switch_to_one_time_code_timeout_recovered',
|
||||
}
|
||||
);
|
||||
if (transition.action === 'done') {
|
||||
return transition.result;
|
||||
}
|
||||
if (transition.action === 'password' || transition.action === 'email') {
|
||||
return transition;
|
||||
}
|
||||
return transition.result;
|
||||
}
|
||||
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
@@ -2401,11 +2525,22 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou
|
||||
});
|
||||
}
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return await createStep6LoginTimeoutRecoverableResult(
|
||||
const transition = await createStep6LoginTimeoutRecoveryTransition(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'切换到一次性验证码登录后进入登录超时报错页。'
|
||||
'切换到一次性验证码登录后进入登录超时报错页。',
|
||||
{
|
||||
loginVerificationRequestedAt,
|
||||
via: 'switch_to_one_time_code_timeout_recovered',
|
||||
}
|
||||
);
|
||||
if (transition.action === 'done') {
|
||||
return transition.result;
|
||||
}
|
||||
if (transition.action === 'password' || transition.action === 'email') {
|
||||
return transition;
|
||||
}
|
||||
return transition.result;
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
@@ -2419,7 +2554,7 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou
|
||||
});
|
||||
}
|
||||
|
||||
async function step6SwitchToOneTimeCodeLogin(snapshot) {
|
||||
async function step6SwitchToOneTimeCodeLogin(payload, snapshot) {
|
||||
const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger();
|
||||
if (!switchTrigger || !isActionEnabled(switchTrigger)) {
|
||||
return createStep6RecoverableResult('missing_one_time_code_trigger', normalizeStep6Snapshot(inspectLoginAuthState()), {
|
||||
@@ -2441,6 +2576,12 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) {
|
||||
via: result.via || 'switch_to_one_time_code_login',
|
||||
});
|
||||
}
|
||||
if (result?.action === 'password') {
|
||||
return step6LoginFromPasswordPage(payload, result.snapshot);
|
||||
}
|
||||
if (result?.action === 'email') {
|
||||
return step6LoginFromEmailPage(payload, result.snapshot);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2452,7 +2593,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||
if (!hasPassword) {
|
||||
if (currentSnapshot.switchTrigger) {
|
||||
log('步骤 7:当前未提供密码,改走一次性验证码登录。', 'warn');
|
||||
return step6SwitchToOneTimeCodeLogin(currentSnapshot);
|
||||
return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot);
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('missing_password_and_one_time_code_trigger', currentSnapshot, {
|
||||
@@ -2482,8 +2623,14 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||
log(`步骤 7:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn');
|
||||
return transition.result;
|
||||
}
|
||||
if (transition.action === 'password') {
|
||||
return step6LoginFromPasswordPage(payload, transition.snapshot);
|
||||
}
|
||||
if (transition.action === 'email') {
|
||||
return step6LoginFromEmailPage(payload, transition.snapshot);
|
||||
}
|
||||
if (transition.action === 'switch') {
|
||||
return step6SwitchToOneTimeCodeLogin(transition.snapshot);
|
||||
return step6SwitchToOneTimeCodeLogin(payload, transition.snapshot);
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('password_submit_unknown', normalizeStep6Snapshot(inspectLoginAuthState()), {
|
||||
@@ -2492,7 +2639,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||
}
|
||||
|
||||
if (currentSnapshot.switchTrigger) {
|
||||
return step6SwitchToOneTimeCodeLogin(currentSnapshot);
|
||||
return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot);
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('password_page_unactionable', currentSnapshot, {
|
||||
@@ -2532,6 +2679,9 @@ async function step6LoginFromEmailPage(payload, snapshot) {
|
||||
log(`步骤 7:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn');
|
||||
return transition.result;
|
||||
}
|
||||
if (transition.action === 'email') {
|
||||
return step6LoginFromEmailPage(payload, transition.snapshot);
|
||||
}
|
||||
if (transition.action === 'password') {
|
||||
return step6LoginFromPasswordPage(payload, transition.snapshot);
|
||||
}
|
||||
@@ -2545,11 +2695,10 @@ async function step6_login(payload) {
|
||||
const { email } = payload;
|
||||
if (!email) throw new Error('登录时缺少邮箱地址。');
|
||||
|
||||
log(`步骤 7:正在使用 ${email} 登录...`);
|
||||
|
||||
const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000));
|
||||
|
||||
if (snapshot.state === 'verification_page') {
|
||||
log('步骤 7:认证页已在登录验证码页,开始确认页面是否稳定。');
|
||||
return finalizeStep6VerificationReady({
|
||||
logLabel: '步骤 7 收尾',
|
||||
loginVerificationRequestedAt: null,
|
||||
@@ -2558,19 +2707,39 @@ async function step6_login(payload) {
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
log('步骤 7:检测到登录超时报错,准备重新执行步骤 7。', 'warn');
|
||||
return await createStep6LoginTimeoutRecoverableResult(
|
||||
log('步骤 7:检测到登录超时报错页,先尝试恢复当前页面。', 'warn');
|
||||
const transition = await createStep6LoginTimeoutRecoveryTransition(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'当前页面处于登录超时报错页。'
|
||||
'当前页面处于登录超时报错页。',
|
||||
{
|
||||
loginVerificationRequestedAt: null,
|
||||
via: 'login_timeout_initial_recovered',
|
||||
}
|
||||
);
|
||||
if (transition.action === 'done') {
|
||||
return finalizeStep6VerificationReady({
|
||||
logLabel: '步骤 7 收尾',
|
||||
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || null,
|
||||
via: transition.result.via || 'login_timeout_initial_recovered',
|
||||
});
|
||||
}
|
||||
if (transition.action === 'email') {
|
||||
return step6LoginFromEmailPage(payload, transition.snapshot);
|
||||
}
|
||||
if (transition.action === 'password') {
|
||||
return step6LoginFromPasswordPage(payload, transition.snapshot);
|
||||
}
|
||||
return transition.result;
|
||||
}
|
||||
|
||||
if (snapshot.state === 'email_page') {
|
||||
log(`步骤 7:正在使用 ${email} 登录...`);
|
||||
return step6LoginFromEmailPage(payload, snapshot);
|
||||
}
|
||||
|
||||
if (snapshot.state === 'password_page') {
|
||||
log('步骤 7:认证页已在密码页,继续当前登录流程。');
|
||||
return step6LoginFromPasswordPage(payload, snapshot);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "codex-oauth-automation-extension",
|
||||
"version": "5.8",
|
||||
"version_name": "Pro5.8",
|
||||
"version": "7.0",
|
||||
"version_name": "Pro7.0",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
dom.rowSub2ApiPassword,
|
||||
dom.rowSub2ApiGroup,
|
||||
dom.rowSub2ApiDefaultProxy,
|
||||
dom.rowCodex2ApiUrl,
|
||||
dom.rowCodex2ApiAdminKey,
|
||||
dom.rowCustomPassword,
|
||||
dom.rowAccountRunHistoryHelperBaseUrl,
|
||||
].filter(Boolean);
|
||||
|
||||
@@ -806,6 +806,17 @@ header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#row-codex2api-url .data-label,
|
||||
#row-codex2api-admin-key .data-label {
|
||||
width: 76px;
|
||||
}
|
||||
|
||||
#row-codex2api-url .data-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
@@ -2393,6 +2404,19 @@ header {
|
||||
line-height: 1.55;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 14px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.modal-message a,
|
||||
.modal-alert a {
|
||||
color: var(--blue);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.modal-message a:hover,
|
||||
.modal-alert a:hover {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.modal-alert {
|
||||
|
||||
+61
-25
@@ -112,6 +112,7 @@
|
||||
<select id="select-panel-mode" class="data-select">
|
||||
<option value="cpa">CPA 面板</option>
|
||||
<option value="sub2api">SUB2API</option>
|
||||
<option value="codex2api">Codex2API</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="contribution-mode-panel" class="contribution-mode-panel" hidden>
|
||||
@@ -192,6 +193,16 @@
|
||||
<input type="text" id="input-sub2api-default-proxy" class="data-input"
|
||||
placeholder="留空则不使用代理;或填写代理名称 / ID" />
|
||||
</div>
|
||||
<div class="data-row" id="row-codex2api-url" style="display:none;">
|
||||
<span class="data-label">Codex2API</span>
|
||||
<input type="text" id="input-codex2api-url" class="data-input"
|
||||
placeholder="http://localhost:8080/admin/accounts" />
|
||||
</div>
|
||||
<div class="data-row" id="row-codex2api-admin-key" style="display:none;">
|
||||
<span class="data-label">管理密钥</span>
|
||||
<input type="password" id="input-codex2api-admin-key" class="data-input"
|
||||
placeholder="请输入 Codex2API Admin Secret" />
|
||||
</div>
|
||||
<div class="data-row" id="row-custom-password">
|
||||
<span class="data-label">账户密码</span>
|
||||
<div class="input-with-icon">
|
||||
@@ -235,31 +246,6 @@
|
||||
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
|
||||
<span class="data-label">Temp API</span>
|
||||
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
||||
<span class="data-label">Admin Auth</span>
|
||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon" placeholder="Cloudflare Temp Email admin password" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
||||
<span class="data-label">Custom Auth</span>
|
||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon" placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
|
||||
<span class="data-label">邮件接收</span>
|
||||
<input type="text" id="input-temp-email-receive-mailbox" class="data-input" placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-domain" style="display:none;">
|
||||
<span class="data-label">Temp 域名</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-temp-email-domain" class="data-select"></select>
|
||||
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
|
||||
style="display:none;" />
|
||||
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-cf-domain" style="display:none;">
|
||||
<span class="data-label">CF 域名</span>
|
||||
<div class="data-inline">
|
||||
@@ -389,6 +375,56 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cloudflare-temp-email-section" class="data-card hotmail-card" style="display:none;">
|
||||
<div class="section-mini-header">
|
||||
<div class="section-mini-copy">
|
||||
<span class="section-label">Cloudflare Temp Email</span>
|
||||
<span class="data-value">用于生成邮箱或接收转发邮件</span>
|
||||
</div>
|
||||
<div class="section-mini-actions">
|
||||
<button id="btn-cloudflare-temp-email-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
|
||||
<button id="btn-cloudflare-temp-email-github" class="btn btn-ghost btn-xs" type="button">GitHub</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
|
||||
<span class="data-label">Temp API</span>
|
||||
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
||||
<span class="data-label">Admin Auth</span>
|
||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon" placeholder="Cloudflare Temp Email admin password" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
||||
<span class="data-label">Custom Auth</span>
|
||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon" placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
|
||||
<span class="data-label">邮件接收</span>
|
||||
<input type="text" id="input-temp-email-receive-mailbox" class="data-input" placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-random-subdomain-toggle" style="display:none;">
|
||||
<span class="data-label">随机子域</span>
|
||||
<div class="data-inline">
|
||||
<label class="toggle-switch" for="input-temp-email-use-random-subdomain" title="依赖后端 RANDOM_SUBDOMAIN_DOMAINS 配置">
|
||||
<input type="checkbox" id="input-temp-email-use-random-subdomain" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
<span>启用</span>
|
||||
</label>
|
||||
<span class="setting-caption">依赖后端 RANDOM_SUBDOMAIN_DOMAINS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-domain" style="display:none;">
|
||||
<span class="data-label">Temp 域名</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-temp-email-domain" class="data-select"></select>
|
||||
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
|
||||
style="display:none;" />
|
||||
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hotmail-section" class="data-card hotmail-card" style="display:none;">
|
||||
<div class="section-mini-header">
|
||||
<div class="section-mini-copy">
|
||||
|
||||
+212
-13
@@ -94,6 +94,10 @@ const rowSub2ApiGroup = document.getElementById('row-sub2api-group');
|
||||
const inputSub2ApiGroup = document.getElementById('input-sub2api-group');
|
||||
const rowSub2ApiDefaultProxy = document.getElementById('row-sub2api-default-proxy');
|
||||
const inputSub2ApiDefaultProxy = document.getElementById('input-sub2api-default-proxy');
|
||||
const rowCodex2ApiUrl = document.getElementById('row-codex2api-url');
|
||||
const inputCodex2ApiUrl = document.getElementById('input-codex2api-url');
|
||||
const rowCodex2ApiAdminKey = document.getElementById('row-codex2api-admin-key');
|
||||
const inputCodex2ApiAdminKey = document.getElementById('input-codex2api-admin-key');
|
||||
const rowCustomPassword = document.getElementById('row-custom-password');
|
||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||
const btnMailLogin = document.getElementById('btn-mail-login');
|
||||
@@ -110,10 +114,15 @@ const rowTempEmailCustomAuth = document.getElementById('row-temp-email-custom-au
|
||||
const inputTempEmailCustomAuth = document.getElementById('input-temp-email-custom-auth');
|
||||
const rowTempEmailReceiveMailbox = document.getElementById('row-temp-email-receive-mailbox');
|
||||
const inputTempEmailReceiveMailbox = document.getElementById('input-temp-email-receive-mailbox');
|
||||
const rowTempEmailRandomSubdomainToggle = document.getElementById('row-temp-email-random-subdomain-toggle');
|
||||
const inputTempEmailUseRandomSubdomain = document.getElementById('input-temp-email-use-random-subdomain');
|
||||
const rowTempEmailDomain = document.getElementById('row-temp-email-domain');
|
||||
const selectTempEmailDomain = document.getElementById('select-temp-email-domain');
|
||||
const inputTempEmailDomain = document.getElementById('input-temp-email-domain');
|
||||
const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode');
|
||||
const cloudflareTempEmailSection = document.getElementById('cloudflare-temp-email-section');
|
||||
const btnCloudflareTempEmailUsageGuide = document.getElementById('btn-cloudflare-temp-email-usage-guide');
|
||||
const btnCloudflareTempEmailGithub = document.getElementById('btn-cloudflare-temp-email-github');
|
||||
const hotmailSection = document.getElementById('hotmail-section');
|
||||
const mail2925Section = document.getElementById('mail2925-section');
|
||||
const luckmailSection = document.getElementById('luckmail-section');
|
||||
@@ -246,6 +255,7 @@ const DEFAULT_CPA_CALLBACK_MODE = 'step8';
|
||||
const MAIL_2925_MODE_PROVIDE = 'provide';
|
||||
const MAIL_2925_MODE_RECEIVE = 'receive';
|
||||
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
|
||||
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
|
||||
const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed';
|
||||
const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed';
|
||||
const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
|
||||
@@ -606,6 +616,10 @@ const LOG_LEVEL_LABELS = {
|
||||
error: '错误',
|
||||
};
|
||||
|
||||
const CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email';
|
||||
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
|
||||
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
|
||||
|
||||
function usesGeneratedAliasMailProvider(provider, mail2925Mode = getSelectedMail2925Mode()) {
|
||||
return isManagedAliasProvider(provider, mail2925Mode);
|
||||
}
|
||||
@@ -670,6 +684,19 @@ function resetActionModalAlert() {
|
||||
autoStartAlert.className = 'modal-alert';
|
||||
}
|
||||
|
||||
function setActionModalMessageContent({ text = '', html = '' } = {}) {
|
||||
if (!autoStartMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (html) {
|
||||
autoStartMessage.innerHTML = html;
|
||||
return;
|
||||
}
|
||||
|
||||
autoStartMessage.textContent = text;
|
||||
}
|
||||
|
||||
function resetActionModalButtons() {
|
||||
const buttons = [btnAutoStartCancel, btnAutoStartRestart, btnAutoStartContinue];
|
||||
buttons.forEach((button) => {
|
||||
@@ -745,7 +772,7 @@ function resolveModalChoice(choice) {
|
||||
}
|
||||
}
|
||||
|
||||
function openActionModal({ title, message, actions, option, alert, buildResult }) {
|
||||
function openActionModal({ title, message, messageHtml, actions, option, alert, buildResult }) {
|
||||
if (!autoStartModal) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
@@ -756,7 +783,7 @@ function openActionModal({ title, message, actions, option, alert, buildResult }
|
||||
|
||||
resetActionModalButtons();
|
||||
autoStartTitle.textContent = title;
|
||||
autoStartMessage.textContent = message;
|
||||
setActionModalMessageContent({ text: message, html: messageHtml });
|
||||
currentModalActions = actions || [];
|
||||
modalResultBuilder = typeof buildResult === 'function' ? buildResult : null;
|
||||
const buttonSlots = currentModalActions.length <= 2
|
||||
@@ -850,6 +877,59 @@ function setPromptDismissed(storageKey, dismissed) {
|
||||
}
|
||||
}
|
||||
|
||||
function isNewUserGuidePromptDismissed() {
|
||||
return isPromptDismissed(NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY);
|
||||
}
|
||||
|
||||
function setNewUserGuidePromptDismissed(dismissed) {
|
||||
setPromptDismissed(NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY, dismissed);
|
||||
}
|
||||
|
||||
function shouldPromptNewUserGuide() {
|
||||
if (isNewUserGuidePromptDismissed()) {
|
||||
return false;
|
||||
}
|
||||
if (!btnContributionMode || btnContributionMode.disabled) {
|
||||
return false;
|
||||
}
|
||||
if (latestState?.contributionMode) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getContributionPortalUrl() {
|
||||
return String(contributionContentService?.portalUrl || 'https://apikey.qzz.io').trim();
|
||||
}
|
||||
|
||||
function openNewUserGuidePrompt() {
|
||||
return openActionModal({
|
||||
title: '新手引导',
|
||||
message: '如果你是第一次使用,可以先查看贡献页里的公告和使用教程。点击“查看引导”会自动打开贡献页面。',
|
||||
alert: {
|
||||
text: '本提示仅出现一次。',
|
||||
},
|
||||
actions: [
|
||||
{ id: null, label: '取消', variant: 'btn-ghost' },
|
||||
{ id: 'confirm', label: '查看引导', variant: 'btn-primary' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function maybeShowNewUserGuidePrompt() {
|
||||
if (!shouldPromptNewUserGuide()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setNewUserGuidePromptDismissed(true);
|
||||
const choice = await openNewUserGuidePrompt();
|
||||
if (choice === 'confirm') {
|
||||
openExternalUrl(getContributionPortalUrl());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getDismissedContributionContentPromptVersion() {
|
||||
return String(localStorage.getItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY) || '').trim();
|
||||
}
|
||||
@@ -1456,6 +1536,18 @@ function setCloudflareTempEmailDomainEditMode(editing, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyCloudflareTempEmailSettingsState(state = {}) {
|
||||
inputTempEmailBaseUrl.value = state?.cloudflareTempEmailBaseUrl || '';
|
||||
inputTempEmailAdminAuth.value = state?.cloudflareTempEmailAdminAuth || '';
|
||||
inputTempEmailCustomAuth.value = state?.cloudflareTempEmailCustomAuth || '';
|
||||
inputTempEmailReceiveMailbox.value = state?.cloudflareTempEmailReceiveMailbox || '';
|
||||
if (inputTempEmailUseRandomSubdomain) {
|
||||
inputTempEmailUseRandomSubdomain.checked = Boolean(state?.cloudflareTempEmailUseRandomSubdomain);
|
||||
}
|
||||
renderCloudflareTempEmailDomainOptions(state?.cloudflareTempEmailDomain || '');
|
||||
setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
|
||||
}
|
||||
|
||||
function collectSettingsPayload() {
|
||||
const { domains, activeDomain } = getCloudflareDomainsFromState();
|
||||
const selectedCloudflareDomain = normalizeCloudflareDomainValue(
|
||||
@@ -1479,6 +1571,8 @@ function collectSettingsPayload() {
|
||||
sub2apiPassword: inputSub2ApiPassword.value,
|
||||
sub2apiGroupName: inputSub2ApiGroup.value.trim(),
|
||||
sub2apiDefaultProxyName: inputSub2ApiDefaultProxy.value.trim(),
|
||||
codex2apiUrl: inputCodex2ApiUrl.value.trim(),
|
||||
codex2apiAdminKey: inputCodex2ApiAdminKey.value.trim(),
|
||||
...(contributionModeEnabled ? {} : {
|
||||
customPassword: inputPassword.value,
|
||||
}),
|
||||
@@ -1509,6 +1603,7 @@ function collectSettingsPayload() {
|
||||
cloudflareTempEmailAdminAuth: inputTempEmailAdminAuth.value,
|
||||
cloudflareTempEmailCustomAuth: inputTempEmailCustomAuth.value,
|
||||
cloudflareTempEmailReceiveMailbox: normalizeCloudflareTempEmailReceiveMailboxValue(inputTempEmailReceiveMailbox.value),
|
||||
cloudflareTempEmailUseRandomSubdomain: Boolean(inputTempEmailUseRandomSubdomain?.checked),
|
||||
cloudflareTempEmailDomain: selectedCloudflareTempEmailDomain,
|
||||
cloudflareTempEmailDomains: tempEmailDomains,
|
||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||
@@ -1859,6 +1954,8 @@ function applySettingsState(state) {
|
||||
inputSub2ApiPassword.value = state?.sub2apiPassword || '';
|
||||
inputSub2ApiGroup.value = state?.sub2apiGroupName || '';
|
||||
inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || '';
|
||||
inputCodex2ApiUrl.value = state?.codex2apiUrl || '';
|
||||
inputCodex2ApiAdminKey.value = state?.codex2apiAdminKey || '';
|
||||
const restoredMailProvider = isCustomMailProvider(state?.mailProvider)
|
||||
|| [ICLOUD_PROVIDER, 'hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim())
|
||||
? String(state?.mailProvider || '163').trim()
|
||||
@@ -1913,14 +2010,9 @@ function applySettingsState(state) {
|
||||
inputLuckmailBaseUrl.value = normalizeLuckmailBaseUrl(state?.luckmailBaseUrl);
|
||||
selectLuckmailEmailType.value = normalizeLuckmailEmailType(state?.luckmailEmailType);
|
||||
inputLuckmailDomain.value = state?.luckmailDomain || '';
|
||||
inputTempEmailBaseUrl.value = state?.cloudflareTempEmailBaseUrl || '';
|
||||
inputTempEmailAdminAuth.value = state?.cloudflareTempEmailAdminAuth || '';
|
||||
inputTempEmailCustomAuth.value = state?.cloudflareTempEmailCustomAuth || '';
|
||||
inputTempEmailReceiveMailbox.value = state?.cloudflareTempEmailReceiveMailbox || '';
|
||||
applyCloudflareTempEmailSettingsState(state);
|
||||
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
|
||||
setCloudflareDomainEditMode(false, { clearInput: true });
|
||||
renderCloudflareTempEmailDomainOptions(state?.cloudflareTempEmailDomain || '');
|
||||
setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
|
||||
inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.value = String(normalizeAutoRunThreadIntervalMinutes(state?.autoRunFallbackThreadIntervalMinutes));
|
||||
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
|
||||
@@ -2039,6 +2131,51 @@ function openReleaseListPage() {
|
||||
openExternalUrl(getReleaseListUrl());
|
||||
}
|
||||
|
||||
function buildCloudflareTempEmailUsageGuideModalConfig() {
|
||||
const useCloudflareTempEmailProvider = String(selectMailProvider?.value || '').trim().toLowerCase() === 'cloudflare-temp-email';
|
||||
const useCloudflareTempEmailGenerator = getSelectedEmailGenerator() === 'cloudflare-temp-email';
|
||||
let alertText = '当前还没有把 Cloudflare Temp Email 选为邮箱服务或邮箱生成器。下面说明同时覆盖“生成邮箱”和“接收转发邮件”两种用法。';
|
||||
if (useCloudflareTempEmailProvider && useCloudflareTempEmailGenerator) {
|
||||
alertText = '当前同时把 Cloudflare Temp Email 用作“邮箱服务”和“邮箱生成器”。请同时配置生成邮箱和接收转发两套必填项。';
|
||||
} else if (useCloudflareTempEmailProvider) {
|
||||
alertText = '当前把 Cloudflare Temp Email 用作“邮箱服务”。重点填写 Temp API、Custom Auth 和 邮件接收。';
|
||||
} else if (useCloudflareTempEmailGenerator) {
|
||||
alertText = '当前把 Cloudflare Temp Email 用作“邮箱生成器”。重点填写 Temp API、Admin Auth 和 Temp 域名;随机子域按需开启。';
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Cloudflare Temp Email 使用教程',
|
||||
alert: { text: alertText },
|
||||
messageHtml: `Temp API:填写 Cloudflare Temp Email 后端地址。<br><br>
|
||||
Admin Auth:填写后端 admin auth。<br>
|
||||
仅在“邮箱生成 = Cloudflare Temp Email”时必填。<br><br>
|
||||
Custom Auth:仅当站点额外开启访问密码时填写。没有开启就留空。<br><br>
|
||||
Temp 域名:填写允许创建的基础域名。启用随机子域时,这里仍然填写基础域名。<br><br>
|
||||
随机子域:仅在“邮箱生成 = Cloudflare Temp Email”时使用。<br>
|
||||
后端需要已配置 RANDOM_SUBDOMAIN_DOMAINS。<br>
|
||||
CF DNS 需要设置 MX *,参考 <a href="${CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL}" target="_blank" rel="noopener noreferrer">Issue #942</a>。<br><br>
|
||||
邮件接收:仅在“邮箱服务 = Cloudflare Temp Email”时填写。<br>
|
||||
这里填写真正接收转发邮件的目标邮箱。<br><br>
|
||||
搭建教程:<a href="${CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL}" target="_blank" rel="noopener noreferrer">LINUX DO 教程</a>。`,
|
||||
};
|
||||
}
|
||||
|
||||
function showCloudflareTempEmailUsageGuide() {
|
||||
const modalConfig = buildCloudflareTempEmailUsageGuideModalConfig();
|
||||
return openActionModal({
|
||||
title: modalConfig.title,
|
||||
messageHtml: modalConfig.messageHtml,
|
||||
alert: modalConfig.alert,
|
||||
actions: [
|
||||
{ id: 'ok', label: '知道了', variant: 'btn-primary' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function openCloudflareTempEmailRepositoryPage() {
|
||||
openExternalUrl(CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL);
|
||||
}
|
||||
|
||||
function createUpdateNoteList(notes = []) {
|
||||
if (!Array.isArray(notes) || notes.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
@@ -2673,10 +2810,14 @@ function updateMailProviderUI() {
|
||||
const showCloudflareDomain = useEmailGenerator && useCloudflare;
|
||||
const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator);
|
||||
const showCloudflareTempEmailReceiveMailbox = useCloudflareTempEmailProvider && !useCloudflareTempEmailGenerator;
|
||||
const showCloudflareTempEmailRandomSubdomainToggle = useEmailGenerator && useCloudflareTempEmailGenerator;
|
||||
const showCloudflareTempEmailDomain = useEmailGenerator && useCloudflareTempEmailGenerator;
|
||||
if (rowEmailGenerator) {
|
||||
rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none';
|
||||
}
|
||||
if (cloudflareTempEmailSection) {
|
||||
cloudflareTempEmailSection.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
}
|
||||
if (icloudSection) {
|
||||
const showIcloudSection = (useEmailGenerator && useIcloud) || useIcloudProvider;
|
||||
icloudSection.style.display = showIcloudSection ? '' : 'none';
|
||||
@@ -2698,6 +2839,9 @@ function updateMailProviderUI() {
|
||||
rowTempEmailAdminAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
rowTempEmailCustomAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
rowTempEmailReceiveMailbox.style.display = showCloudflareTempEmailReceiveMailbox ? '' : 'none';
|
||||
if (rowTempEmailRandomSubdomainToggle) {
|
||||
rowTempEmailRandomSubdomainToggle.style.display = showCloudflareTempEmailRandomSubdomainToggle ? '' : 'none';
|
||||
}
|
||||
rowTempEmailDomain.style.display = showCloudflareTempEmailDomain ? '' : 'none';
|
||||
const { domains: tempEmailDomains } = getCloudflareTempEmailDomainsFromState();
|
||||
if (showCloudflareTempEmailDomain) {
|
||||
@@ -2785,6 +2929,9 @@ function updateMailProviderUI() {
|
||||
if (autoHintText && showCloudflareTempEmailReceiveMailbox) {
|
||||
autoHintText.textContent = '若注册邮箱会转发到 Cloudflare Temp Email,请在“邮件接收”中填写实际接收转发邮件的邮箱。';
|
||||
}
|
||||
if (autoHintText && showCloudflareTempEmailRandomSubdomainToggle && inputTempEmailUseRandomSubdomain?.checked) {
|
||||
autoHintText.textContent = '已启用随机子域名:扩展会按当前选中的 Temp 域名提交,并额外携带 enableRandomSubdomain;是否生效取决于后端 RANDOM_SUBDOMAIN_DOMAINS 配置。';
|
||||
}
|
||||
if (useHotmail) {
|
||||
inputEmail.value = getCurrentHotmailEmail();
|
||||
} else if (useLuckmail) {
|
||||
@@ -2865,18 +3012,24 @@ async function saveCloudflareTempEmailDomainSettings(domains, activeDomain, opti
|
||||
|
||||
function updatePanelModeUI() {
|
||||
const useSub2Api = selectPanelMode.value === 'sub2api';
|
||||
rowVpsUrl.style.display = useSub2Api ? 'none' : '';
|
||||
rowVpsPassword.style.display = useSub2Api ? 'none' : '';
|
||||
rowLocalCpaStep9Mode.style.display = useSub2Api ? 'none' : '';
|
||||
const useCodex2Api = selectPanelMode.value === 'codex2api';
|
||||
const useCpa = !useSub2Api && !useCodex2Api;
|
||||
rowVpsUrl.style.display = useCpa ? '' : 'none';
|
||||
rowVpsPassword.style.display = useCpa ? '' : 'none';
|
||||
rowLocalCpaStep9Mode.style.display = useCpa ? '' : 'none';
|
||||
rowSub2ApiUrl.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiEmail.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiPassword.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiGroup.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiDefaultProxy.style.display = useSub2Api ? '' : 'none';
|
||||
rowCodex2ApiUrl.style.display = useCodex2Api ? '' : 'none';
|
||||
rowCodex2ApiAdminKey.style.display = useCodex2Api ? '' : 'none';
|
||||
|
||||
const step9Btn = document.querySelector('.step-btn[data-step-key="platform-verify"]');
|
||||
if (step9Btn) {
|
||||
step9Btn.textContent = useSub2Api ? 'SUB2API 回调验证' : 'CPA 回调验证';
|
||||
step9Btn.textContent = useSub2Api
|
||||
? 'SUB2API 回调验证'
|
||||
: (useCodex2Api ? 'Codex2API 回调验证' : 'CPA 回调验证');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3777,6 +3930,14 @@ btnRepoHome?.addEventListener('click', () => {
|
||||
openRepositoryHomePage();
|
||||
});
|
||||
|
||||
btnCloudflareTempEmailUsageGuide?.addEventListener('click', () => {
|
||||
showCloudflareTempEmailUsageGuide();
|
||||
});
|
||||
|
||||
btnCloudflareTempEmailGithub?.addEventListener('click', () => {
|
||||
openCloudflareTempEmailRepositoryPage();
|
||||
});
|
||||
|
||||
extensionUpdateStatus?.addEventListener('click', () => {
|
||||
openReleaseListPage();
|
||||
});
|
||||
@@ -4271,6 +4432,22 @@ inputSub2ApiDefaultProxy.addEventListener('blur', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputCodex2ApiUrl.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
inputCodex2ApiUrl.addEventListener('blur', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputCodex2ApiAdminKey.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
inputCodex2ApiAdminKey.addEventListener('blur', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputEmailPrefix.addEventListener('input', () => {
|
||||
maybeClearGeneratedAliasAfterEmailPrefixChange().catch(() => { });
|
||||
syncManagedAliasBaseEmailDraftFromInput();
|
||||
@@ -4388,6 +4565,13 @@ inputTempEmailReceiveMailbox.addEventListener('blur', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputTempEmailUseRandomSubdomain?.addEventListener('change', () => {
|
||||
updateMailProviderUI();
|
||||
clearRegistrationEmail({ silent: true }).catch(() => { });
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
@@ -4610,9 +4794,19 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message.payload.cloudflareTempEmailReceiveMailbox !== undefined) {
|
||||
inputTempEmailReceiveMailbox.value = message.payload.cloudflareTempEmailReceiveMailbox || '';
|
||||
}
|
||||
if (message.payload.cloudflareTempEmailUseRandomSubdomain !== undefined && inputTempEmailUseRandomSubdomain) {
|
||||
inputTempEmailUseRandomSubdomain.checked = Boolean(message.payload.cloudflareTempEmailUseRandomSubdomain);
|
||||
}
|
||||
if (message.payload.cloudflareTempEmailDomain !== undefined || message.payload.cloudflareTempEmailDomains !== undefined) {
|
||||
renderCloudflareTempEmailDomainOptions(message.payload.cloudflareTempEmailDomain || latestState?.cloudflareTempEmailDomain || '');
|
||||
}
|
||||
if (
|
||||
message.payload.cloudflareTempEmailUseRandomSubdomain !== undefined
|
||||
|| message.payload.cloudflareTempEmailDomain !== undefined
|
||||
|| message.payload.cloudflareTempEmailDomains !== undefined
|
||||
) {
|
||||
updateMailProviderUI();
|
||||
}
|
||||
if (message.payload.currentHotmailAccountId !== undefined || message.payload.hotmailAccounts !== undefined) {
|
||||
renderHotmailAccounts();
|
||||
if (selectMailProvider.value === 'hotmail-api') {
|
||||
@@ -4808,7 +5002,12 @@ restoreState().then(() => {
|
||||
updatePanelModeUI();
|
||||
updateButtonStates();
|
||||
updateStatusDisplay(latestState);
|
||||
return refreshContributionContentHint();
|
||||
return refreshContributionContentHint()
|
||||
.catch((error) => {
|
||||
console.warn('Failed to refresh contribution content hint during initialization:', error);
|
||||
return null;
|
||||
})
|
||||
.then(() => maybeShowNewUserGuidePrompt());
|
||||
}).catch((err) => {
|
||||
console.error('Failed to initialize sidepanel state:', err);
|
||||
});
|
||||
|
||||
@@ -50,6 +50,7 @@ function extractFunction(name) {
|
||||
|
||||
test('background account history settings are normalized independently from hotmail service mode', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeCodex2ApiUrl'),
|
||||
extractFunction('normalizeHotmailLocalBaseUrl'),
|
||||
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
|
||||
extractFunction('normalizeVerificationResendCount'),
|
||||
@@ -60,6 +61,7 @@ test('background account history settings are normalized independently from hotm
|
||||
const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
|
||||
const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL;
|
||||
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
|
||||
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const DEFAULT_SUB2API_PROXY_NAME = '';
|
||||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||||
@@ -70,7 +72,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: '163',
|
||||
};
|
||||
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
|
||||
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
|
||||
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
|
||||
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
|
||||
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
|
||||
@@ -118,4 +120,16 @@ return {
|
||||
api.normalizePersistentSettingValue('sub2apiDefaultProxyName', ' proxy-a '),
|
||||
'proxy-a'
|
||||
);
|
||||
assert.equal(
|
||||
api.normalizePersistentSettingValue('codex2apiUrl', 'localhost:8080/admin'),
|
||||
'http://localhost:8080/admin/accounts'
|
||||
);
|
||||
assert.equal(
|
||||
api.normalizePersistentSettingValue('codex2apiUrl', 'https://codex-admin.example.com/'),
|
||||
'https://codex-admin.example.com/admin/accounts'
|
||||
);
|
||||
assert.equal(
|
||||
api.normalizePersistentSettingValue('codex2apiAdminKey', ' secret-key '),
|
||||
'secret-key'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('cloudflare temp email settings normalize and expose the random-subdomain toggle', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeCloudflareTempEmailReceiveMailbox'),
|
||||
extractFunction('getCloudflareTempEmailConfig'),
|
||||
extractFunction('normalizePersistentSettingValue'),
|
||||
extractFunction('buildPersistentSettingsPayload'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PERSISTED_SETTING_DEFAULTS = {
|
||||
panelMode: 'cpa',
|
||||
autoStepDelaySeconds: null,
|
||||
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
|
||||
mailProvider: '163',
|
||||
mail2925Mode: 'provide',
|
||||
emailGenerator: 'duck',
|
||||
autoDeleteUsedIcloudAlias: false,
|
||||
accountRunHistoryTextEnabled: false,
|
||||
cloudflareTempEmailUseRandomSubdomain: false,
|
||||
cloudflareTempEmailDomain: '',
|
||||
cloudflareTempEmailDomains: [],
|
||||
};
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
|
||||
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
|
||||
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
|
||||
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
|
||||
function normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
|
||||
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
|
||||
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
|
||||
function normalizeMail2925Mode(value) { return String(value || '').trim().toLowerCase() === 'receive' ? 'receive' : 'provide'; }
|
||||
function normalizeEmailGenerator(value) { return String(value || '').trim().toLowerCase() || 'duck'; }
|
||||
function normalizeIcloudHost(value) { const normalized = String(value || '').trim().toLowerCase(); return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; }
|
||||
function normalizeAccountRunHistoryHelperBaseUrl(value) { return String(value || '').trim(); }
|
||||
function normalizeHotmailServiceMode(value) { return String(value || '').trim().toLowerCase() === 'remote' ? 'remote' : 'local'; }
|
||||
function normalizeHotmailRemoteBaseUrl(value) { return String(value || '').trim(); }
|
||||
function normalizeHotmailLocalBaseUrl(value) { return String(value || '').trim(); }
|
||||
function normalizeCloudflareDomain(value) { return String(value || '').trim().toLowerCase(); }
|
||||
function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : []; }
|
||||
function normalizeCloudflareTempEmailBaseUrl(value) { return String(value || '').trim(); }
|
||||
function normalizeCloudflareTempEmailAddress(value = '') { return String(value || '').trim().toLowerCase(); }
|
||||
function normalizeCloudflareTempEmailDomain(value) { return String(value || '').trim().toLowerCase(); }
|
||||
function normalizeCloudflareTempEmailDomains(value) {
|
||||
const seen = new Set();
|
||||
const domains = [];
|
||||
for (const item of Array.isArray(value) ? value : []) {
|
||||
const normalized = normalizeCloudflareTempEmailDomain(item);
|
||||
if (!normalized || seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
domains.push(normalized);
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
function normalizeHotmailAccounts(value) { return Array.isArray(value) ? value : []; }
|
||||
function normalizeMail2925Accounts(value) { return Array.isArray(value) ? value : []; }
|
||||
function resolveLegacyAutoStepDelaySeconds() { return undefined; }
|
||||
${bundle}
|
||||
return {
|
||||
buildPersistentSettingsPayload,
|
||||
getCloudflareTempEmailConfig,
|
||||
normalizePersistentSettingValue,
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.normalizePersistentSettingValue('cloudflareTempEmailUseRandomSubdomain', 1), true);
|
||||
|
||||
const payload = api.buildPersistentSettingsPayload({
|
||||
cloudflareTempEmailUseRandomSubdomain: true,
|
||||
cloudflareTempEmailDomain: 'mail.example.com',
|
||||
cloudflareTempEmailDomains: ['mail.example.com', 'alt.example.com'],
|
||||
});
|
||||
assert.equal(payload.cloudflareTempEmailUseRandomSubdomain, true);
|
||||
assert.equal(payload.cloudflareTempEmailDomain, 'mail.example.com');
|
||||
assert.deepEqual(payload.cloudflareTempEmailDomains, ['mail.example.com', 'alt.example.com']);
|
||||
|
||||
const config = api.getCloudflareTempEmailConfig({
|
||||
cloudflareTempEmailBaseUrl: 'https://temp.example.com',
|
||||
cloudflareTempEmailAdminAuth: 'admin-secret',
|
||||
cloudflareTempEmailCustomAuth: 'custom-secret',
|
||||
cloudflareTempEmailReceiveMailbox: 'Forward@Example.com',
|
||||
cloudflareTempEmailUseRandomSubdomain: true,
|
||||
cloudflareTempEmailDomain: 'mail.example.com',
|
||||
cloudflareTempEmailDomains: ['mail.example.com'],
|
||||
});
|
||||
assert.deepEqual(config, {
|
||||
baseUrl: 'https://temp.example.com',
|
||||
adminAuth: 'admin-secret',
|
||||
customAuth: 'custom-secret',
|
||||
receiveMailbox: 'forward@example.com',
|
||||
useRandomSubdomain: true,
|
||||
domain: 'mail.example.com',
|
||||
domains: ['mail.example.com'],
|
||||
});
|
||||
});
|
||||
@@ -537,7 +537,7 @@ return { refreshOAuthUrlBeforeStep6 };
|
||||
delete globalThis.LOG_PREFIX;
|
||||
});
|
||||
|
||||
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API path explicitly when contributionMode=false', async () => {
|
||||
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API/Codex2API path explicitly when contributionMode=false', async () => {
|
||||
const bundle = extractFunction(backgroundSource, 'refreshOAuthUrlBeforeStep6');
|
||||
const calls = [];
|
||||
|
||||
@@ -575,7 +575,7 @@ return { refreshOAuthUrlBeforeStep6 };
|
||||
|
||||
assert.equal(oauthUrl, 'https://panel.example.com/oauth');
|
||||
assert.deepStrictEqual(calls, [
|
||||
{ type: 'log', message: '步骤 7:contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' },
|
||||
{ type: 'log', message: '步骤 7:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' },
|
||||
{ type: 'panel' },
|
||||
{
|
||||
type: 'step',
|
||||
|
||||
@@ -2,24 +2,25 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadGeneratedEmailHelpersApi() {
|
||||
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
|
||||
}
|
||||
|
||||
test('background imports generated email helper module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /importScripts\([\s\S]*'background\/generated-email-helpers\.js'/);
|
||||
});
|
||||
|
||||
test('generated email helper module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
|
||||
const api = loadGeneratedEmailHelpersApi();
|
||||
|
||||
assert.equal(typeof api?.createGeneratedEmailHelpers, 'function');
|
||||
});
|
||||
|
||||
test('generated email helper falls back to normal generator when 2925 is in receive mode', async () => {
|
||||
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
|
||||
const api = loadGeneratedEmailHelpersApi();
|
||||
const events = [];
|
||||
|
||||
const helpers = api.createGeneratedEmailHelpers({
|
||||
@@ -76,3 +77,157 @@ test('generated email helper falls back to normal generator when 2925 is in rece
|
||||
['email', 'duck@example.com'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('generated email helper uses the regular temp email domain when random subdomain mode is disabled', async () => {
|
||||
const api = loadGeneratedEmailHelpersApi();
|
||||
const requests = [];
|
||||
const savedEmails = [];
|
||||
|
||||
const helpers = api.createGeneratedEmailHelpers({
|
||||
addLog: async () => {},
|
||||
buildGeneratedAliasEmail: () => {
|
||||
throw new Error('should not build managed alias');
|
||||
},
|
||||
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
|
||||
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
|
||||
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
|
||||
fetch: async (url, options = {}) => {
|
||||
requests.push({
|
||||
url,
|
||||
method: options.method,
|
||||
body: options.body ? JSON.parse(options.body) : null,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({ address: 'user@mail.example.com' }),
|
||||
};
|
||||
},
|
||||
fetchIcloudHideMyEmail: async () => {
|
||||
throw new Error('should not use icloud generator');
|
||||
},
|
||||
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
|
||||
getCloudflareTempEmailConfig: () => ({
|
||||
baseUrl: 'https://temp.example.com',
|
||||
adminAuth: 'admin-secret',
|
||||
customAuth: '',
|
||||
useRandomSubdomain: false,
|
||||
domain: 'mail.example.com',
|
||||
}),
|
||||
getState: async () => ({
|
||||
mailProvider: '163',
|
||||
emailGenerator: 'cloudflare-temp-email',
|
||||
}),
|
||||
ensureMail2925AccountForFlow: async () => {
|
||||
throw new Error('should not allocate mail2925 account');
|
||||
},
|
||||
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
|
||||
normalizeCloudflareDomain: () => '',
|
||||
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
|
||||
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
|
||||
isGeneratedAliasProvider: () => false,
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScript: async () => {
|
||||
throw new Error('should not use duck generator');
|
||||
},
|
||||
setEmailState: async (email) => {
|
||||
savedEmails.push(email);
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const email = await helpers.fetchGeneratedEmail({
|
||||
emailGenerator: 'cloudflare-temp-email',
|
||||
}, {
|
||||
generator: 'cloudflare-temp-email',
|
||||
});
|
||||
|
||||
assert.equal(email, 'user@mail.example.com');
|
||||
assert.deepEqual(savedEmails, ['user@mail.example.com']);
|
||||
assert.equal(requests.length, 1);
|
||||
assert.equal(requests[0].url, 'https://temp.example.com/admin/new_address');
|
||||
assert.equal(requests[0].method, 'POST');
|
||||
assert.deepEqual(requests[0].body, {
|
||||
enablePrefix: true,
|
||||
enableRandomSubdomain: false,
|
||||
name: requests[0].body.name,
|
||||
domain: 'mail.example.com',
|
||||
});
|
||||
assert.match(requests[0].body.name, /^[a-z0-9]+$/);
|
||||
});
|
||||
|
||||
test('generated email helper requests random subdomain creation while preserving the returned address', async () => {
|
||||
const api = loadGeneratedEmailHelpersApi();
|
||||
const requests = [];
|
||||
const savedEmails = [];
|
||||
|
||||
const helpers = api.createGeneratedEmailHelpers({
|
||||
addLog: async () => {},
|
||||
buildGeneratedAliasEmail: () => {
|
||||
throw new Error('should not build managed alias');
|
||||
},
|
||||
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
|
||||
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
|
||||
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
|
||||
fetch: async (url, options = {}) => {
|
||||
requests.push({
|
||||
url,
|
||||
method: options.method,
|
||||
body: options.body ? JSON.parse(options.body) : null,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({ address: 'user@a1b2c3d4.example.com' }),
|
||||
};
|
||||
},
|
||||
fetchIcloudHideMyEmail: async () => {
|
||||
throw new Error('should not use icloud generator');
|
||||
},
|
||||
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
|
||||
getCloudflareTempEmailConfig: () => ({
|
||||
baseUrl: 'https://temp.example.com',
|
||||
adminAuth: 'admin-secret',
|
||||
customAuth: '',
|
||||
useRandomSubdomain: true,
|
||||
domain: 'mail.example.com',
|
||||
}),
|
||||
getState: async () => ({
|
||||
mailProvider: '163',
|
||||
emailGenerator: 'cloudflare-temp-email',
|
||||
}),
|
||||
ensureMail2925AccountForFlow: async () => {
|
||||
throw new Error('should not allocate mail2925 account');
|
||||
},
|
||||
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
|
||||
normalizeCloudflareDomain: () => '',
|
||||
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
|
||||
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
|
||||
isGeneratedAliasProvider: () => false,
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScript: async () => {
|
||||
throw new Error('should not use duck generator');
|
||||
},
|
||||
setEmailState: async (email) => {
|
||||
savedEmails.push(email);
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const email = await helpers.fetchGeneratedEmail({
|
||||
emailGenerator: 'cloudflare-temp-email',
|
||||
}, {
|
||||
generator: 'cloudflare-temp-email',
|
||||
localPart: 'user',
|
||||
});
|
||||
|
||||
assert.equal(email, 'user@a1b2c3d4.example.com');
|
||||
assert.deepEqual(savedEmails, ['user@a1b2c3d4.example.com']);
|
||||
assert.equal(requests.length, 1);
|
||||
assert.equal(requests[0].url, 'https://temp.example.com/admin/new_address');
|
||||
assert.equal(requests[0].method, 'POST');
|
||||
assert.deepEqual(requests[0].body, {
|
||||
enablePrefix: true,
|
||||
enableRandomSubdomain: true,
|
||||
name: 'user',
|
||||
domain: 'mail.example.com',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -267,6 +267,91 @@ test('ensureMail2925MailboxSession logs in when login page is detected and accou
|
||||
assert.equal(result.result.loggedIn, true);
|
||||
});
|
||||
|
||||
test('ensureMail2925MailboxSession recovers after login-page navigation reload breaks the old content-script channel', async () => {
|
||||
let currentState = {
|
||||
autoRunning: false,
|
||||
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
|
||||
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
|
||||
]),
|
||||
currentMail2925AccountId: 'acc-1',
|
||||
};
|
||||
let tabUrl = 'https://2925.com/login/';
|
||||
const events = {
|
||||
logs: [],
|
||||
readyCalls: 0,
|
||||
sendCalls: 0,
|
||||
waitCompleteCalls: 0,
|
||||
};
|
||||
|
||||
const manager = api.createMail2925SessionManager({
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
broadcastDataUpdate: () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async () => ({ id: 9, url: tabUrl, status: tabUrl.includes('/#/mailList') ? 'complete' : 'loading' }),
|
||||
},
|
||||
cookies: {
|
||||
getAll: async () => [],
|
||||
remove: async () => ({ ok: true }),
|
||||
},
|
||||
browsingData: {
|
||||
removeCookies: async () => {},
|
||||
},
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {
|
||||
events.readyCalls += 1;
|
||||
},
|
||||
findMail2925Account: mail2925Utils.findMail2925Account,
|
||||
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
|
||||
getState: async () => currentState,
|
||||
isAutoRunLockedState: () => false,
|
||||
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
|
||||
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
|
||||
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
|
||||
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
|
||||
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
|
||||
reuseOrCreateTab: async () => 9,
|
||||
sendToContentScriptResilient: async () => {
|
||||
events.sendCalls += 1;
|
||||
if (events.sendCalls === 1) {
|
||||
throw new Error('Could not establish connection. Receiving end does not exist.');
|
||||
}
|
||||
return { loggedIn: true, currentView: 'mailbox', mailboxEmail: 'acc1@2925.com' };
|
||||
},
|
||||
setPersistentSettings: async (payload) => {
|
||||
currentState = { ...currentState, ...payload };
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
|
||||
waitForTabComplete: async () => {
|
||||
events.waitCompleteCalls += 1;
|
||||
tabUrl = 'https://2925.com/#/mailList';
|
||||
return { id: 9, url: tabUrl, status: 'complete' };
|
||||
},
|
||||
});
|
||||
|
||||
const result = await manager.ensureMail2925MailboxSession({
|
||||
accountId: 'acc-1',
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: true,
|
||||
actionLabel: '步骤 4:确认 2925 邮箱登录态',
|
||||
});
|
||||
|
||||
assert.equal(events.sendCalls, 2);
|
||||
assert.equal(events.readyCalls, 2);
|
||||
assert.equal(events.waitCompleteCalls, 1);
|
||||
assert.equal(result.result.loggedIn, true);
|
||||
const combinedLogs = events.logs.map(({ message }) => message).join('\n');
|
||||
assert.match(combinedLogs, /登录提交后页面发生跳转或重载/);
|
||||
assert.match(combinedLogs, /登录跳转恢复后当前标签地址:https:\/\/2925\.com\/#\/mailList/);
|
||||
assert.match(combinedLogs, /页面恢复完成,正在重新确认登录态/);
|
||||
});
|
||||
|
||||
test('ensureMail2925MailboxSession relogs with selected account when mailbox page email mismatches and pool is on', async () => {
|
||||
let currentState = {
|
||||
autoRunning: false,
|
||||
@@ -426,3 +511,69 @@ test('ensureMail2925MailboxSession stops when mailbox page email mismatches and
|
||||
assert.equal(stopCalls.length, 1);
|
||||
assert.match(stopCalls[0].logMessage, /与目标账号 target@2925\.com 不一致/);
|
||||
});
|
||||
|
||||
test('ensureMail2925MailboxSession does not crash when mailbox page is reused but top email cannot be detected', async () => {
|
||||
let currentState = {
|
||||
autoRunning: false,
|
||||
mail2925UseAccountPool: false,
|
||||
mail2925Accounts: [],
|
||||
currentMail2925AccountId: null,
|
||||
};
|
||||
let sendCalls = 0;
|
||||
|
||||
const manager = api.createMail2925SessionManager({
|
||||
addLog: async () => {},
|
||||
broadcastDataUpdate: () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async () => ({ id: 9, url: 'https://2925.com/#/mailList' }),
|
||||
},
|
||||
cookies: {
|
||||
getAll: async () => [],
|
||||
remove: async () => ({ ok: true }),
|
||||
},
|
||||
browsingData: {
|
||||
removeCookies: async () => {},
|
||||
},
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
findMail2925Account: mail2925Utils.findMail2925Account,
|
||||
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
|
||||
getState: async () => currentState,
|
||||
isAutoRunLockedState: () => false,
|
||||
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
|
||||
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
|
||||
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
|
||||
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
|
||||
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
|
||||
reuseOrCreateTab: async () => 9,
|
||||
sendToMailContentScriptResilient: async () => {
|
||||
sendCalls += 1;
|
||||
return {
|
||||
loggedIn: true,
|
||||
currentView: 'mailbox',
|
||||
mailboxEmail: '',
|
||||
};
|
||||
},
|
||||
setPersistentSettings: async (payload) => {
|
||||
currentState = { ...currentState, ...payload };
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
|
||||
});
|
||||
|
||||
const result = await manager.ensureMail2925MailboxSession({
|
||||
accountId: null,
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: false,
|
||||
expectedMailboxEmail: 'target@2925.com',
|
||||
actionLabel: '步骤 4:确认 2925 邮箱登录态',
|
||||
});
|
||||
|
||||
assert.equal(sendCalls, 1);
|
||||
assert.equal(result.account, null);
|
||||
assert.equal(result.result.usedExistingSession, true);
|
||||
});
|
||||
|
||||
@@ -8,11 +8,13 @@ test('background mail2925 session uses /login/ as relogin entry url', () => {
|
||||
assert.match(source, /const MAIL2925_LOGIN_URL = 'https:\/\/2925\.com\/login\/';/);
|
||||
});
|
||||
|
||||
test('background mail2925 session keeps login message timeout above the 40-second mailbox wait', () => {
|
||||
test('background mail2925 session keeps a long login response timeout and a separate page-recovery window', () => {
|
||||
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
|
||||
assert.match(source, /timeoutMs:\s*50000,/);
|
||||
assert.match(source, /responseTimeoutMs:\s*50000,/);
|
||||
assert.match(source, /40 秒内未进入收件箱/);
|
||||
assert.match(source, /const MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS = 15000;/);
|
||||
assert.match(source, /const MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS = 120000;/);
|
||||
assert.match(source, /const MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS = 120000;/);
|
||||
assert.match(source, /responseTimeoutMs:\s*MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS,/);
|
||||
assert.match(source, /recoverMail2925LoginPageAfterTransportError/);
|
||||
});
|
||||
|
||||
test('ensureMail2925MailboxSession waits 3 seconds before and after opening login page on force relogin', async () => {
|
||||
|
||||
@@ -15,6 +15,8 @@ function createRouter(overrides = {}) {
|
||||
notifyCompletions: [],
|
||||
notifyErrors: [],
|
||||
securityBlocks: [],
|
||||
invalidations: [],
|
||||
executedSteps: [],
|
||||
};
|
||||
|
||||
const router = api.createMessageRouter({
|
||||
@@ -41,7 +43,9 @@ function createRouter(overrides = {}) {
|
||||
disableUsedLuckmailPurchases: async () => {},
|
||||
doesStepUseCompletionSignal: () => false,
|
||||
ensureManualInteractionAllowed: async () => ({}),
|
||||
executeStep: async () => {},
|
||||
executeStep: async (step) => {
|
||||
events.executedSteps.push(step);
|
||||
},
|
||||
executeStepViaCompletionSignal: async () => {},
|
||||
exportSettingsBundle: async () => ({}),
|
||||
fetchGeneratedEmail: async () => '',
|
||||
@@ -55,6 +59,7 @@ function createRouter(overrides = {}) {
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getSourceLabel: () => '',
|
||||
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
|
||||
getTabId: overrides.getTabId || (async () => null),
|
||||
getStopRequested: () => false,
|
||||
handleAutoRunLoopUnhandledError: async () => {},
|
||||
handleCloudflareSecurityBlocked: overrides.handleCloudflareSecurityBlocked || (async (error) => {
|
||||
@@ -63,13 +68,16 @@ function createRouter(overrides = {}) {
|
||||
return message.replace(/^CF_SECURITY_BLOCKED::/, '') || message;
|
||||
}),
|
||||
importSettingsBundle: async () => {},
|
||||
invalidateDownstreamAfterStepRestart: async () => {},
|
||||
invalidateDownstreamAfterStepRestart: async (step, options) => {
|
||||
events.invalidations.push({ step, options });
|
||||
},
|
||||
isCloudflareSecurityBlockedError: overrides.isCloudflareSecurityBlockedError || ((error) => /^CF_SECURITY_BLOCKED::/.test(typeof error === 'string' ? error : error?.message || '')),
|
||||
isAutoRunLockedState: () => false,
|
||||
isHotmailProvider: () => false,
|
||||
isLocalhostOAuthCallbackUrl: () => true,
|
||||
isLuckmailProvider: () => false,
|
||||
isStopError: () => false,
|
||||
isTabAlive: overrides.isTabAlive || (async () => false),
|
||||
launchAutoRunTimerPlan: async () => {},
|
||||
listIcloudAliases: async () => [],
|
||||
listLuckmailPurchasesForManagement: async () => [],
|
||||
@@ -226,3 +234,22 @@ test('message router stops the flow and surfaces cloudflare security block error
|
||||
error: '您已触发Cloudflare 安全防护系统',
|
||||
});
|
||||
});
|
||||
|
||||
test('message router blocks manual step 4 execution when signup page tab is missing', async () => {
|
||||
const { router, events } = createRouter({
|
||||
getTabId: async () => null,
|
||||
isTabAlive: async () => false,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => router.handleMessage({
|
||||
type: 'EXECUTE_STEP',
|
||||
source: 'sidepanel',
|
||||
payload: { step: 4 },
|
||||
}, {}),
|
||||
/手动执行步骤 4 前,请先执行步骤 1 或步骤 2/
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(events.invalidations, []);
|
||||
assert.deepStrictEqual(events.executedSteps, []);
|
||||
});
|
||||
|
||||
@@ -15,3 +15,23 @@ test('navigation utils module exposes a factory', () => {
|
||||
|
||||
assert.equal(typeof api?.createNavigationUtils, 'function');
|
||||
});
|
||||
|
||||
test('navigation utils support codex2api mode and url normalization', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
|
||||
const utils = api.createNavigationUtils({
|
||||
DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts',
|
||||
DEFAULT_SUB2API_URL: 'https://sub.example.com/admin/accounts',
|
||||
normalizeLocalCpaStep9Mode: (value) => value,
|
||||
});
|
||||
|
||||
assert.equal(utils.normalizeCodex2ApiUrl('localhost:8080/admin'), 'http://localhost:8080/admin/accounts');
|
||||
assert.equal(
|
||||
utils.normalizeCodex2ApiUrl('https://codex-admin.example.com/'),
|
||||
'https://codex-admin.example.com/admin/accounts'
|
||||
);
|
||||
assert.equal(utils.getPanelMode({ panelMode: 'codex2api' }), 'codex2api');
|
||||
assert.equal(utils.getPanelModeLabel('codex2api'), 'Codex2API');
|
||||
});
|
||||
|
||||
@@ -21,3 +21,53 @@ test('panel bridge requests oauth url with step 7 log label payload', () => {
|
||||
assert.match(source, /logStep:\s*7/);
|
||||
assert.doesNotMatch(source, /logStep:\s*6/);
|
||||
});
|
||||
|
||||
test('panel bridge can request codex2api oauth url via protocol', async () => {
|
||||
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
assert.equal(url, 'http://localhost:8080/api/admin/oauth/generate-auth-url');
|
||||
assert.equal(options.method, 'POST');
|
||||
assert.equal(options.headers['X-Admin-Key'], 'admin-secret');
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
auth_url: 'https://auth.openai.com/authorize?state=oauth-state',
|
||||
session_id: 'session-123',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
|
||||
const bridge = api.createPanelBridge({
|
||||
addLog: async () => {},
|
||||
chrome: {},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'codex2api',
|
||||
normalizeCodex2ApiUrl: (value) => value ? `http://${value.replace(/^https?:\/\//, '')}`.replace(/\/admin$/, '/admin/accounts') : 'http://localhost:8080/admin/accounts',
|
||||
normalizeSub2ApiUrl: (value) => value,
|
||||
rememberSourceLastUrl: async () => {},
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
waitForTabUrlFamily: async () => null,
|
||||
DEFAULT_SUB2API_GROUP_NAME: 'codex',
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
|
||||
});
|
||||
|
||||
const result = await bridge.requestOAuthUrlFromPanel({
|
||||
panelMode: 'codex2api',
|
||||
codex2apiUrl: 'localhost:8080/admin',
|
||||
codex2apiAdminKey: 'admin-secret',
|
||||
}, { logLabel: '步骤 7' });
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
|
||||
codex2apiSessionId: 'session-123',
|
||||
codex2apiOAuthState: 'oauth-state',
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
test('platform verify module supports codex2api protocol callback exchange', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
assert.equal(url, 'http://localhost:8080/api/admin/oauth/exchange-code');
|
||||
assert.equal(options.method, 'POST');
|
||||
assert.equal(options.headers['X-Admin-Key'], 'admin-secret');
|
||||
assert.deepStrictEqual(JSON.parse(options.body), {
|
||||
session_id: 'session-123',
|
||||
code: 'callback-code',
|
||||
state: 'oauth-state',
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
message: 'OAuth 账号 flow@example.com 添加成功',
|
||||
id: 42,
|
||||
email: 'flow@example.com',
|
||||
plan_type: 'pro',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const completed = [];
|
||||
const logs = [];
|
||||
const executor = api.createStep10Executor({
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completed.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'codex2api',
|
||||
getTabId: async () => 0,
|
||||
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
|
||||
isTabAlive: async () => false,
|
||||
normalizeCodex2ApiUrl: () => 'http://localhost:8080/admin/accounts',
|
||||
normalizeSub2ApiUrl: (value) => value,
|
||||
rememberSourceLastUrl: async () => {},
|
||||
reuseOrCreateTab: async () => 0,
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
shouldBypassStep9ForLocalCpa: () => false,
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
|
||||
});
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'codex2api',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
codex2apiUrl: 'http://localhost:8080/admin/accounts',
|
||||
codex2apiAdminKey: 'admin-secret',
|
||||
codex2apiSessionId: 'session-123',
|
||||
codex2apiOAuthState: 'oauth-state',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(logs, [
|
||||
{ message: '步骤 10:正在向 Codex2API 提交回调并创建账号...', level: 'info' },
|
||||
{ message: '步骤 10:OAuth 账号 flow@example.com 添加成功', level: 'ok' },
|
||||
]);
|
||||
assert.deepStrictEqual(completed, [
|
||||
{
|
||||
step: 10,
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'OAuth 账号 flow@example.com 添加成功',
|
||||
},
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -177,3 +177,88 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 7 stops immediately when management secret is missing', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
refreshCalls: 0,
|
||||
sendCalls: 0,
|
||||
logs: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => {
|
||||
events.refreshCalls += 1;
|
||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => {
|
||||
events.sendCalls += 1;
|
||||
return { step6Outcome: 'success' };
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
|
||||
/管理密钥/
|
||||
);
|
||||
|
||||
assert.equal(events.refreshCalls, 1);
|
||||
assert.equal(events.sendCalls, 0);
|
||||
assert.ok(events.logs.some(({ message }) => /管理密钥缺失或错误,不再重试,当前流程停止/.test(message)));
|
||||
assert.ok(!events.logs.some(({ message }) => /准备重试/.test(message)));
|
||||
});
|
||||
|
||||
test('step 7 stops immediately when management secret is invalid', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
refreshCalls: 0,
|
||||
logs: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => {
|
||||
events.refreshCalls += 1;
|
||||
throw new Error('Codex2API 请求失败(HTTP 401)。X-Admin-Key 无效或未授权。');
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => ({ step6Outcome: 'success' }),
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
|
||||
/401|未授权|无效/
|
||||
);
|
||||
|
||||
assert.equal(events.refreshCalls, 1);
|
||||
assert.ok(events.logs.some(({ message }) => /管理密钥缺失或错误,不再重试,当前流程停止/.test(message)));
|
||||
assert.ok(!events.logs.some(({ message }) => /准备重试/.test(message)));
|
||||
});
|
||||
|
||||
@@ -92,6 +92,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn
|
||||
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;
|
||||
let ensureOptions = null;
|
||||
const tabUpdates = [];
|
||||
const tabReuses = [];
|
||||
const realDateNow = Date.now;
|
||||
@@ -108,8 +109,9 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureMail2925MailboxSession: async () => {
|
||||
ensureMail2925MailboxSession: async (options) => {
|
||||
ensureCalls += 1;
|
||||
ensureOptions = options;
|
||||
},
|
||||
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
|
||||
rerunStep7ForStep8Recovery: async () => {},
|
||||
@@ -122,7 +124,15 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
|
||||
url: 'https://2925.com',
|
||||
navigateOnReuse: false,
|
||||
}),
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
getState: async () => ({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
mail2925UseAccountPool: true,
|
||||
currentMail2925AccountId: 'acc-1',
|
||||
mail2925Accounts: [
|
||||
{ id: 'acc-1', email: 'pool-user@2925.com' },
|
||||
],
|
||||
}),
|
||||
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
@@ -148,16 +158,26 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
mail2925UseAccountPool: true,
|
||||
currentMail2925AccountId: 'acc-1',
|
||||
mail2925Accounts: [
|
||||
{ id: 'acc-1', email: 'pool-user@2925.com' },
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
Date.now = realDateNow;
|
||||
}
|
||||
|
||||
assert.equal(ensureCalls, 0);
|
||||
assert.equal(ensureCalls, 1);
|
||||
assert.deepStrictEqual(ensureOptions, {
|
||||
accountId: 'acc-1',
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: true,
|
||||
expectedMailboxEmail: 'pool-user@2925.com',
|
||||
actionLabel: 'Step 8: ensure 2925 mailbox session',
|
||||
});
|
||||
assert.deepStrictEqual(tabReuses, []);
|
||||
assert.deepStrictEqual(tabUpdates, [
|
||||
{ tabId: 1, payload: { active: true } },
|
||||
{ tabId: 2, payload: { active: true } },
|
||||
]);
|
||||
assert.equal(capturedOptions.filterAfterTimestamp, 300000);
|
||||
assert.equal(capturedOptions.resendIntervalMs, 0);
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/mail-163.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('findMailItems falls back to visible aria-label mail rows when legacy selector is missing', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('isVisibleNode'),
|
||||
extractFunction('isLikelyMailItemNode'),
|
||||
extractFunction('findMailItems'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const mailRow = {
|
||||
hidden: false,
|
||||
textContent: 'Your temporary ChatGPT verification code 911113',
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-label') return 'Your temporary ChatGPT verification code 911113 发件人 OpenAI';
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
return { width: 600, height: 48 };
|
||||
},
|
||||
matches() {
|
||||
return false;
|
||||
},
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'div[sign="letter"]') return [];
|
||||
if (selector === '[role="option"][aria-label]') return [mailRow];
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
const window = {
|
||||
getComputedStyle() {
|
||||
return { display: 'block', visibility: 'visible' };
|
||||
},
|
||||
};
|
||||
|
||||
${bundle}
|
||||
|
||||
return { findMailItems };
|
||||
`)();
|
||||
|
||||
const rows = api.findMailItems();
|
||||
assert.equal(rows.length, 1);
|
||||
});
|
||||
|
||||
test('getMailTimestamp parses visible hh:mm text even when no title attribute exists', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('parseMail163Timestamp'),
|
||||
extractFunction('isLikelyMailTimestampText'),
|
||||
extractFunction('collectMailTimestampCandidates'),
|
||||
extractFunction('getMailTimestamp'),
|
||||
].join('\n');
|
||||
|
||||
const timestamp = new Function(`
|
||||
const item = {
|
||||
getAttribute() {
|
||||
return '';
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === '.e00, [title], [aria-label], time, [class*="time"], [class*="date"]') {
|
||||
return [];
|
||||
}
|
||||
if (selector === 'span, div, td, strong, b') {
|
||||
return [
|
||||
{
|
||||
textContent: '22:22',
|
||||
getAttribute() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
${bundle}
|
||||
|
||||
return getMailTimestamp(item);
|
||||
`)();
|
||||
|
||||
const now = new Date();
|
||||
const expected = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 22, 22, 0, 0).getTime();
|
||||
assert.equal(timestamp, expected);
|
||||
});
|
||||
|
||||
test('readOpenedMailText prefers opened body content that contains the verification code', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('collectOpenedMailTextCandidates'),
|
||||
extractFunction('selectOpenedMailTextCandidate'),
|
||||
extractFunction('readOpenedMailText'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const text = new Function(`
|
||||
const item = {
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
function getMailSubjectText() {
|
||||
return 'Your temporary ChatGPT login code';
|
||||
}
|
||||
|
||||
function getMailSenderText() {
|
||||
return 'OpenAI';
|
||||
}
|
||||
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'iframe') {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{ innerText: 'Your temporary ChatGPT login code', textContent: 'Your temporary ChatGPT login code' },
|
||||
{ innerText: 'Enter this temporary verification code to continue: 214203', textContent: 'Enter this temporary verification code to continue: 214203' },
|
||||
];
|
||||
},
|
||||
body: {
|
||||
innerText: 'fallback body',
|
||||
textContent: 'fallback body',
|
||||
},
|
||||
};
|
||||
|
||||
${bundle}
|
||||
|
||||
return readOpenedMailText(item);
|
||||
`)();
|
||||
|
||||
assert.match(text, /214203/);
|
||||
});
|
||||
|
||||
test('openMailAndGetMessageText reads opened body text and returns to inbox', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('collectOpenedMailTextCandidates'),
|
||||
extractFunction('selectOpenedMailTextCandidate'),
|
||||
extractFunction('readOpenedMailText'),
|
||||
extractFunction('returnToInbox'),
|
||||
extractFunction('openMailAndGetMessageText'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let inInbox = true;
|
||||
let clickCount = 0;
|
||||
const mailItem = {
|
||||
click() {
|
||||
clickCount += 1;
|
||||
inInbox = false;
|
||||
},
|
||||
};
|
||||
const inboxLink = {
|
||||
click() {
|
||||
inInbox = true;
|
||||
},
|
||||
};
|
||||
|
||||
function getMailSubjectText() {
|
||||
return 'Your temporary ChatGPT login code';
|
||||
}
|
||||
|
||||
function getMailSenderText() {
|
||||
return 'OpenAI';
|
||||
}
|
||||
|
||||
const document = {
|
||||
querySelector(selector) {
|
||||
if (selector === '.nui-tree-item-text[title="收件箱"], [title="收件箱"]') return inboxLink;
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'iframe') {
|
||||
return [];
|
||||
}
|
||||
return inInbox ? [] : [{ innerText: 'Enter this temporary verification code to continue: 214203', textContent: 'Enter this temporary verification code to continue: 214203' }];
|
||||
},
|
||||
body: {
|
||||
innerText: '',
|
||||
textContent: '',
|
||||
},
|
||||
};
|
||||
|
||||
function findMailItems() {
|
||||
return inInbox ? [mailItem] : [];
|
||||
}
|
||||
|
||||
async function sleep() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
openMailAndGetMessageText,
|
||||
getClickCount: () => clickCount,
|
||||
isInInbox: () => inInbox,
|
||||
mailItem,
|
||||
};
|
||||
`)();
|
||||
|
||||
const text = await api.openMailAndGetMessageText(api.mailItem);
|
||||
assert.match(text, /214203/);
|
||||
assert.equal(api.getClickCount(), 1);
|
||||
assert.equal(api.isInInbox(), true);
|
||||
});
|
||||
|
||||
test('openMailAndGetMessageText ignores stale pre-open text that contains an old code', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('collectOpenedMailTextCandidates'),
|
||||
extractFunction('selectOpenedMailTextCandidate'),
|
||||
extractFunction('readOpenedMailText'),
|
||||
extractFunction('returnToInbox'),
|
||||
extractFunction('openMailAndGetMessageText'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let stage = 'before';
|
||||
const oldText = 'OpenAI Your temporary ChatGPT login code. Ignore this old code 111111. '.repeat(10);
|
||||
const newText = 'OpenAI Your temporary ChatGPT login code. Your new code is 222222.';
|
||||
const mailItem = {
|
||||
click() {
|
||||
stage = 'after';
|
||||
},
|
||||
};
|
||||
const inboxLink = {
|
||||
click() {
|
||||
stage = 'done';
|
||||
},
|
||||
};
|
||||
|
||||
function getMailSubjectText() {
|
||||
return 'Your temporary ChatGPT login code';
|
||||
}
|
||||
|
||||
function getMailSenderText() {
|
||||
return 'OpenAI';
|
||||
}
|
||||
|
||||
const document = {
|
||||
querySelector(selector) {
|
||||
if (selector === '.nui-tree-item-text[title="收件箱"], [title="收件箱"]') return inboxLink;
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'iframe') {
|
||||
return [];
|
||||
}
|
||||
if (stage === 'before') {
|
||||
return [{ innerText: oldText, textContent: oldText }];
|
||||
}
|
||||
if (stage === 'after') {
|
||||
return [
|
||||
{ innerText: oldText, textContent: oldText },
|
||||
{ innerText: newText, textContent: newText },
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
body: {
|
||||
innerText: '',
|
||||
textContent: '',
|
||||
},
|
||||
};
|
||||
|
||||
function findMailItems() {
|
||||
return stage === 'done' ? [mailItem] : [];
|
||||
}
|
||||
|
||||
async function sleep() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
return { openMailAndGetMessageText, mailItem };
|
||||
`)();
|
||||
|
||||
const text = await api.openMailAndGetMessageText(api.mailItem);
|
||||
assert.match(text, /222222/);
|
||||
assert.doesNotMatch(text, /111111/);
|
||||
});
|
||||
|
||||
test('handlePollEmail ignores same-minute old snapshot mail before fallback', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let currentItems = [{ id: 'old-mail' }];
|
||||
const seenCodes = new Set();
|
||||
|
||||
function findMailItems() {
|
||||
return currentItems;
|
||||
}
|
||||
|
||||
function getCurrentMailIds(items = []) {
|
||||
return new Set((items.length ? items : currentItems).map((item) => item.id));
|
||||
}
|
||||
|
||||
function getMailItemId(item) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function getMailTimestamp() {
|
||||
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
|
||||
}
|
||||
|
||||
function getMailSenderText() {
|
||||
return 'OpenAI';
|
||||
}
|
||||
|
||||
function getMailSubjectText() {
|
||||
return 'Your temporary ChatGPT verification code';
|
||||
}
|
||||
|
||||
function getMailRowText() {
|
||||
return 'Your temporary ChatGPT verification code 911113 发件人 OpenAI';
|
||||
}
|
||||
|
||||
function extractVerificationCode() {
|
||||
return '911113';
|
||||
}
|
||||
|
||||
async function waitForElement() {
|
||||
return { click() {} };
|
||||
}
|
||||
async function refreshInbox() {}
|
||||
async function sleep() {}
|
||||
function log() {}
|
||||
function persistSeenCodes() {}
|
||||
function scheduleEmailCleanup() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
return { handlePollEmail };
|
||||
`)();
|
||||
|
||||
await assert.rejects(
|
||||
() => api.handlePollEmail(4, {
|
||||
senderFilters: ['openai'],
|
||||
subjectFilters: ['verification'],
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
filterAfterTimestamp: new Date(2026, 3, 22, 22, 22, 40, 0).getTime(),
|
||||
}),
|
||||
/未在 163 邮箱中找到新的匹配邮件/
|
||||
);
|
||||
});
|
||||
|
||||
test('handlePollEmail accepts a new same-minute mail that appears after the snapshot', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const oldMail = {
|
||||
id: 'old-mail',
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-label') return 'Old verification mail 111111 发件人 OpenAI';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const newMail = {
|
||||
id: 'new-mail',
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-label') return 'Your temporary ChatGPT verification code 654321 发件人 OpenAI';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
let refreshCount = 0;
|
||||
let currentItems = [oldMail];
|
||||
const seenCodes = new Set();
|
||||
|
||||
function findMailItems() {
|
||||
return currentItems;
|
||||
}
|
||||
|
||||
function getCurrentMailIds(items = []) {
|
||||
return new Set((items.length ? items : currentItems).map((item) => item.id));
|
||||
}
|
||||
|
||||
function getMailItemId(item) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function getMailTimestamp() {
|
||||
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
|
||||
}
|
||||
|
||||
function getMailSenderText() {
|
||||
return 'OpenAI';
|
||||
}
|
||||
|
||||
function getMailSubjectText(item) {
|
||||
return item.id === 'new-mail' ? 'Your temporary ChatGPT verification code' : 'Old verification mail';
|
||||
}
|
||||
|
||||
function getMailRowText(item) {
|
||||
return item.id === 'new-mail'
|
||||
? 'Your temporary ChatGPT verification code 654321 发件人 OpenAI'
|
||||
: 'Old verification mail 111111 发件人 OpenAI';
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
const match = String(text || '').match(/(\\d{6})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
async function waitForElement() {
|
||||
return { click() {} };
|
||||
}
|
||||
async function refreshInbox() {
|
||||
refreshCount += 1;
|
||||
if (refreshCount >= 1) {
|
||||
currentItems = [oldMail, newMail];
|
||||
}
|
||||
}
|
||||
async function sleep() {}
|
||||
function log() {}
|
||||
function persistSeenCodes() {}
|
||||
function scheduleEmailCleanup() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
return { handlePollEmail };
|
||||
`)();
|
||||
|
||||
const result = await api.handlePollEmail(4, {
|
||||
senderFilters: ['openai'],
|
||||
subjectFilters: ['verification'],
|
||||
maxAttempts: 2,
|
||||
intervalMs: 1,
|
||||
filterAfterTimestamp: new Date(2026, 3, 22, 22, 22, 40, 0).getTime(),
|
||||
});
|
||||
|
||||
assert.equal(result.code, '654321');
|
||||
assert.equal(result.mailId, 'new-mail');
|
||||
});
|
||||
|
||||
test('handlePollEmail falls back to row text when the subject node is missing', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const matchingMail = {
|
||||
id: 'mail-1',
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-label') return 'OpenAI Your temporary ChatGPT verification code 123456';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const seenCodes = new Set();
|
||||
|
||||
function findMailItems() {
|
||||
return [matchingMail];
|
||||
}
|
||||
|
||||
function getCurrentMailIds() {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
function getMailItemId(item) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function getMailTimestamp() {
|
||||
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
|
||||
}
|
||||
|
||||
function getMailSenderText() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getMailSubjectText() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getMailRowText() {
|
||||
return 'OpenAI Your temporary ChatGPT verification code 123456';
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
const match = String(text || '').match(/(\\d{6})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
async function waitForElement() {
|
||||
return { click() {} };
|
||||
}
|
||||
async function refreshInbox() {}
|
||||
async function sleep() {}
|
||||
function log() {}
|
||||
function persistSeenCodes() {}
|
||||
function scheduleEmailCleanup() {}
|
||||
async function openMailAndGetMessageText() {
|
||||
return '';
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return { handlePollEmail };
|
||||
`)();
|
||||
|
||||
const result = await api.handlePollEmail(8, {
|
||||
senderFilters: ['openai'],
|
||||
subjectFilters: ['verification'],
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
filterAfterTimestamp: 0,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '123456');
|
||||
assert.equal(result.mailId, 'mail-1');
|
||||
});
|
||||
|
||||
test('handlePollEmail opens matching mail body when preview has no code', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const matchingMail = {
|
||||
id: 'mail-body-1',
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-label') return 'OpenAI Your temporary ChatGPT login code';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const seenCodes = new Set();
|
||||
let openedCount = 0;
|
||||
|
||||
function findMailItems() {
|
||||
return [matchingMail];
|
||||
}
|
||||
|
||||
function getCurrentMailIds() {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
function getMailItemId(item) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function getMailTimestamp() {
|
||||
return new Date(2026, 3, 22, 22, 49, 0, 0).getTime();
|
||||
}
|
||||
|
||||
function getMailSenderText() {
|
||||
return 'OpenAI';
|
||||
}
|
||||
|
||||
function getMailSubjectText() {
|
||||
return 'Your temporary ChatGPT login code';
|
||||
}
|
||||
|
||||
function getMailRowText() {
|
||||
return 'OpenAI Your temporary ChatGPT login code';
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
const match = String(text || '').match(/(\\d{6})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
async function waitForElement() {
|
||||
return { click() {} };
|
||||
}
|
||||
async function refreshInbox() {}
|
||||
async function sleep() {}
|
||||
function log() {}
|
||||
function persistSeenCodes() {}
|
||||
function scheduleEmailCleanup() {}
|
||||
async function openMailAndGetMessageText() {
|
||||
openedCount += 1;
|
||||
return 'Enter this temporary verification code to continue: 214203';
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return { handlePollEmail, getOpenedCount: () => openedCount };
|
||||
`)();
|
||||
|
||||
const result = await api.handlePollEmail(8, {
|
||||
senderFilters: ['openai'],
|
||||
subjectFilters: ['verification', 'login'],
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
filterAfterTimestamp: 0,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '214203');
|
||||
assert.equal(result.mailId, 'mail-body-1');
|
||||
assert.equal(api.getOpenedCount(), 1);
|
||||
});
|
||||
@@ -32,13 +32,14 @@ const MAIL2925_AGREEMENT_PATTERNS = [];
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === '.mail-item') return [];
|
||||
if (selector === 'body *') return [headerEmail];
|
||||
if (selector.includes('[class*="user"]')) return [headerEmail];
|
||||
if (selector === 'body *') return [headerEmail, wrongHeader];
|
||||
if (selector === '.right-header' || selector.includes('right-header')) return [headerEmail];
|
||||
if (selector.includes('[class*="user"]')) return [];
|
||||
return [];
|
||||
},
|
||||
body: {
|
||||
innerText: 'QLHazycoder qlhazycoder@2925.com',
|
||||
textContent: 'QLHazycoder qlhazycoder@2925.com',
|
||||
innerText: 'QLHazycoder qlhazycoder@2925.com tm1.openai.com@foo.example',
|
||||
textContent: 'QLHazycoder qlhazycoder@2925.com tm1.openai.com@foo.example',
|
||||
},
|
||||
};
|
||||
const window = {
|
||||
@@ -54,6 +55,13 @@ const headerEmail = {
|
||||
getBoundingClientRect() { return { top: 40, left: 400, width: 120, height: 20 }; },
|
||||
closest() { return null; },
|
||||
};
|
||||
const wrongHeader = {
|
||||
hidden: false,
|
||||
textContent: 'tm1.openai.com@foo.example',
|
||||
innerText: 'tm1.openai.com@foo.example',
|
||||
getBoundingClientRect() { return { top: 48, left: 430, width: 150, height: 20 }; },
|
||||
closest() { return null; },
|
||||
};
|
||||
function detectMail2925LimitMessage() { return ''; }
|
||||
function findMail2925LoginPasswordInput() { return null; }
|
||||
function findMail2925LoginEmailInput() { return null; }
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
function createRow(initialDisplay = 'none') {
|
||||
return {
|
||||
style: { display: initialDisplay },
|
||||
};
|
||||
}
|
||||
|
||||
test('sidepanel html places cloudflare temp email controls in a standalone section', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
assert.match(html, /id="cloudflare-temp-email-section"/);
|
||||
assert.match(html, /id="btn-cloudflare-temp-email-usage-guide"/);
|
||||
assert.match(html, /id="btn-cloudflare-temp-email-github"/);
|
||||
assert.match(html, /id="row-temp-email-random-subdomain-toggle"/);
|
||||
assert.match(html, /id="input-temp-email-use-random-subdomain"/);
|
||||
assert.doesNotMatch(html, /id="row-temp-email-random-subdomain-domain"/);
|
||||
});
|
||||
|
||||
test('sidepanel modal message preserves line breaks and supports inline links', () => {
|
||||
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
|
||||
assert.match(css, /\.modal-message\s*\{[\s\S]*white-space:\s*pre-line;/);
|
||||
assert.match(css, /\.modal-message a,\s*[\s\S]*\.modal-alert a/);
|
||||
});
|
||||
|
||||
test('buildCloudflareTempEmailUsageGuideModalConfig returns a modal payload with inline links for generator mode', () => {
|
||||
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
|
||||
|
||||
const api = new Function(`
|
||||
const selectMailProvider = { value: '163' };
|
||||
const selectEmailGenerator = { value: 'cloudflare-temp-email' };
|
||||
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
|
||||
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
|
||||
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
|
||||
${bundle}
|
||||
return {
|
||||
buildCloudflareTempEmailUsageGuideModalConfig,
|
||||
};
|
||||
`)();
|
||||
|
||||
const modalConfig = api.buildCloudflareTempEmailUsageGuideModalConfig();
|
||||
assert.equal(typeof modalConfig.title, 'string');
|
||||
assert.equal(typeof modalConfig.messageHtml, 'string');
|
||||
assert.equal(typeof modalConfig.alert?.text, 'string');
|
||||
assert.equal(modalConfig.title.length > 0, true);
|
||||
assert.equal(modalConfig.messageHtml.length > 0, true);
|
||||
assert.equal(modalConfig.alert.text.length > 0, true);
|
||||
assert.equal(modalConfig.messageHtml.includes('<a '), true);
|
||||
assert.equal(modalConfig.messageHtml.includes('Issue #942'), true);
|
||||
assert.equal(modalConfig.messageHtml.includes('LINUX DO 教程'), true);
|
||||
});
|
||||
|
||||
test('buildCloudflareTempEmailUsageGuideModalConfig returns a distinct alert for provider mode', () => {
|
||||
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
|
||||
|
||||
const api = new Function(`
|
||||
const selectMailProvider = { value: 'cloudflare-temp-email' };
|
||||
const selectEmailGenerator = { value: 'duck' };
|
||||
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
|
||||
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
|
||||
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
|
||||
${bundle}
|
||||
return {
|
||||
buildCloudflareTempEmailUsageGuideModalConfig,
|
||||
};
|
||||
`)();
|
||||
|
||||
const providerConfig = api.buildCloudflareTempEmailUsageGuideModalConfig();
|
||||
|
||||
const generatorApi = new Function(`
|
||||
const selectMailProvider = { value: '163' };
|
||||
const selectEmailGenerator = { value: 'cloudflare-temp-email' };
|
||||
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
|
||||
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
|
||||
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
|
||||
${bundle}
|
||||
return {
|
||||
buildCloudflareTempEmailUsageGuideModalConfig,
|
||||
};
|
||||
`)();
|
||||
|
||||
const generatorConfig = generatorApi.buildCloudflareTempEmailUsageGuideModalConfig();
|
||||
assert.equal(typeof providerConfig.alert?.text, 'string');
|
||||
assert.equal(typeof providerConfig.messageHtml, 'string');
|
||||
assert.equal(providerConfig.alert.text.length > 0, true);
|
||||
assert.equal(providerConfig.messageHtml.length > 0, true);
|
||||
assert.notEqual(providerConfig.alert.text, generatorConfig.alert.text);
|
||||
});
|
||||
|
||||
test('openCloudflareTempEmailRepositoryPage opens the upstream repository', () => {
|
||||
const bundle = extractFunction('openCloudflareTempEmailRepositoryPage');
|
||||
|
||||
const api = new Function(`
|
||||
const calls = [];
|
||||
const CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email';
|
||||
function openExternalUrl(url) { calls.push(url); }
|
||||
${bundle}
|
||||
return {
|
||||
calls,
|
||||
openCloudflareTempEmailRepositoryPage,
|
||||
};
|
||||
`)();
|
||||
|
||||
api.openCloudflareTempEmailRepositoryPage();
|
||||
assert.deepEqual(api.calls, ['https://github.com/dreamhunter2333/cloudflare_temp_email']);
|
||||
});
|
||||
|
||||
test('applyCloudflareTempEmailSettingsState restores the random subdomain toggle and temp domain list', () => {
|
||||
const bundle = extractFunction('applyCloudflareTempEmailSettingsState');
|
||||
|
||||
const api = new Function(`
|
||||
const inputTempEmailBaseUrl = { value: '' };
|
||||
const inputTempEmailAdminAuth = { value: '' };
|
||||
const inputTempEmailCustomAuth = { value: '' };
|
||||
const inputTempEmailReceiveMailbox = { value: '' };
|
||||
const inputTempEmailUseRandomSubdomain = { checked: false };
|
||||
const calls = {
|
||||
domainOptions: [],
|
||||
domainEditMode: [],
|
||||
};
|
||||
function renderCloudflareTempEmailDomainOptions(value) { calls.domainOptions.push(value); }
|
||||
function setCloudflareTempEmailDomainEditMode(editing, options) { calls.domainEditMode.push({ editing, options }); }
|
||||
${bundle}
|
||||
return {
|
||||
applyCloudflareTempEmailSettingsState,
|
||||
calls,
|
||||
inputTempEmailBaseUrl,
|
||||
inputTempEmailAdminAuth,
|
||||
inputTempEmailCustomAuth,
|
||||
inputTempEmailReceiveMailbox,
|
||||
inputTempEmailUseRandomSubdomain,
|
||||
};
|
||||
`)();
|
||||
|
||||
api.applyCloudflareTempEmailSettingsState({
|
||||
cloudflareTempEmailBaseUrl: 'https://temp.example.com',
|
||||
cloudflareTempEmailAdminAuth: 'admin-secret',
|
||||
cloudflareTempEmailCustomAuth: 'custom-secret',
|
||||
cloudflareTempEmailReceiveMailbox: 'relay@example.com',
|
||||
cloudflareTempEmailUseRandomSubdomain: true,
|
||||
cloudflareTempEmailDomain: 'mail.example.com',
|
||||
});
|
||||
|
||||
assert.equal(api.inputTempEmailBaseUrl.value, 'https://temp.example.com');
|
||||
assert.equal(api.inputTempEmailAdminAuth.value, 'admin-secret');
|
||||
assert.equal(api.inputTempEmailCustomAuth.value, 'custom-secret');
|
||||
assert.equal(api.inputTempEmailReceiveMailbox.value, 'relay@example.com');
|
||||
assert.equal(api.inputTempEmailUseRandomSubdomain.checked, true);
|
||||
assert.deepEqual(api.calls.domainOptions, ['mail.example.com']);
|
||||
assert.deepEqual(api.calls.domainEditMode, [{ editing: false, options: { clearInput: true } }]);
|
||||
});
|
||||
|
||||
test('updateMailProviderUI keeps the temp domain selector visible and updates the hint when random subdomain is enabled', () => {
|
||||
const bundle = extractFunction('updateMailProviderUI');
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = {
|
||||
cloudflareTempEmailDomains: ['mail.example.com'],
|
||||
};
|
||||
let cloudflareTempEmailDomainEditMode = false;
|
||||
const ICLOUD_PROVIDER = 'icloud';
|
||||
const GMAIL_PROVIDER = 'gmail';
|
||||
const LUCKMAIL_PROVIDER = 'luckmail-api';
|
||||
const rowMail2925Mode = ${JSON.stringify(createRow('none'))};
|
||||
const rowMail2925PoolSettings = ${JSON.stringify(createRow('none'))};
|
||||
const rowEmailPrefix = ${JSON.stringify(createRow('none'))};
|
||||
const rowInbucketHost = ${JSON.stringify(createRow('none'))};
|
||||
const rowInbucketMailbox = ${JSON.stringify(createRow('none'))};
|
||||
const rowEmailGenerator = ${JSON.stringify(createRow(''))};
|
||||
const rowCfDomain = ${JSON.stringify(createRow('none'))};
|
||||
const rowTempEmailBaseUrl = ${JSON.stringify(createRow('none'))};
|
||||
const rowTempEmailAdminAuth = ${JSON.stringify(createRow('none'))};
|
||||
const rowTempEmailCustomAuth = ${JSON.stringify(createRow('none'))};
|
||||
const rowTempEmailReceiveMailbox = ${JSON.stringify(createRow('none'))};
|
||||
const rowTempEmailRandomSubdomainToggle = ${JSON.stringify(createRow('none'))};
|
||||
const rowTempEmailDomain = ${JSON.stringify(createRow('none'))};
|
||||
const cloudflareTempEmailSection = ${JSON.stringify(createRow('none'))};
|
||||
const hotmailSection = ${JSON.stringify(createRow('none'))};
|
||||
const mail2925Section = ${JSON.stringify(createRow('none'))};
|
||||
const luckmailSection = ${JSON.stringify(createRow('none'))};
|
||||
const icloudSection = ${JSON.stringify(createRow('none'))};
|
||||
const labelEmailPrefix = { textContent: '' };
|
||||
const inputEmailPrefix = { placeholder: '', style: { display: '' }, readOnly: false };
|
||||
const labelMail2925UseAccountPool = ${JSON.stringify(createRow('none'))};
|
||||
const selectMail2925PoolAccount = { style: { display: 'none' }, disabled: false };
|
||||
const btnFetchEmail = { hidden: false, disabled: false, textContent: '' };
|
||||
const btnMailLogin = { disabled: false, textContent: '', title: '' };
|
||||
const inputEmail = { readOnly: false, placeholder: '', value: '' };
|
||||
const autoHintText = { textContent: '' };
|
||||
const rowHotmailServiceMode = ${JSON.stringify(createRow('none'))};
|
||||
const rowHotmailRemoteBaseUrl = ${JSON.stringify(createRow('none'))};
|
||||
const rowHotmailLocalBaseUrl = ${JSON.stringify(createRow('none'))};
|
||||
const inputMail2925UseAccountPool = { checked: false };
|
||||
const selectMailProvider = { value: '163' };
|
||||
const selectEmailGenerator = { value: 'cloudflare-temp-email', disabled: false };
|
||||
const inputTempEmailUseRandomSubdomain = { checked: false };
|
||||
const calls = {
|
||||
tempDomainEditMode: [],
|
||||
};
|
||||
function isLuckmailProvider() { return false; }
|
||||
function isCustomMailProvider() { return false; }
|
||||
function isIcloudMailProvider() { return false; }
|
||||
function usesGeneratedAliasMailProvider() { return false; }
|
||||
function getSelectedMail2925Mode() { return 'provide'; }
|
||||
function getManagedAliasProviderUiCopy() { return null; }
|
||||
function getCurrentRegistrationEmailUiCopy() {
|
||||
return {
|
||||
buttonLabel: '生成 Temp',
|
||||
placeholder: '点击生成 Cloudflare Temp Email,或手动粘贴邮箱',
|
||||
label: 'Cloudflare Temp Email',
|
||||
};
|
||||
}
|
||||
function updateMailLoginButtonState() {}
|
||||
function getSelectedHotmailServiceMode() { return 'local'; }
|
||||
function getCloudflareDomainsFromState() { return { domains: [], activeDomain: '' }; }
|
||||
function setCloudflareDomainEditMode() {}
|
||||
function getCloudflareTempEmailDomainsFromState() { return { domains: ['mail.example.com'], activeDomain: 'mail.example.com' }; }
|
||||
function setCloudflareTempEmailDomainEditMode(editing) { calls.tempDomainEditMode.push(editing); }
|
||||
function queueIcloudAliasRefresh() {}
|
||||
function hideIcloudLoginHelp() {}
|
||||
function syncMail2925PoolAccountOptions() {}
|
||||
function getMail2925Accounts() { return []; }
|
||||
function renderHotmailAccounts() {}
|
||||
function renderMail2925Accounts() {}
|
||||
function renderLuckmailPurchases() {}
|
||||
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
|
||||
function isAutoRunLockedPhase() { return false; }
|
||||
${bundle}
|
||||
return {
|
||||
updateMailProviderUI,
|
||||
cloudflareTempEmailSection,
|
||||
rowTempEmailRandomSubdomainToggle,
|
||||
rowTempEmailDomain,
|
||||
inputTempEmailUseRandomSubdomain,
|
||||
autoHintText,
|
||||
calls,
|
||||
};
|
||||
`)();
|
||||
|
||||
api.updateMailProviderUI();
|
||||
assert.equal(api.cloudflareTempEmailSection.style.display, '');
|
||||
assert.equal(api.rowTempEmailRandomSubdomainToggle.style.display, '');
|
||||
assert.equal(api.rowTempEmailDomain.style.display, '');
|
||||
|
||||
api.inputTempEmailUseRandomSubdomain.checked = true;
|
||||
api.updateMailProviderUI();
|
||||
assert.equal(api.cloudflareTempEmailSection.style.display, '');
|
||||
assert.equal(api.rowTempEmailDomain.style.display, '');
|
||||
assert.match(api.autoHintText.textContent, /RANDOM_SUBDOMAIN_DOMAINS/);
|
||||
});
|
||||
@@ -135,6 +135,8 @@ const inputSub2ApiEmail = { value: 'user@example.com' };
|
||||
const inputSub2ApiPassword = { value: 'sub-secret' };
|
||||
const inputSub2ApiGroup = { value: ' codex ' };
|
||||
const inputSub2ApiDefaultProxy = { value: ' proxy-a ' };
|
||||
const inputCodex2ApiUrl = { value: 'http://localhost:8080/admin/accounts' };
|
||||
const inputCodex2ApiAdminKey = { value: 'codex-admin-secret' };
|
||||
const inputPassword = { value: 'Secret123!' };
|
||||
const selectMailProvider = { value: '163' };
|
||||
const selectEmailGenerator = { value: 'duck' };
|
||||
@@ -154,6 +156,7 @@ const inputTempEmailBaseUrl = { value: 'https://temp.example.com' };
|
||||
const inputTempEmailAdminAuth = { value: 'admin-secret' };
|
||||
const inputTempEmailCustomAuth = { value: 'custom-secret' };
|
||||
const inputTempEmailReceiveMailbox = { value: 'relay@example.com' };
|
||||
const inputTempEmailUseRandomSubdomain = { checked: true };
|
||||
const inputAutoSkipFailures = { checked: false };
|
||||
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
|
||||
const inputAutoDelayEnabled = { checked: true };
|
||||
@@ -190,12 +193,16 @@ return {
|
||||
assert.equal('customPassword' in contributionPayload, false);
|
||||
assert.equal('accountRunHistoryTextEnabled' in contributionPayload, false);
|
||||
assert.equal('accountRunHistoryHelperBaseUrl' in contributionPayload, false);
|
||||
assert.equal(contributionPayload.cloudflareTempEmailUseRandomSubdomain, true);
|
||||
|
||||
api.setLatestState({ contributionMode: false });
|
||||
const normalPayload = api.collectSettingsPayload();
|
||||
assert.equal(normalPayload.customPassword, 'Secret123!');
|
||||
assert.equal(normalPayload.accountRunHistoryTextEnabled, true);
|
||||
assert.equal(normalPayload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
|
||||
assert.equal(normalPayload.codex2apiUrl, 'http://localhost:8080/admin/accounts');
|
||||
assert.equal(normalPayload.codex2apiAdminKey, 'codex-admin-secret');
|
||||
assert.equal(normalPayload.cloudflareTempEmailUseRandomSubdomain, true);
|
||||
});
|
||||
|
||||
test('contribution mode manager enters mode, starts main auto flow, polls contribution status, and exits cleanly', async () => {
|
||||
@@ -264,6 +271,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
|
||||
rowSub2ApiGroup: createElement(),
|
||||
rowSub2ApiPassword: createElement(),
|
||||
rowSub2ApiUrl: createElement(),
|
||||
rowCodex2ApiUrl: createElement(),
|
||||
rowCodex2ApiAdminKey: createElement(),
|
||||
rowVpsPassword: createElement(),
|
||||
rowVpsUrl: createElement(),
|
||||
selectPanelMode: createElement({ value: 'sub2api' }),
|
||||
@@ -414,6 +423,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
|
||||
assert.equal(dom.contributionModeSummary.textContent.length > 0, true);
|
||||
assert.equal(dom.btnContributionMode.classList.contains('is-active'), true);
|
||||
assert.equal(dom.rowVpsUrl.classList.contains('is-contribution-hidden'), true);
|
||||
assert.equal(dom.rowCodex2ApiUrl.classList.contains('is-contribution-hidden'), true);
|
||||
assert.equal(dom.rowCodex2ApiAdminKey.classList.contains('is-contribution-hidden'), true);
|
||||
assert.ok(closeConfigMenuCount >= 1);
|
||||
assert.ok(closeAccountRecordsCount >= 1);
|
||||
assert.ok(updatePanelModeCount >= 1);
|
||||
|
||||
@@ -166,6 +166,8 @@ const inputSub2ApiEmail = { value: '' };
|
||||
const inputSub2ApiPassword = { value: '' };
|
||||
const inputSub2ApiGroup = { value: '' };
|
||||
const inputSub2ApiDefaultProxy = { value: '' };
|
||||
const inputCodex2ApiUrl = { value: '' };
|
||||
const inputCodex2ApiAdminKey = { value: '' };
|
||||
const inputPassword = { value: '' };
|
||||
const selectMailProvider = { value: '2925' };
|
||||
const selectEmailGenerator = { value: 'duck' };
|
||||
@@ -186,6 +188,7 @@ const inputTempEmailBaseUrl = { value: '' };
|
||||
const inputTempEmailAdminAuth = { value: '' };
|
||||
const inputTempEmailCustomAuth = { value: '' };
|
||||
const inputTempEmailReceiveMailbox = { value: '' };
|
||||
const inputTempEmailUseRandomSubdomain = { checked: false };
|
||||
const inputAutoSkipFailures = { checked: false };
|
||||
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
|
||||
const inputAutoDelayEnabled = { checked: false };
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('new user guide prompt is only eligible before the one-time dismissal is set', () => {
|
||||
const bundle = [
|
||||
extractFunction('isPromptDismissed'),
|
||||
extractFunction('setPromptDismissed'),
|
||||
extractFunction('isNewUserGuidePromptDismissed'),
|
||||
extractFunction('setNewUserGuidePromptDismissed'),
|
||||
extractFunction('shouldPromptNewUserGuide'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
|
||||
const storage = new Map();
|
||||
const localStorage = {
|
||||
getItem(key) {
|
||||
return storage.has(key) ? storage.get(key) : null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
storage.set(key, String(value));
|
||||
},
|
||||
removeItem(key) {
|
||||
storage.delete(key);
|
||||
},
|
||||
};
|
||||
const btnContributionMode = { disabled: false };
|
||||
let latestState = { contributionMode: false };
|
||||
${bundle}
|
||||
return {
|
||||
shouldPromptNewUserGuide,
|
||||
setDismissed(value) {
|
||||
setNewUserGuidePromptDismissed(value);
|
||||
},
|
||||
setButtonDisabled(value) {
|
||||
btnContributionMode.disabled = Boolean(value);
|
||||
},
|
||||
setContributionMode(value) {
|
||||
latestState = { contributionMode: Boolean(value) };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.shouldPromptNewUserGuide(), true);
|
||||
|
||||
api.setDismissed(true);
|
||||
assert.equal(api.shouldPromptNewUserGuide(), false);
|
||||
|
||||
api.setDismissed(false);
|
||||
api.setButtonDisabled(true);
|
||||
assert.equal(api.shouldPromptNewUserGuide(), false);
|
||||
|
||||
api.setButtonDisabled(false);
|
||||
api.setContributionMode(true);
|
||||
assert.equal(api.shouldPromptNewUserGuide(), false);
|
||||
});
|
||||
|
||||
test('new user guide prompt persists dismissal before awaiting the user choice and opens the contribution page on confirm', async () => {
|
||||
const bundle = [
|
||||
extractFunction('isPromptDismissed'),
|
||||
extractFunction('setPromptDismissed'),
|
||||
extractFunction('isNewUserGuidePromptDismissed'),
|
||||
extractFunction('setNewUserGuidePromptDismissed'),
|
||||
extractFunction('shouldPromptNewUserGuide'),
|
||||
extractFunction('getContributionPortalUrl'),
|
||||
extractFunction('openNewUserGuidePrompt'),
|
||||
extractFunction('maybeShowNewUserGuidePrompt'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
|
||||
const storage = new Map();
|
||||
const localStorage = {
|
||||
getItem(key) {
|
||||
return storage.has(key) ? storage.get(key) : null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
storage.set(key, String(value));
|
||||
},
|
||||
removeItem(key) {
|
||||
storage.delete(key);
|
||||
},
|
||||
};
|
||||
const btnContributionMode = { disabled: false };
|
||||
const latestState = { contributionMode: false };
|
||||
const contributionContentService = { portalUrl: 'https://apikey.qzz.io' };
|
||||
const openedUrls = [];
|
||||
let modalOptions = null;
|
||||
let nextChoice = 'confirm';
|
||||
function openExternalUrl(url) {
|
||||
openedUrls.push(url);
|
||||
}
|
||||
function openActionModal(options) {
|
||||
modalOptions = options;
|
||||
return Promise.resolve(nextChoice);
|
||||
}
|
||||
${bundle}
|
||||
return {
|
||||
maybeShowNewUserGuidePrompt,
|
||||
getDismissed() {
|
||||
return localStorage.getItem(NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY);
|
||||
},
|
||||
getOpenedUrls() {
|
||||
return openedUrls.slice();
|
||||
},
|
||||
getModalOptions() {
|
||||
return modalOptions;
|
||||
},
|
||||
setNextChoice(choice) {
|
||||
nextChoice = choice;
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const confirmed = await api.maybeShowNewUserGuidePrompt();
|
||||
const modalOptions = api.getModalOptions();
|
||||
|
||||
assert.equal(confirmed, true);
|
||||
assert.equal(api.getDismissed(), '1');
|
||||
assert.deepStrictEqual(api.getOpenedUrls(), ['https://apikey.qzz.io']);
|
||||
assert.equal(modalOptions.title, '新手引导');
|
||||
assert.equal(modalOptions.alert.text, '本提示仅出现一次。');
|
||||
assert.deepStrictEqual(
|
||||
modalOptions.actions.map((item) => ({ id: item.id, label: item.label })),
|
||||
[
|
||||
{ id: null, label: '取消' },
|
||||
{ id: 'confirm', label: '查看引导' },
|
||||
]
|
||||
);
|
||||
|
||||
api.setNextChoice(null);
|
||||
const skipped = await api.maybeShowNewUserGuidePrompt();
|
||||
assert.equal(skipped, false);
|
||||
assert.deepStrictEqual(api.getOpenedUrls(), ['https://apikey.qzz.io']);
|
||||
});
|
||||
@@ -147,7 +147,7 @@ return {
|
||||
|
||||
test('signup entry diagnostics captures hidden signup button style and blocking ancestor details', () => {
|
||||
const api = new Function(`
|
||||
const SIGNUP_ENTRY_TRIGGER_PATTERN = /鍏嶈垂娉ㄥ唽|绔嬪嵆娉ㄥ唽|娉ㄥ唽|sign\\s*up|register|create\\s*account|create\\s+account/i;
|
||||
const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\\s*up|register|create\\s*account|create\\s+account/i;
|
||||
const location = { href: 'https://chatgpt.com/' };
|
||||
const hiddenSection = {
|
||||
tagName: 'DIV',
|
||||
|
||||
@@ -87,7 +87,8 @@ test('step6LoginFromPasswordPage switches to one-time-code login when password i
|
||||
globalThis.log = (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
};
|
||||
globalThis.step6SwitchToOneTimeCodeLogin = async (value) => {
|
||||
globalThis.step6SwitchToOneTimeCodeLogin = async (payload, value) => {
|
||||
assert.deepStrictEqual(payload, { email: 'user@example.com', password: '' });
|
||||
assert.strictEqual(value, snapshot);
|
||||
return { step6Outcome: 'success', via: 'switch_to_one_time_code_login' };
|
||||
};
|
||||
|
||||
@@ -72,12 +72,18 @@ async function recoverCurrentAuthRetryPage() {
|
||||
return { recovered: true };
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleep() {}
|
||||
|
||||
function log(message, level = 'info') {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
|
||||
${extractFunction('createStep6SuccessResult')}
|
||||
${extractFunction('createStep6RecoverableResult')}
|
||||
${extractFunction('normalizeStep6Snapshot')}
|
||||
${extractFunction('waitForKnownLoginAuthState')}
|
||||
${extractFunction('createStep6LoginTimeoutRecoveryTransition')}
|
||||
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
|
||||
|
||||
return {
|
||||
@@ -104,6 +110,141 @@ return {
|
||||
assert.equal(result.message, '当前页面处于登录超时报错页。');
|
||||
});
|
||||
|
||||
test('step 7 timeout recovery transition continues from password page after retry succeeds', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
let recoverCalls = 0;
|
||||
let currentState = 'login_timeout_error_page';
|
||||
|
||||
const location = {
|
||||
href: 'https://auth.openai.com/log-in',
|
||||
};
|
||||
|
||||
function inspectLoginAuthState() {
|
||||
return {
|
||||
state: currentState,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function recoverCurrentAuthRetryPage() {
|
||||
recoverCalls += 1;
|
||||
currentState = 'password_page';
|
||||
return { recovered: true };
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleep() {}
|
||||
|
||||
function log(message, level = 'info') {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
|
||||
${extractFunction('createStep6SuccessResult')}
|
||||
${extractFunction('createStep6RecoverableResult')}
|
||||
${extractFunction('normalizeStep6Snapshot')}
|
||||
${extractFunction('waitForKnownLoginAuthState')}
|
||||
${extractFunction('createStep6LoginTimeoutRecoveryTransition')}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
return createStep6LoginTimeoutRecoveryTransition(
|
||||
'login_timeout_error_page',
|
||||
{ state: 'login_timeout_error_page', url: location.href },
|
||||
'当前页面处于登录超时报错页。',
|
||||
{
|
||||
via: 'login_timeout_initial_recovered',
|
||||
}
|
||||
);
|
||||
},
|
||||
snapshot() {
|
||||
return { logs, recoverCalls };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.equal(snapshot.recoverCalls, 1);
|
||||
assert.equal(result.action, 'password');
|
||||
assert.equal(result.snapshot.state, 'password_page');
|
||||
assert.equal(snapshot.logs.some(({ message }) => /密码页/.test(message)), true);
|
||||
});
|
||||
|
||||
test('step 7 entry resumes password flow after retry page recovery reaches password page', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
let recoverCalls = 0;
|
||||
let currentState = 'login_timeout_error_page';
|
||||
|
||||
const location = {
|
||||
href: 'https://auth.openai.com/log-in',
|
||||
};
|
||||
|
||||
function inspectLoginAuthState() {
|
||||
return {
|
||||
state: currentState,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function recoverCurrentAuthRetryPage() {
|
||||
recoverCalls += 1;
|
||||
currentState = 'password_page';
|
||||
return { recovered: true };
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleep() {}
|
||||
|
||||
function log(message, level = 'info') {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
|
||||
async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||
return { branch: 'password', payload, snapshot };
|
||||
}
|
||||
|
||||
async function step6LoginFromEmailPage(payload, snapshot) {
|
||||
return { branch: 'email', payload, snapshot };
|
||||
}
|
||||
|
||||
async function finalizeStep6VerificationReady(options) {
|
||||
return { branch: 'verification', options };
|
||||
}
|
||||
|
||||
function throwForStep6FatalState() {}
|
||||
|
||||
${extractFunction('createStep6SuccessResult')}
|
||||
${extractFunction('createStep6RecoverableResult')}
|
||||
${extractFunction('normalizeStep6Snapshot')}
|
||||
${extractFunction('waitForKnownLoginAuthState')}
|
||||
${extractFunction('createStep6LoginTimeoutRecoveryTransition')}
|
||||
${extractFunction('step6_login')}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
return step6_login({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
});
|
||||
},
|
||||
snapshot() {
|
||||
return { logs, recoverCalls };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.equal(snapshot.recoverCalls, 1);
|
||||
assert.equal(result.branch, 'password');
|
||||
assert.equal(result.snapshot.state, 'password_page');
|
||||
assert.equal(snapshot.logs.some(({ message }) => /密码页/.test(message)), true);
|
||||
});
|
||||
|
||||
test('step 7 finalize converts verification page that falls into retry page into recoverable result', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
@@ -150,6 +291,8 @@ function getLoginAuthStateLabel(snapshot) {
|
||||
${extractFunction('createStep6SuccessResult')}
|
||||
${extractFunction('createStep6RecoverableResult')}
|
||||
${extractFunction('normalizeStep6Snapshot')}
|
||||
${extractFunction('waitForKnownLoginAuthState')}
|
||||
${extractFunction('createStep6LoginTimeoutRecoveryTransition')}
|
||||
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
|
||||
${extractFunction('finalizeStep6VerificationReady')}
|
||||
|
||||
|
||||
+28
-11
@@ -22,7 +22,7 @@
|
||||
- 刷新 OAuth 链接并登录
|
||||
- 轮询登录验证码
|
||||
- 自动确认 OAuth 同意页
|
||||
- 把 localhost 回调提交到 CPA 或 SUB2API
|
||||
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
|
||||
|
||||
## 2. 核心运行参与者
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
保存持久配置与账号运行历史:
|
||||
|
||||
- CPA / SUB2API 配置
|
||||
- Codex2API 配置
|
||||
- 邮箱 provider 配置
|
||||
- Hotmail 账号池
|
||||
- 2925 账号池
|
||||
@@ -167,7 +168,7 @@
|
||||
注意:
|
||||
|
||||
- `contributionMode` 不属于持久配置,也不参与导入/导出;它只存在于运行态
|
||||
- `panelMode` 仍然只表示 `cpa | sub2api` 来源,不能把贡献模式实现成新的来源枚举
|
||||
- `panelMode` 当前表示 `cpa | sub2api | codex2api` 三个来源;贡献模式仍不是新的来源枚举
|
||||
|
||||
当启用了独立的账号运行历史本地同步配置时,账号运行历史会通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 整体同步写入 `data/account-run-history.json` 快照文件,便于开发者直接查看完整记录。
|
||||
这条配置链路独立于 `mailProvider` 和 Hotmail 的接码模式。
|
||||
@@ -190,10 +191,10 @@
|
||||
1. 用户点击顶部 `贡献`
|
||||
2. sidepanel 直接通过 `SET_CONTRIBUTION_MODE` 切换运行态,不再弹确认窗口
|
||||
3. 进入贡献模式后会强制 `panelMode = cpa`,并临时清空运行态 `customPassword`、禁用运行态账号记录快照同步
|
||||
4. sidepanel 隐藏 CPA 管理地址、管理密钥、SUB2API 配置、自定义密码、本地同步等普通模式配置,并禁用来源选择、配置菜单和记录入口
|
||||
4. sidepanel 隐藏 CPA 管理地址、管理密钥、SUB2API 配置、Codex2API 配置、自定义密码、本地同步等普通模式配置,并禁用来源选择、配置菜单和记录入口
|
||||
5. 用户点击 `开始贡献` 后,不再单独走一条旁路 OAuth 流程,而是直接复用顶部 `自动` 的同一套主自动流
|
||||
6. 步骤 1~6 仍按原来的注册自动化执行
|
||||
7. 当主流程进入步骤 7 时,后台改为调用公开接口 `POST https://apikey.qzz.io/oauth/api/start` 申请贡献登录地址,而不是去 CPA / SUB2API 面板刷新 OAuth
|
||||
7. 当主流程进入步骤 7 时,后台改为调用公开接口 `POST https://apikey.qzz.io/oauth/api/start` 申请贡献登录地址,而不是去 CPA / SUB2API / Codex2API 来源刷新 OAuth
|
||||
8. 步骤 7 拿到 `session_id / auth_url / state` 后,继续沿用原有登录链路进入授权页
|
||||
9. 步骤 9 仍负责捕获 localhost callback;贡献模式下后台也会持续监听导航变化,必要时提前兼容处理 callback
|
||||
10. 步骤 10 在贡献模式下不再打开 CPA 管理页,而是围绕公开贡献会话做 callback 提交兼容和最终状态确认;当状态进入 `auto_approved / auto_rejected / manual_review_required / expired / error` 时结束
|
||||
@@ -333,12 +334,14 @@
|
||||
|
||||
- `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。
|
||||
- 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页,并先比对页面顶部显示的邮箱地址是否与当前目标邮箱一致:如果一致,就直接复用当前已登录页面;如果不一致且启用了 2925 账号池,则会先清理 cookie 再登录当前选中的账号;如果不一致且未启用账号池,则直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理。
|
||||
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。
|
||||
- `2925` 在执行自动登录后,如果登录页因为跳转或重载导致原内容脚本通信中断,后台不会立刻判失败;而是会等待当前标签页重新加载完成、重新确认内容脚本就绪后,再继续确认是否已经进入收件箱。这段登录恢复窗口当前按 2 分钟控制。
|
||||
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 在 Step 4 / Step 8 会固定使用“步骤开始时间向前回看 10 分钟”的时间窗,不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中落在该固定时间窗内的匹配邮件。
|
||||
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
|
||||
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
|
||||
- `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。
|
||||
- `2925` 在 provide 模式下仍保持宽松匹配:只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。
|
||||
- `2925` 在 receive 模式下会恢复“弱目标邮箱匹配”:只有当邮件里显式出现了其他收件邮箱时才会跳过;如果邮件里没有明确写出邮箱,仍允许继续尝试该验证码。
|
||||
- 手动点击 Step 4 重新执行时,后台会先检查 `signup-page` 认证页标签是否仍然存在;如果步骤 1 / 2 打开的认证页已经关闭,就会直接提示“请先执行步骤 1 或步骤 2,确保认证页仍然打开并停留在验证码页”,不会先重置后续步骤再报技术错误。
|
||||
- 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。
|
||||
- 如果 `2925` 邮箱页在轮询期间出现“子邮箱已达上限邮箱”,后台会记录当前时间,把当前 2925 账号禁用 24 小时,自动切到下一个可用账号并完成登录,然后直接报错结束当前尝试;如果 Auto 开启了自动重试,现有控制器会按原逻辑进入下一次尝试。
|
||||
|
||||
@@ -379,7 +382,10 @@
|
||||
|
||||
流程:
|
||||
|
||||
1. 通过 CPA / SUB2API 刷新 OAuth 地址
|
||||
1. 按当前来源刷新 OAuth 地址:
|
||||
- CPA:打开管理页并读取 OAuth 地址
|
||||
- SUB2API:打开后台并生成 OAuth 地址
|
||||
- Codex2API:直接调用后台协议 `/api/admin/oauth/generate-auth-url`
|
||||
2. 打开最新 OAuth 链接
|
||||
3. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
|
||||
4. 确保真正进入验证码页
|
||||
@@ -389,7 +395,7 @@
|
||||
|
||||
贡献模式补充:
|
||||
|
||||
- 贡献模式下,步骤 7 不再从 CPA / SUB2API 面板刷新 OAuth,而是直接调用公开贡献接口 `/oauth/api/start`
|
||||
- 贡献模式下,步骤 7 不再从 CPA / SUB2API / Codex2API 来源刷新 OAuth,而是直接调用公开贡献接口 `/oauth/api/start`
|
||||
- 贡献接口返回的 `auth_url` 会写回运行态 `oauthUrl`,后续步骤 7 / 8 / 9 继续复用现有授权链路
|
||||
|
||||
### Step 8
|
||||
@@ -436,15 +442,22 @@
|
||||
流程:
|
||||
|
||||
1. 校验 localhost callback 是否有效
|
||||
2. 判断是 CPA 还是 SUB2API
|
||||
3. 打开相应后台
|
||||
4. 提交回调地址
|
||||
2. 判断当前来源是 CPA、SUB2API 还是 Codex2API
|
||||
3. CPA / SUB2API 打开相应后台;Codex2API 直接走协议分支,不打开后台页面
|
||||
4. 提交回调地址或等价的授权码交换请求
|
||||
5. 仅当出现精确成功徽标,且该徽标不是红色/错误态、页面上也没有同时可见的失败提示时,才判定成功
|
||||
6. 识别 `认证失败:*`、`认证失败: timeout of 30000ms exceeded`、`回调 URL 提交失败: oauth flow is not pending` 等失败提示并立即报错
|
||||
7. 完成平台侧验证
|
||||
8. 追加账号运行历史成功记录
|
||||
9. 做成功后的清理与标记
|
||||
|
||||
Codex2API 补充:
|
||||
|
||||
- 步骤 7 直接调用 `POST /api/admin/oauth/generate-auth-url` 获取 `auth_url / session_id`
|
||||
- 授权页仍沿用现有 OpenAI 登录、验证码、OAuth 同意页与 localhost callback 主链
|
||||
- 步骤 10 直接调用 `POST /api/admin/oauth/exchange-code`,用 callback 中的 `code / state` 完成账号创建
|
||||
- Codex2API 这条来源不新增 panel content script,也不依赖“添加账号 -> OAuth 授权 -> 生成授权链接”页面按钮 DOM
|
||||
|
||||
贡献模式补充:
|
||||
|
||||
- 贡献模式下,步骤 10 不再打开 CPA 管理页
|
||||
@@ -678,6 +691,10 @@
|
||||
5. Step 4 / 7 的验证码流
|
||||
6. 成功收尾逻辑
|
||||
|
||||
如果来源本身提供稳定协议接口,还必须额外判断:
|
||||
|
||||
7. 是否可以不打开来源后台页面,直接把步骤 7 / 10 收敛到协议分支
|
||||
|
||||
### 新增配置项
|
||||
|
||||
必须同时检查:
|
||||
@@ -704,7 +721,7 @@
|
||||
- 新增共享恢复层:`content/auth-page-recovery.js`。
|
||||
- Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续回到密码页重提和验证码页确认流程。
|
||||
- 但如果 Step 4 的认证重试页正文中出现 `user_already_exists`,则会直接视为“当前用户已存在”:不点击 `重试`,不再回到步骤 1 重开当前轮,而是立即结束当前轮;开启自动重试时直接进入下一轮。
|
||||
- Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;若仍未恢复,则按原有可恢复失败逻辑重跑 Step 7。
|
||||
- Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;恢复成功后会优先按当前页面状态继续当前登录流程,例如直接续跑邮箱页或密码页;只有仍未恢复到可继续状态时,才按原有可恢复失败逻辑重跑 Step 7。
|
||||
- Step 7 在首次识别到登录验证码页后,不会立刻把步骤判定为成功;还会额外做一轮“收尾确认”,确保页面稳定停留在登录验证码页。如果只是短暂进入验证码页、随后又掉进登录重试页,则会先走共享恢复逻辑,再按既有可恢复失败逻辑重跑 Step 7。
|
||||
- Step 7 的这轮收尾确认是主要责任边界;Step 8 默认建立在“登录验证码页已经由 Step 7 稳定确认”的前提上,只在后台入口保留防御性回退判断,不替代 Step 7 收尾。
|
||||
- Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。
|
||||
|
||||
+9
-1
@@ -82,6 +82,14 @@
|
||||
5. 是否需要成功收尾逻辑
|
||||
6. 是否需要 README 与完整链路文档更新
|
||||
|
||||
补充约定:
|
||||
|
||||
- 如果新增来源本身已经提供稳定的后台协议接口,可以直接走协议分支接入:
|
||||
- 步骤 7 通过 `background/panel-bridge.js` 生成 `auth_url`
|
||||
- 步骤 10 通过 `background/steps/platform-verify.js` 直接提交 localhost callback
|
||||
- 这类来源优先复用现有 OpenAI 授权页与 localhost callback 主链,不要为了“看起来统一”再额外新增一套页面 DOM 自动点击内容脚本。
|
||||
- 只有当目标来源没有可用协议接口、必须依赖后台页面按钮时,才新增对应的 panel content script。
|
||||
|
||||
### 3.2.1 共享别名邮箱逻辑补充
|
||||
|
||||
当 Gmail / 2925 这类“既影响注册邮箱生成,又影响 sidepanel 表单行为”的 provider 发生变化时,必须优先检查是否应落入共享层,而不是继续把规则分散写在:
|
||||
@@ -119,7 +127,7 @@
|
||||
当前约定示例:
|
||||
|
||||
- `contributionMode` 是 sidepanel 的运行态 UI 模式,不是新的 `panelMode`
|
||||
- `panelMode` 仍然只允许 `cpa | sub2api`
|
||||
- `panelMode` 当前允许 `cpa | sub2api | codex2api`
|
||||
- 运行态模式不能混进 `PERSISTED_SETTING_DEFAULTS`
|
||||
- 运行态模式不能混进配置导入/导出
|
||||
- 如果运行态模式会临时覆盖某些持久配置的显示值,必须同时处理好“退出模式后恢复”和“自动保存不能误覆盖原配置”这两个问题
|
||||
|
||||
+8
-6
@@ -48,8 +48,8 @@
|
||||
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
|
||||
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
|
||||
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。
|
||||
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。
|
||||
- `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
|
||||
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
|
||||
- `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 内容脚本做弱匹配;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
|
||||
@@ -64,7 +64,7 @@
|
||||
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
|
||||
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
|
||||
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
|
||||
- `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证。
|
||||
- `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换。
|
||||
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
|
||||
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断。
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
## `docs/`
|
||||
|
||||
- `docs/仓库协作者AI分析PR与合并标准流程.md`:仓库协作者进行 AI 分析 PR 与合并时的流程说明。
|
||||
- `docs/使用教程.md`:面向使用者的补充教程文档,当前说明扩展更新流程与 Clash Verge 的“非港轮询”配置步骤。
|
||||
- `docs/images/交流群.jpg`:README 中展示的交流群图片资源。
|
||||
- `docs/images/十轮自动.png`:README 中展示的自动运行效果图。
|
||||
- `docs/images/微信.png`:README 中展示的微信收款码图片。
|
||||
@@ -119,10 +120,10 @@
|
||||
- `sidepanel/mail-2925-manager.js`:侧边栏 2925 账号池管理器,负责 2925 账号的新增、导入、切换、手动登录、启停、清冷却与删除。
|
||||
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
|
||||
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
|
||||
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。
|
||||
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
|
||||
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。
|
||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
|
||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密钥配置行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
|
||||
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
|
||||
|
||||
## `tests/`
|
||||
@@ -150,6 +151,7 @@
|
||||
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑。
|
||||
- `tests/background-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。
|
||||
- `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
|
||||
- `tests/background-platform-verify-codex2api.test.js`:测试 Codex2API 新来源在步骤 10 走协议式 callback 交换,不依赖后台页面注入。
|
||||
- `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
|
||||
- `tests/background-skip-step-linking.test.js`:测试手动跳过步骤 1 时,会级联跳过步骤 2~5,并仅跳过其中未完成且未运行的步骤。
|
||||
- `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成。
|
||||
|
||||
Reference in New Issue
Block a user