Merge remote-tracking branch 'origin/dev' into master-sync
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user