Files
2026-05-21 07:21:34 +08:00

435 lines
16 KiB
JavaScript

(function attachBackgroundStep10(root, factory) {
root.MultiPageBackgroundStep10 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep10Module() {
function createStep10Executor(deps = {}) {
const {
addLog,
chrome,
closeConflictingTabsForSource,
completeNodeFromBackground,
ensureContentScriptReadyOnTab,
getPanelMode,
getTabId,
getStepIdByKeyForState,
isLocalhostOAuthCallbackUrl,
isTabAlive,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
rememberSourceLastUrl,
reuseOrCreateTab,
sendToContentScript,
sendToContentScriptResilient,
shouldBypassStep9ForLocalCpa,
DEFAULT_SUB2API_GROUP_NAME = 'codex',
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
} = deps;
let sub2ApiApi = null;
function getSub2ApiApi() {
if (sub2ApiApi) {
return sub2ApiApi;
}
const factory = deps.createSub2ApiApi
|| self.MultiPageBackgroundSub2ApiApi?.createSub2ApiApi;
if (typeof factory !== 'function') {
throw new Error('SUB2API 直连接口模块未加载,无法提交回调。');
}
sub2ApiApi = factory({
addLog,
normalizeSub2ApiUrl,
DEFAULT_SUB2API_GROUP_NAME,
});
return sub2ApiApi;
}
function normalizeString(value = '') {
return String(value || '').trim();
}
function resolvePlatformVerifyStep(state = {}) {
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
return visibleStep >= 10 ? visibleStep : 10;
}
function resolveStepIdByKey(state = {}, stepKey = '') {
if (typeof getStepIdByKeyForState !== 'function') {
return null;
}
const step = Number(getStepIdByKeyForState(stepKey, state));
return Number.isInteger(step) && step > 0 ? step : null;
}
function resolveConfirmOauthStep(platformVerifyStep = 10, state = {}) {
const dynamicStep = resolveStepIdByKey(state, 'confirm-oauth');
if (dynamicStep && dynamicStep < Number(platformVerifyStep)) {
return dynamicStep;
}
return Math.max(1, Number(platformVerifyStep) - 1);
}
function resolveAuthLoginStep(platformVerifyStep = 10, state = {}) {
const reloginStep = resolveStepIdByKey(state, 'relogin-bound-email');
if (reloginStep && reloginStep < Number(platformVerifyStep)) {
return reloginStep;
}
const oauthLoginStep = resolveStepIdByKey(state, 'oauth-login');
if (oauthLoginStep && oauthLoginStep < Number(platformVerifyStep)) {
return oauthLoginStep;
}
return Number(platformVerifyStep) >= 13 ? 10 : 7;
}
function addStepLog(step, message, level = 'info') {
return addLog(message, level, { step, stepKey: 'platform-verify' });
}
function parseLocalhostCallback(rawUrl, platformVerifyStep = 10, state = {}) {
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep, state);
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmOauthStep}`);
}
const code = normalizeString(parsed.searchParams.get('code'));
const oauthState = normalizeString(parsed.searchParams.get('state'));
if (!code || !oauthState) {
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmOauthStep}`);
}
return {
url: parsed.toString(),
code,
state: oauthState,
};
}
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})。`;
}
function deriveCpaManagementOrigin(vpsUrl) {
const normalizedUrl = normalizeString(vpsUrl);
if (!normalizedUrl) {
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
}
let parsed;
try {
parsed = new URL(normalizedUrl);
} catch {
throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
}
return parsed.origin;
}
function getCpaApiErrorMessage(payload, responseStatus = 500) {
const details = [
payload?.error,
payload?.message,
payload?.detail,
payload?.reason,
]
.map((value) => normalizeString(value))
.find(Boolean);
return details || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
}
async function fetchCpaManagementJson(origin, path, options = {}) {
const controller = new AbortController();
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const managementKey = normalizeString(options.managementKey);
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
if (managementKey) {
headers.Authorization = `Bearer ${managementKey}`;
headers['X-Management-Key'] = managementKey;
}
const response = await fetch(`${origin}${path}`, {
method: options.method || 'POST',
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
signal: controller.signal,
});
let payload = {};
try {
payload = await response.json();
} catch {
payload = {};
}
if (!response.ok) {
throw new Error(getCpaApiErrorMessage(payload, response.status));
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('CPA 管理接口请求超时,请稍后重试。');
}
throw error;
} finally {
clearTimeout(timer);
}
}
function isSub2ApiTransientExchangeError(error) {
const message = normalizeString(error?.message || error);
if (!message) {
return false;
}
const tokenExchangeFailure = /auth\.openai\.com\/oauth\/token/i.test(message);
const transientNetworkSignal = /unexpected\s+eof|eof|connection\s+refused|i\/o\s+timeout|context\s+deadline\s+exceeded|connection\s+reset|broken\s+pipe|failed\s+to\s+fetch|temporarily\s+unavailable|timeout/i.test(message);
const transientExchangeUserSignal = /token_exchange_user_error|invalid\s+request\.\s+please\s+try\s+again\s+later/i.test(message);
if (transientExchangeUserSignal) {
return true;
}
return tokenExchangeFailure && transientNetworkSignal;
}
async function sleep(ms = 0) {
const timeout = Math.max(0, Number(ms) || 0);
if (!timeout) return;
await new Promise((resolve) => setTimeout(resolve, timeout));
}
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);
}
return executeCpaStep10(state);
}
async function executeCpaStep10(state) {
const platformVerifyStep = resolvePlatformVerifyStep(state);
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep, state);
const authLoginStep = resolveAuthLoginStep(platformVerifyStep, state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}`);
}
if (!state.localhostUrl) {
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}`);
}
if (!state.vpsUrl) {
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
}
if (shouldBypassStep9ForLocalCpa(state)) {
await addStepLog(platformVerifyStep, '检测到本地 CPA,且当前策略为“跳过平台回调验证”,本轮不再重复提交回调地址。', 'info');
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
localhostUrl: state.localhostUrl,
verifiedStatus: 'local-auto',
});
return;
}
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep, state);
const expectedState = normalizeString(state.cpaOAuthState);
if (expectedState && expectedState !== callback.state) {
throw new Error(`CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}`);
}
const managementKey = normalizeString(state.vpsPassword);
if (!managementKey) {
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
}
await addStepLog(platformVerifyStep, '正在通过 CPA 管理接口提交回调地址...');
try {
const origin = normalizeString(state.cpaManagementOrigin) || deriveCpaManagementOrigin(state.vpsUrl);
const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', {
method: 'POST',
managementKey,
body: {
provider: 'codex',
redirect_url: callback.url,
},
});
const verifiedStatus = normalizeString(result?.message)
|| normalizeString(result?.status_message)
|| 'CPA 已通过接口提交回调';
await addStepLog(platformVerifyStep, verifiedStatus, 'ok');
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
localhostUrl: callback.url,
verifiedStatus,
});
} catch (error) {
const reason = normalizeString(error?.message) || 'unknown error';
await addStepLog(platformVerifyStep, `CPA 接口提交失败:${reason}`, 'error');
throw error;
}
}
async function executeCodex2ApiStep10(state) {
const platformVerifyStep = resolvePlatformVerifyStep(state);
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep, state);
const authLoginStep = resolveAuthLoginStep(platformVerifyStep, state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}`);
}
if (!state.localhostUrl) {
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}`);
}
if (!state.codex2apiSessionId) {
throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${authLoginStep}`);
}
if (!normalizeString(state.codex2apiAdminKey)) {
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
}
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep, state);
const expectedState = normalizeString(state.codex2apiOAuthState);
if (expectedState && expectedState !== callback.state) {
throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}`);
}
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
const origin = new URL(codex2apiUrl).origin;
await addStepLog(platformVerifyStep, '正在向 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 addStepLog(platformVerifyStep, verifiedStatus, 'ok');
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
localhostUrl: callback.url,
verifiedStatus,
});
}
async function executeSub2ApiStep10(state) {
const platformVerifyStep = resolvePlatformVerifyStep(state);
const visibleStep = platformVerifyStep;
const confirmOauthStep = resolveConfirmOauthStep(visibleStep, state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}`);
}
if (!state.localhostUrl) {
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}`);
}
if (!state.sub2apiSessionId) {
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
}
if (!state.sub2apiEmail) {
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
}
if (!state.sub2apiPassword) {
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
}
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
if (!sub2apiUrl) {
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
}
const api = getSub2ApiApi();
const maxExchangeAttempts = 3;
let lastError = null;
for (let attempt = 1; attempt <= maxExchangeAttempts; attempt += 1) {
try {
const result = await api.submitOpenAiCallback({
...state,
visibleStep,
sub2apiUrl,
}, {
visibleStep,
logLabel: `步骤 ${visibleStep}`,
logOptions: { step: visibleStep, stepKey: 'platform-verify' },
timeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
});
await completeNodeFromBackground(state?.nodeId || 'platform-verify', result);
return;
} catch (error) {
lastError = error;
if (!isSub2ApiTransientExchangeError(error) || attempt >= maxExchangeAttempts) {
throw error;
}
await addLog(
`SUB2API 回调交换出现临时网络波动(${error.message}),正在重试 ${attempt + 1}/${maxExchangeAttempts}...`,
'warn',
{ step: visibleStep, stepKey: 'platform-verify' }
);
await sleep(1200 * attempt);
}
}
if (lastError) {
throw lastError;
}
}
return {
executeCpaStep10,
executeCodex2ApiStep10,
executeStep10,
executeSub2ApiStep10,
};
}
return { createStep10Executor };
});