fix: preserve phone identity on oauth-login completion
- Merge latest origin/dev into PR #245 for the current review baseline.\n- Sync Step 7 completion identity before oauth-login step-key handling returns.\n- Cover the real oauth-login stepKey route in the message-router regression test.
This commit is contained in:
@@ -9,16 +9,22 @@
|
||||
isRecoverableStep9AuthFailure,
|
||||
LOG_PREFIX,
|
||||
setState,
|
||||
sourceRegistry = null,
|
||||
STOP_ERROR_MESSAGE,
|
||||
} = deps;
|
||||
|
||||
function getSourceLabel(source) {
|
||||
if (sourceRegistry?.getSourceLabel) {
|
||||
return sourceRegistry.getSourceLabel(source);
|
||||
}
|
||||
const labels = {
|
||||
'openai-auth': '认证页',
|
||||
'gmail-mail': 'Gmail 邮箱',
|
||||
'sidepanel': '侧边栏',
|
||||
'signup-page': '认证页',
|
||||
'vps-panel': 'CPA 面板',
|
||||
'sub2api-panel': 'SUB2API 后台',
|
||||
'codex2api-panel': 'Codex2API 后台',
|
||||
'qq-mail': 'QQ 邮箱',
|
||||
'mail-163': '163 邮箱',
|
||||
'mail-2925': '2925 邮箱',
|
||||
@@ -28,6 +34,10 @@
|
||||
'luckmail-api': 'LuckMail(API 购邮)',
|
||||
'cloudflare-temp-email': 'Cloudflare Temp Email',
|
||||
'cloudmail': 'Cloud Mail',
|
||||
'plus-checkout': 'Plus Checkout',
|
||||
'paypal-flow': 'PayPal 授权页',
|
||||
'gopay-flow': 'GoPay 授权页',
|
||||
'unknown-source': '未知来源',
|
||||
};
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
(function attachBackgroundMailRuleRegistry(root, factory) {
|
||||
root.MultiPageBackgroundMailRuleRegistry = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundMailRuleRegistryModule() {
|
||||
function createMailRuleRegistry(deps = {}) {
|
||||
const {
|
||||
defaultFlowId = 'openai',
|
||||
flowBuilders = {},
|
||||
} = deps;
|
||||
|
||||
function resolveFlowId(state = {}) {
|
||||
const activeFlowId = String(state?.activeFlowId || '').trim();
|
||||
return activeFlowId || String(defaultFlowId || '').trim() || 'openai';
|
||||
}
|
||||
|
||||
function getFlowBuilder(flowId) {
|
||||
const normalizedFlowId = String(flowId || '').trim();
|
||||
return normalizedFlowId ? flowBuilders[normalizedFlowId] || null : null;
|
||||
}
|
||||
|
||||
function getVerificationMailRule(step, state = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
if (!flowBuilder || typeof flowBuilder.getRuleDefinition !== 'function') {
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件规则定义。`);
|
||||
}
|
||||
return flowBuilder.getRuleDefinition(step, state);
|
||||
}
|
||||
|
||||
function buildVerificationPollPayload(step, state = {}, overrides = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
if (!flowBuilder || typeof flowBuilder.buildVerificationPollPayload !== 'function') {
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件轮询规则构造器。`);
|
||||
}
|
||||
return flowBuilder.buildVerificationPollPayload(step, state, overrides);
|
||||
}
|
||||
|
||||
return {
|
||||
buildVerificationPollPayload,
|
||||
getVerificationMailRule,
|
||||
resolveFlowId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createMailRuleRegistry,
|
||||
};
|
||||
});
|
||||
@@ -51,13 +51,67 @@
|
||||
getStepIdsForState,
|
||||
getLastStepIdForState,
|
||||
normalizeSignupMethod = (value = '') => String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email',
|
||||
canUsePhoneSignup = (state = {}) => Boolean(state?.phoneVerificationEnabled)
|
||||
&& !Boolean(state?.plusModeEnabled)
|
||||
&& !Boolean(state?.contributionMode),
|
||||
canUsePhoneSignup = (state = {}) => {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
|
||||
defaultFlowId: 'openai',
|
||||
}) || null;
|
||||
if (capabilityRegistry?.canUsePhoneSignup) {
|
||||
return capabilityRegistry.canUsePhoneSignup(state);
|
||||
}
|
||||
return Boolean(state?.phoneVerificationEnabled)
|
||||
&& !Boolean(state?.plusModeEnabled)
|
||||
&& !Boolean(state?.contributionMode);
|
||||
},
|
||||
resolveSignupMethod = (state = {}) => {
|
||||
const method = normalizeSignupMethod(state?.signupMethod);
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
|
||||
defaultFlowId: 'openai',
|
||||
}) || null;
|
||||
if (capabilityRegistry?.resolveSignupMethod) {
|
||||
return capabilityRegistry.resolveSignupMethod(state, method);
|
||||
}
|
||||
return method === 'phone' && canUsePhoneSignup(state) ? 'phone' : 'email';
|
||||
},
|
||||
validateAutoRunStart = (state = {}, options = {}) => {
|
||||
const validationState = options?.state || state;
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
|
||||
defaultFlowId: 'openai',
|
||||
}) || null;
|
||||
if (!capabilityRegistry?.validateAutoRunStart) {
|
||||
return { ok: true, errors: [] };
|
||||
}
|
||||
return capabilityRegistry.validateAutoRunStart({
|
||||
activeFlowId: options?.activeFlowId ?? validationState?.activeFlowId,
|
||||
panelMode: options?.panelMode ?? validationState?.panelMode,
|
||||
signupMethod: options?.signupMethod ?? validationState?.signupMethod,
|
||||
state: validationState,
|
||||
});
|
||||
},
|
||||
validateModeSwitch = (state = {}, options = {}) => {
|
||||
const validationState = options?.state || state;
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
|
||||
defaultFlowId: 'openai',
|
||||
}) || null;
|
||||
if (!capabilityRegistry?.validateModeSwitch) {
|
||||
return {
|
||||
ok: true,
|
||||
changedKeys: Array.isArray(options?.changedKeys) ? options.changedKeys : [],
|
||||
errors: [],
|
||||
normalizedUpdates: {},
|
||||
};
|
||||
}
|
||||
return capabilityRegistry.validateModeSwitch({
|
||||
activeFlowId: options?.activeFlowId ?? validationState?.activeFlowId,
|
||||
changedKeys: options?.changedKeys,
|
||||
panelMode: options?.panelMode ?? validationState?.panelMode,
|
||||
signupMethod: options?.signupMethod ?? validationState?.signupMethod,
|
||||
state: validationState,
|
||||
});
|
||||
},
|
||||
getTabId,
|
||||
getStopRequested,
|
||||
handleAutoRunLoopUnhandledError,
|
||||
@@ -421,6 +475,9 @@
|
||||
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||||
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiGroupIds !== undefined) updates.sub2apiGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: [];
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
|
||||
@@ -437,6 +494,7 @@
|
||||
const stepKey = getStepKeyForState(step, stateForStep);
|
||||
|
||||
if (stepKey === 'oauth-login') {
|
||||
await syncStepAccountIdentityFromPayload(payload);
|
||||
if (payload.skipLoginVerificationStep) {
|
||||
await setState({ loginVerificationRequestedAt: null });
|
||||
const latestState = await getState();
|
||||
@@ -938,6 +996,10 @@
|
||||
}
|
||||
}
|
||||
const state = await getState();
|
||||
const autoRunStartValidation = validateAutoRunStart(state, { state });
|
||||
if (autoRunStartValidation?.ok === false) {
|
||||
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
|
||||
}
|
||||
if (getPendingAutoRunTimerPlan(state)) {
|
||||
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
|
||||
}
|
||||
@@ -965,6 +1027,11 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
const state = await getState();
|
||||
const autoRunStartValidation = validateAutoRunStart(state, { state });
|
||||
if (autoRunStartValidation?.ok === false) {
|
||||
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
|
||||
}
|
||||
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
|
||||
return await scheduleAutoRun(totalRuns, {
|
||||
delayMinutes: message.payload?.delayMinutes,
|
||||
@@ -1036,6 +1103,16 @@
|
||||
const currentState = await getState();
|
||||
const updates = buildPersistentSettingsPayload(message.payload || {});
|
||||
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
|
||||
const modeValidation = validateModeSwitch({
|
||||
...currentState,
|
||||
...updates,
|
||||
resolvedSignupMethod: null,
|
||||
}, {
|
||||
changedKeys: Object.keys(updates),
|
||||
});
|
||||
if (modeValidation?.normalizedUpdates && Object.keys(modeValidation.normalizedUpdates).length > 0) {
|
||||
Object.assign(updates, modeValidation.normalizedUpdates);
|
||||
}
|
||||
const nextSignupState = {
|
||||
...currentState,
|
||||
...updates,
|
||||
@@ -1045,6 +1122,9 @@
|
||||
Object.prototype.hasOwnProperty.call(updates, 'phoneVerificationEnabled')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'panelMode')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'activeFlowId')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'contributionMode')
|
||||
) {
|
||||
updates.signupMethod = resolveSignupMethod(nextSignupState);
|
||||
}
|
||||
@@ -1145,7 +1225,12 @@
|
||||
);
|
||||
await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info');
|
||||
}
|
||||
return { ok: true, state: await getState(), proxyRouting };
|
||||
return {
|
||||
ok: true,
|
||||
modeValidation,
|
||||
proxyRouting,
|
||||
state: await getState(),
|
||||
};
|
||||
}
|
||||
|
||||
case 'REFRESH_GPC_CARD_BALANCE': {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
DEFAULT_CODEX2API_URL,
|
||||
DEFAULT_SUB2API_URL,
|
||||
normalizeLocalCpaStep9Mode,
|
||||
sourceRegistry = null,
|
||||
} = deps;
|
||||
|
||||
function parseUrlSafely(rawUrl) {
|
||||
@@ -66,7 +67,7 @@
|
||||
}
|
||||
|
||||
function isSignupEntryHost(hostname = '') {
|
||||
return ['chatgpt.com', 'chat.openai.com'].includes(hostname);
|
||||
return ['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com'].includes(hostname);
|
||||
}
|
||||
|
||||
function isSignupPasswordPageUrl(rawUrl) {
|
||||
@@ -117,12 +118,16 @@
|
||||
}
|
||||
|
||||
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
|
||||
if (sourceRegistry?.matchesSourceUrlFamily) {
|
||||
return sourceRegistry.matchesSourceUrlFamily(source, candidateUrl, referenceUrl);
|
||||
}
|
||||
const candidate = parseUrlSafely(candidateUrl);
|
||||
if (!candidate) return false;
|
||||
|
||||
const reference = parseUrlSafely(referenceUrl);
|
||||
|
||||
switch (source) {
|
||||
case 'openai-auth':
|
||||
case 'signup-page':
|
||||
return isSignupPageHost(candidate.hostname) || isSignupEntryHost(candidate.hostname);
|
||||
case 'duck-mail':
|
||||
|
||||
+24
-46
@@ -19,6 +19,25 @@
|
||||
SUB2API_STEP1_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 直连接口模块未加载,无法生成 OAuth 链接。');
|
||||
}
|
||||
sub2ApiApi = factory({
|
||||
addLog,
|
||||
normalizeSub2ApiUrl,
|
||||
DEFAULT_SUB2API_GROUP_NAME,
|
||||
});
|
||||
return sub2ApiApi;
|
||||
}
|
||||
|
||||
function normalizeAdminKey(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
@@ -250,7 +269,6 @@
|
||||
async function requestSub2ApiOAuthUrl(state, options = {}) {
|
||||
const { logLabel = 'OAuth 刷新' } = options;
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME;
|
||||
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
@@ -262,54 +280,14 @@
|
||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await addLog(`${logLabel}:正在打开 SUB2API 后台...`);
|
||||
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl);
|
||||
|
||||
const tab = typeof createAutomationTab === 'function'
|
||||
? await createAutomationTab({ url: sub2apiUrl, active: true })
|
||||
: await chrome.tabs.create({ url: sub2apiUrl, active: true });
|
||||
const tabId = tab.id;
|
||||
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||
|
||||
await addLog(`${logLabel}:SUB2API 页面已打开,正在等待页面进入目标地址...`);
|
||||
const matchedTab = await waitForTabUrlFamily('sub2api-panel', tabId, sub2apiUrl, {
|
||||
timeoutMs: 15000,
|
||||
retryDelayMs: 400,
|
||||
});
|
||||
if (!matchedTab) {
|
||||
await addLog(`${logLabel}:SUB2API 页面尚未稳定,继续尝试连接内容脚本...`, 'warn');
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
||||
inject: injectFiles,
|
||||
injectSource: 'sub2api-panel',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `${logLabel}:SUB2API 页面仍在加载,正在重试连接内容脚本...`,
|
||||
});
|
||||
|
||||
const result = await sendToContentScript('sub2api-panel', {
|
||||
type: 'REQUEST_OAUTH_URL',
|
||||
source: 'background',
|
||||
payload: {
|
||||
const api = getSub2ApiApi();
|
||||
return api.generateOpenAiAuthUrl({
|
||||
...state,
|
||||
sub2apiUrl,
|
||||
sub2apiEmail: state.sub2apiEmail,
|
||||
sub2apiPassword: state.sub2apiPassword,
|
||||
sub2apiGroupName: groupName,
|
||||
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
|
||||
sub2apiAccountPriority: state.sub2apiAccountPriority,
|
||||
logStep: 7,
|
||||
},
|
||||
}, {
|
||||
responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
logLabel,
|
||||
timeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
(function attachBackgroundRuntimeState(root, factory) {
|
||||
root.MultiPageBackgroundRuntimeState = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundRuntimeStateModule() {
|
||||
function createRuntimeStateHelpers(deps = {}) {
|
||||
const {
|
||||
DEFAULT_ACTIVE_FLOW_ID = 'openai',
|
||||
defaultStepStatuses = {},
|
||||
getStepDefinitionForState = null,
|
||||
} = deps;
|
||||
|
||||
const RUNTIME_SHARED_FIELDS = Object.freeze([
|
||||
'automationWindowId',
|
||||
'tabRegistry',
|
||||
'sourceLastUrls',
|
||||
'flowStartTime',
|
||||
]);
|
||||
const RUNTIME_PROXY_FIELDS = Object.freeze([
|
||||
'ipProxyApiPool',
|
||||
'ipProxyApiCurrentIndex',
|
||||
'ipProxyApiCurrent',
|
||||
'ipProxyAccountPool',
|
||||
'ipProxyAccountCurrentIndex',
|
||||
'ipProxyAccountCurrent',
|
||||
'ipProxyPool',
|
||||
'ipProxyCurrentIndex',
|
||||
'ipProxyCurrent',
|
||||
]);
|
||||
const OPENAI_FLOW_FIELD_GROUPS = Object.freeze({
|
||||
auth: Object.freeze([
|
||||
'oauthUrl',
|
||||
'localhostUrl',
|
||||
]),
|
||||
platformBinding: Object.freeze([
|
||||
'cpaOAuthState',
|
||||
'cpaManagementOrigin',
|
||||
'sub2apiSessionId',
|
||||
'sub2apiOAuthState',
|
||||
'sub2apiGroupId',
|
||||
'sub2apiDraftName',
|
||||
'sub2apiProxyId',
|
||||
'sub2apiGroupIds',
|
||||
'codex2apiSessionId',
|
||||
'codex2apiOAuthState',
|
||||
]),
|
||||
plus: Object.freeze([
|
||||
'plusCheckoutTabId',
|
||||
'plusCheckoutUrl',
|
||||
'plusCheckoutCountry',
|
||||
'plusCheckoutCurrency',
|
||||
'plusCheckoutSource',
|
||||
'plusBillingCountryText',
|
||||
'plusBillingAddress',
|
||||
'plusPaypalApprovedAt',
|
||||
'plusGoPayApprovedAt',
|
||||
'plusReturnUrl',
|
||||
'plusManualConfirmationPending',
|
||||
'plusManualConfirmationRequestId',
|
||||
'plusManualConfirmationStep',
|
||||
'plusManualConfirmationMethod',
|
||||
'plusManualConfirmationTitle',
|
||||
'plusManualConfirmationMessage',
|
||||
'gopayHelperReferenceId',
|
||||
'gopayHelperGoPayGuid',
|
||||
'gopayHelperRedirectUrl',
|
||||
'gopayHelperNextAction',
|
||||
'gopayHelperFlowId',
|
||||
'gopayHelperChallengeId',
|
||||
'gopayHelperStartPayload',
|
||||
'gopayHelperTaskId',
|
||||
'gopayHelperTaskStatus',
|
||||
'gopayHelperStatusText',
|
||||
'gopayHelperRemoteStage',
|
||||
'gopayHelperApiWaitingFor',
|
||||
'gopayHelperApiInputDeadlineAt',
|
||||
'gopayHelperApiInputWaitSeconds',
|
||||
'gopayHelperLastInputError',
|
||||
'gopayHelperOtpInvalidCount',
|
||||
'gopayHelperFailureStage',
|
||||
'gopayHelperFailureDetail',
|
||||
'gopayHelperTaskPayload',
|
||||
'gopayHelperOrderCreatedAt',
|
||||
'gopayHelperTaskProgressSignature',
|
||||
'gopayHelperTaskProgressAt',
|
||||
'gopayHelperTaskProgressTaskId',
|
||||
'gopayHelperPinPayload',
|
||||
'gopayHelperResolvedOtp',
|
||||
'gopayHelperOtpRequestId',
|
||||
'gopayHelperOtpReferenceId',
|
||||
]),
|
||||
phoneVerification: Object.freeze([
|
||||
'currentPhoneActivation',
|
||||
'phoneNumber',
|
||||
'currentPhoneVerificationCode',
|
||||
'currentPhoneVerificationCountdownEndsAt',
|
||||
'currentPhoneVerificationCountdownWindowIndex',
|
||||
'currentPhoneVerificationCountdownWindowTotal',
|
||||
'reusablePhoneActivation',
|
||||
'freeReusablePhoneActivation',
|
||||
'phoneReusableActivationPool',
|
||||
'signupPhoneNumber',
|
||||
'signupPhoneActivation',
|
||||
'signupPhoneCompletedActivation',
|
||||
'signupPhoneVerificationRequestedAt',
|
||||
'signupPhoneVerificationPurpose',
|
||||
]),
|
||||
luckmail: Object.freeze([
|
||||
'currentLuckmailPurchase',
|
||||
'currentLuckmailMailCursor',
|
||||
]),
|
||||
identity: Object.freeze([
|
||||
'resolvedSignupMethod',
|
||||
'accountIdentifierType',
|
||||
'accountIdentifier',
|
||||
'registrationEmailState',
|
||||
'email',
|
||||
'password',
|
||||
'lastEmailTimestamp',
|
||||
'lastSignupCode',
|
||||
'lastLoginCode',
|
||||
'step8VerificationTargetEmail',
|
||||
]),
|
||||
});
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => cloneValue(item));
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizePlainObject(value) {
|
||||
return isPlainObject(value) ? value : {};
|
||||
}
|
||||
|
||||
function normalizeFlowId(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized || DEFAULT_ACTIVE_FLOW_ID;
|
||||
}
|
||||
|
||||
function normalizeRunId(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizeStepNumber(value) {
|
||||
const step = Math.floor(Number(value) || 0);
|
||||
return step > 0 ? step : 0;
|
||||
}
|
||||
|
||||
function normalizeNodeStatus(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return 'pending';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildDefaultStepStatuses() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(normalizePlainObject(defaultStepStatuses)).map(([key, value]) => [
|
||||
String(key),
|
||||
normalizeNodeStatus(value),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeStepStatuses(value) {
|
||||
const base = buildDefaultStepStatuses();
|
||||
if (!isPlainObject(value)) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const next = { ...base };
|
||||
for (const [key, status] of Object.entries(value)) {
|
||||
const step = normalizeStepNumber(key);
|
||||
if (!step) continue;
|
||||
next[String(step)] = normalizeNodeStatus(status);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeLegacyStepCompat(value = {}, state = {}) {
|
||||
const candidate = normalizePlainObject(value);
|
||||
const currentStep = normalizeStepNumber(
|
||||
Object.prototype.hasOwnProperty.call(state, 'currentStep')
|
||||
? state.currentStep
|
||||
: candidate.currentStep
|
||||
);
|
||||
const stepStatuses = normalizeStepStatuses(
|
||||
Object.prototype.hasOwnProperty.call(state, 'stepStatuses')
|
||||
? state.stepStatuses
|
||||
: candidate.stepStatuses
|
||||
);
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
stepStatuses,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveStepKey(step, state = {}) {
|
||||
const numericStep = normalizeStepNumber(step);
|
||||
if (!numericStep || typeof getStepDefinitionForState !== 'function') {
|
||||
return '';
|
||||
}
|
||||
return String(getStepDefinitionForState(numericStep, state)?.key || '').trim();
|
||||
}
|
||||
|
||||
function normalizeNodeStatuses(value, state = {}, legacyStepCompat = null) {
|
||||
if (isPlainObject(value) && Object.keys(value).length > 0) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.map(([key, status]) => [String(key || '').trim(), normalizeNodeStatus(status)])
|
||||
.filter(([key]) => Boolean(key))
|
||||
);
|
||||
}
|
||||
|
||||
const compat = legacyStepCompat || normalizeLegacyStepCompat({}, state);
|
||||
const next = {};
|
||||
for (const [stepKey, status] of Object.entries(compat.stepStatuses || {})) {
|
||||
const step = normalizeStepNumber(stepKey);
|
||||
if (!step) continue;
|
||||
const nodeKey = resolveStepKey(step, state) || `step-${step}`;
|
||||
next[nodeKey] = normalizeNodeStatus(status);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function pickDefinedFields(state = {}, fields = []) {
|
||||
const next = {};
|
||||
for (const field of fields) {
|
||||
if (Object.prototype.hasOwnProperty.call(state, field)) {
|
||||
next[field] = cloneValue(state[field]);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildSharedState(baseValue = {}, state = {}) {
|
||||
return {
|
||||
...cloneValue(normalizePlainObject(baseValue)),
|
||||
...pickDefinedFields(state, RUNTIME_SHARED_FIELDS),
|
||||
};
|
||||
}
|
||||
|
||||
function buildServiceState(baseValue = {}, state = {}) {
|
||||
const base = cloneValue(normalizePlainObject(baseValue));
|
||||
return {
|
||||
...base,
|
||||
proxy: {
|
||||
...cloneValue(normalizePlainObject(base.proxy)),
|
||||
...pickDefinedFields(state, RUNTIME_PROXY_FIELDS),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function flattenOpenAiFlowState(flowState = {}) {
|
||||
const openaiState = normalizePlainObject(flowState.openai);
|
||||
const next = {};
|
||||
for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
|
||||
const group = normalizePlainObject(openaiState[groupKey]);
|
||||
for (const field of fields) {
|
||||
if (Object.prototype.hasOwnProperty.call(group, field)) {
|
||||
next[field] = cloneValue(group[field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildOpenAiFlowState(baseValue = {}, state = {}) {
|
||||
const baseFlowState = cloneValue(normalizePlainObject(baseValue));
|
||||
const baseOpenAi = cloneValue(normalizePlainObject(baseFlowState.openai));
|
||||
const openaiState = {
|
||||
...baseOpenAi,
|
||||
};
|
||||
|
||||
for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
|
||||
openaiState[groupKey] = {
|
||||
...cloneValue(normalizePlainObject(baseOpenAi[groupKey])),
|
||||
...pickDefinedFields(state, fields),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseFlowState,
|
||||
openai: openaiState,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuntimeStateDefault() {
|
||||
return {
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
activeRunId: '',
|
||||
currentNodeId: '',
|
||||
nodeStatuses: {},
|
||||
sharedState: {},
|
||||
serviceState: {
|
||||
proxy: {},
|
||||
},
|
||||
flowState: {
|
||||
openai: {
|
||||
auth: {},
|
||||
platformBinding: {},
|
||||
plus: {},
|
||||
phoneVerification: {},
|
||||
luckmail: {},
|
||||
identity: {},
|
||||
},
|
||||
},
|
||||
legacyStepCompat: {
|
||||
currentStep: 0,
|
||||
stepStatuses: buildDefaultStepStatuses(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ensureRuntimeState(state = {}) {
|
||||
const baseRuntimeState = {
|
||||
...buildRuntimeStateDefault(),
|
||||
...cloneValue(normalizePlainObject(state.runtimeState)),
|
||||
};
|
||||
const activeFlowId = normalizeFlowId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'activeFlowId')
|
||||
? state.activeFlowId
|
||||
: baseRuntimeState.activeFlowId
|
||||
);
|
||||
const legacyStepCompat = normalizeLegacyStepCompat(baseRuntimeState.legacyStepCompat, state);
|
||||
const currentNodeId = String(
|
||||
Object.prototype.hasOwnProperty.call(state, 'currentNodeId')
|
||||
? state.currentNodeId
|
||||
: (baseRuntimeState.currentNodeId || resolveStepKey(legacyStepCompat.currentStep, state))
|
||||
).trim();
|
||||
const nodeStatuses = normalizeNodeStatuses(
|
||||
Object.prototype.hasOwnProperty.call(state, 'nodeStatuses')
|
||||
? state.nodeStatuses
|
||||
: baseRuntimeState.nodeStatuses,
|
||||
state,
|
||||
legacyStepCompat
|
||||
);
|
||||
|
||||
return {
|
||||
...baseRuntimeState,
|
||||
activeFlowId,
|
||||
activeRunId: normalizeRunId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'activeRunId')
|
||||
? state.activeRunId
|
||||
: baseRuntimeState.activeRunId
|
||||
),
|
||||
currentNodeId,
|
||||
nodeStatuses,
|
||||
sharedState: buildSharedState(baseRuntimeState.sharedState, state),
|
||||
serviceState: buildServiceState(baseRuntimeState.serviceState, state),
|
||||
flowState: buildOpenAiFlowState(baseRuntimeState.flowState, state),
|
||||
legacyStepCompat,
|
||||
};
|
||||
}
|
||||
|
||||
function buildFlattenedUpdates(updates = {}) {
|
||||
const next = {
|
||||
...updates,
|
||||
};
|
||||
const runtimeState = normalizePlainObject(updates.runtimeState);
|
||||
const legacyStepCompat = normalizePlainObject(updates.legacyStepCompat);
|
||||
const sharedState = normalizePlainObject(updates.sharedState);
|
||||
const serviceState = normalizePlainObject(updates.serviceState);
|
||||
const flowState = normalizePlainObject(updates.flowState);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeFlowId')) {
|
||||
next.activeFlowId = runtimeState.activeFlowId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeRunId')) {
|
||||
next.activeRunId = runtimeState.activeRunId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'currentNodeId')) {
|
||||
next.currentNodeId = runtimeState.currentNodeId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'nodeStatuses')) {
|
||||
next.nodeStatuses = cloneValue(runtimeState.nodeStatuses);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(legacyStepCompat, 'currentStep')) {
|
||||
next.currentStep = legacyStepCompat.currentStep;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(legacyStepCompat, 'stepStatuses')) {
|
||||
next.stepStatuses = cloneValue(legacyStepCompat.stepStatuses);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'legacyStepCompat')) {
|
||||
const compatCandidate = normalizePlainObject(runtimeState.legacyStepCompat);
|
||||
if (Object.prototype.hasOwnProperty.call(compatCandidate, 'currentStep')) {
|
||||
next.currentStep = compatCandidate.currentStep;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(compatCandidate, 'stepStatuses')) {
|
||||
next.stepStatuses = cloneValue(compatCandidate.stepStatuses);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(next, pickDefinedFields(sharedState, RUNTIME_SHARED_FIELDS));
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'sharedState')) {
|
||||
Object.assign(
|
||||
next,
|
||||
pickDefinedFields(normalizePlainObject(runtimeState.sharedState), RUNTIME_SHARED_FIELDS)
|
||||
);
|
||||
}
|
||||
|
||||
const serviceProxy = normalizePlainObject(serviceState.proxy);
|
||||
Object.assign(next, pickDefinedFields(serviceProxy, RUNTIME_PROXY_FIELDS));
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'serviceState')) {
|
||||
const runtimeServiceState = normalizePlainObject(runtimeState.serviceState);
|
||||
Object.assign(
|
||||
next,
|
||||
pickDefinedFields(normalizePlainObject(runtimeServiceState.proxy), RUNTIME_PROXY_FIELDS)
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(next, flattenOpenAiFlowState(flowState));
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowState')) {
|
||||
Object.assign(next, flattenOpenAiFlowState(normalizePlainObject(runtimeState.flowState)));
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildStateView(state = {}) {
|
||||
const runtimeState = ensureRuntimeState(state);
|
||||
return {
|
||||
...state,
|
||||
activeFlowId: runtimeState.activeFlowId,
|
||||
activeRunId: runtimeState.activeRunId,
|
||||
currentNodeId: runtimeState.currentNodeId,
|
||||
nodeStatuses: cloneValue(runtimeState.nodeStatuses),
|
||||
flowState: cloneValue(runtimeState.flowState),
|
||||
sharedState: cloneValue(runtimeState.sharedState),
|
||||
serviceState: cloneValue(runtimeState.serviceState),
|
||||
legacyStepCompat: cloneValue(runtimeState.legacyStepCompat),
|
||||
currentStep: runtimeState.legacyStepCompat.currentStep,
|
||||
stepStatuses: cloneValue(runtimeState.legacyStepCompat.stepStatuses),
|
||||
runtimeState,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionStatePatch(currentState = {}, updates = {}) {
|
||||
const flattenedUpdates = buildFlattenedUpdates(updates);
|
||||
const nextState = {
|
||||
...currentState,
|
||||
...flattenedUpdates,
|
||||
};
|
||||
const runtimeState = ensureRuntimeState(nextState);
|
||||
|
||||
return {
|
||||
...flattenedUpdates,
|
||||
activeFlowId: runtimeState.activeFlowId,
|
||||
activeRunId: runtimeState.activeRunId,
|
||||
currentNodeId: runtimeState.currentNodeId,
|
||||
nodeStatuses: cloneValue(runtimeState.nodeStatuses),
|
||||
currentStep: runtimeState.legacyStepCompat.currentStep,
|
||||
stepStatuses: cloneValue(runtimeState.legacyStepCompat.stepStatuses),
|
||||
runtimeState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_ACTIVE_FLOW_ID,
|
||||
OPENAI_FLOW_FIELD_GROUPS,
|
||||
RUNTIME_PROXY_FIELDS,
|
||||
RUNTIME_SHARED_FIELDS,
|
||||
buildDefaultRuntimeState: buildRuntimeStateDefault,
|
||||
buildSessionStatePatch,
|
||||
buildStateView,
|
||||
ensureRuntimeState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createRuntimeStateHelpers,
|
||||
};
|
||||
});
|
||||
@@ -19,9 +19,29 @@
|
||||
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();
|
||||
}
|
||||
@@ -346,62 +366,22 @@
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
|
||||
await addStepLog(visibleStep, '正在打开 SUB2API 后台...');
|
||||
|
||||
let tabId = await getTabId('sub2api-panel');
|
||||
const alive = tabId && await isTabAlive('sub2api-panel');
|
||||
|
||||
if (!alive) {
|
||||
tabId = await reuseOrCreateTab('sub2api-panel', sub2apiUrl, {
|
||||
inject: injectFiles,
|
||||
injectSource: 'sub2api-panel',
|
||||
reloadIfSameUrl: true,
|
||||
});
|
||||
} else {
|
||||
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl, { excludeTabIds: [tabId] });
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
||||
inject: injectFiles,
|
||||
injectSource: 'sub2api-panel',
|
||||
});
|
||||
|
||||
await addStepLog(visibleStep, '正在向 SUB2API 提交回调并创建账号...');
|
||||
const requestMessage = {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: platformVerifyStep,
|
||||
source: 'background',
|
||||
payload: {
|
||||
localhostUrl: state.localhostUrl,
|
||||
visibleStep,
|
||||
sub2apiUrl,
|
||||
sub2apiEmail: state.sub2apiEmail,
|
||||
sub2apiPassword: state.sub2apiPassword,
|
||||
sub2apiGroupName: state.sub2apiGroupName,
|
||||
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
|
||||
sub2apiAccountPriority: state.sub2apiAccountPriority,
|
||||
sub2apiProxyId: state.sub2apiProxyId,
|
||||
sub2apiSessionId: state.sub2apiSessionId,
|
||||
sub2apiOAuthState: state.sub2apiOAuthState,
|
||||
sub2apiGroupId: state.sub2apiGroupId,
|
||||
sub2apiGroupIds: state.sub2apiGroupIds,
|
||||
sub2apiDraftName: state.sub2apiDraftName,
|
||||
},
|
||||
};
|
||||
const api = getSub2ApiApi();
|
||||
const maxExchangeAttempts = 3;
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= maxExchangeAttempts; attempt += 1) {
|
||||
try {
|
||||
const result = await sendToContentScript('sub2api-panel', requestMessage, {
|
||||
responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
const result = await api.submitOpenAiCallback({
|
||||
...state,
|
||||
visibleStep,
|
||||
sub2apiUrl,
|
||||
}, {
|
||||
visibleStep,
|
||||
logLabel: `步骤 ${visibleStep}`,
|
||||
logOptions: { step: visibleStep, stepKey: 'platform-verify' },
|
||||
timeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
await completeStepFromBackground(platformVerifyStep, result);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
(function attachBackgroundSub2ApiApi(root, factory) {
|
||||
root.MultiPageBackgroundSub2ApiApi = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundSub2ApiApiModule() {
|
||||
function createSub2ApiApi(deps = {}) {
|
||||
const {
|
||||
addLog = async () => {},
|
||||
normalizeSub2ApiUrl = (value) => value,
|
||||
DEFAULT_SUB2API_GROUP_NAME = 'codex',
|
||||
fetchImpl = (...args) => fetch(...args),
|
||||
} = deps;
|
||||
|
||||
const DEFAULT_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||||
const DEFAULT_PROXY_NAME = '';
|
||||
const DEFAULT_CONCURRENCY = 10;
|
||||
const DEFAULT_PRIORITY = 1;
|
||||
const DEFAULT_RATE_MULTIPLIER = 1;
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function extractStateFromAuthUrl(authUrl = '') {
|
||||
try {
|
||||
return new URL(authUrl).searchParams.get('state') || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRedirectUri(input = DEFAULT_REDIRECT_URI) {
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/') {
|
||||
parsed.pathname = '/auth/callback';
|
||||
}
|
||||
if (parsed.pathname !== '/auth/callback') {
|
||||
throw new Error('SUB2API 回调地址必须是 /auth/callback,例如 http://localhost:1455/auth/callback');
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function getSub2ApiOrigin(rawUrl = '') {
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(rawUrl);
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
try {
|
||||
return new URL(sub2apiUrl).origin;
|
||||
} catch {
|
||||
throw new Error('SUB2API URL 格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
}
|
||||
|
||||
function getSub2ApiErrorMessage(payload, responseStatus = 500, path = '') {
|
||||
const candidates = [
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.error,
|
||||
payload?.reason,
|
||||
];
|
||||
const message = candidates.map(normalizeString).find(Boolean);
|
||||
return message || `SUB2API 请求失败(HTTP ${responseStatus}):${path}`;
|
||||
}
|
||||
|
||||
async function requestJson(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 token = normalizeString(options.token);
|
||||
const response = await fetchImpl(`${origin}${path}`, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
|
||||
if (payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'code')) {
|
||||
if (Number(payload.code) === 0) {
|
||||
return payload.data;
|
||||
}
|
||||
throw new Error(getSub2ApiErrorMessage(payload, response.status, path));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getSub2ApiErrorMessage(payload, response.status, path));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`SUB2API 请求超时:${path}`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function loginSub2Api(state = {}, options = {}) {
|
||||
const email = normalizeString(state.sub2apiEmail);
|
||||
const password = String(state.sub2apiPassword || '');
|
||||
const origin = getSub2ApiOrigin(state.sub2apiUrl);
|
||||
|
||||
if (!email) {
|
||||
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||
}
|
||||
if (!password) {
|
||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const loginData = await requestJson(origin, '/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: { email, password },
|
||||
});
|
||||
|
||||
const token = normalizeString(loginData?.access_token || loginData?.accessToken);
|
||||
if (!token) {
|
||||
throw new Error('SUB2API 登录返回缺少 access_token。');
|
||||
}
|
||||
|
||||
return {
|
||||
origin,
|
||||
token,
|
||||
user: loginData?.user || null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSub2ApiGroupNames(value) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '').split(/[\r\n,,;;]+/);
|
||||
const seen = new Set();
|
||||
const names = [];
|
||||
for (const item of source) {
|
||||
const name = normalizeString(item);
|
||||
const key = name.toLowerCase();
|
||||
if (!name || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
names.push(name);
|
||||
}
|
||||
return names.length ? names : [DEFAULT_SUB2API_GROUP_NAME];
|
||||
}
|
||||
|
||||
async function getGroupsByNames(origin, token, groupNames, options = {}) {
|
||||
const targetNames = normalizeSub2ApiGroupNames(groupNames);
|
||||
const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
|
||||
method: 'GET',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
});
|
||||
const matched = [];
|
||||
const missing = [];
|
||||
|
||||
for (const targetName of targetNames) {
|
||||
const normalized = targetName.toLowerCase();
|
||||
const group = (Array.isArray(groups) ? groups : []).find((item) => {
|
||||
const itemName = normalizeString(item?.name).toLowerCase();
|
||||
if (!itemName || itemName !== normalized) return false;
|
||||
return !item.platform || item.platform === 'openai';
|
||||
});
|
||||
if (group) {
|
||||
matched.push(group);
|
||||
} else {
|
||||
missing.push(targetName);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
throw new Error(`SUB2API 中未找到以下 openai 分组:${missing.join('、')}。`);
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
function normalizeSub2ApiProxyPreference(value) {
|
||||
return normalizeString(value);
|
||||
}
|
||||
|
||||
function resolveSub2ApiProxyPreference(state = {}) {
|
||||
if (state.sub2apiDefaultProxyName !== undefined) {
|
||||
return normalizeSub2ApiProxyPreference(state.sub2apiDefaultProxyName);
|
||||
}
|
||||
return DEFAULT_PROXY_NAME;
|
||||
}
|
||||
|
||||
function resolveSub2ApiAccountPriority(state = {}) {
|
||||
const rawValue = normalizeString(state.sub2apiAccountPriority);
|
||||
if (!rawValue) {
|
||||
return DEFAULT_PRIORITY;
|
||||
}
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isSafeInteger(numeric) || numeric < 1) {
|
||||
throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。');
|
||||
}
|
||||
return numeric;
|
||||
}
|
||||
|
||||
function normalizeProxyId(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return null;
|
||||
}
|
||||
const normalized = Number(value);
|
||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildProxyDisplayName(proxy = {}) {
|
||||
const id = normalizeProxyId(proxy.id);
|
||||
const name = normalizeString(proxy.name);
|
||||
const protocol = normalizeString(proxy.protocol);
|
||||
const host = normalizeString(proxy.host);
|
||||
const port = proxy.port === undefined || proxy.port === null ? '' : normalizeString(proxy.port);
|
||||
const address = protocol && host && port ? `${protocol}://${host}:${port}` : '';
|
||||
return [
|
||||
name || '(未命名代理)',
|
||||
id ? `#${id}` : '',
|
||||
address,
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function buildProxySearchText(proxy = {}) {
|
||||
return [
|
||||
proxy.id,
|
||||
proxy.name,
|
||||
proxy.protocol,
|
||||
proxy.host,
|
||||
proxy.port,
|
||||
buildProxyDisplayName(proxy),
|
||||
]
|
||||
.filter((value) => value !== undefined && value !== null && value !== '')
|
||||
.map((value) => normalizeString(value).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function isActiveProxy(proxy = {}) {
|
||||
const status = normalizeString(proxy.status).toLowerCase();
|
||||
return !status || status === 'active';
|
||||
}
|
||||
|
||||
function findSub2ApiProxy(proxies = [], preference = '') {
|
||||
const activeProxies = (Array.isArray(proxies) ? proxies : [])
|
||||
.filter(isActiveProxy)
|
||||
.filter((proxy) => normalizeProxyId(proxy.id));
|
||||
const normalizedPreference = normalizeSub2ApiProxyPreference(preference).toLowerCase();
|
||||
const preferredId = normalizeProxyId(normalizedPreference);
|
||||
|
||||
if (preferredId) {
|
||||
const matchedById = activeProxies.find((proxy) => normalizeProxyId(proxy.id) === preferredId);
|
||||
return {
|
||||
proxy: matchedById || null,
|
||||
reason: matchedById ? 'id' : 'missing-id',
|
||||
candidates: activeProxies,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedPreference) {
|
||||
const exactMatches = activeProxies.filter((proxy) => normalizeString(proxy.name).toLowerCase() === normalizedPreference);
|
||||
if (exactMatches.length === 1) {
|
||||
return { proxy: exactMatches[0], reason: 'name', candidates: activeProxies };
|
||||
}
|
||||
if (exactMatches.length > 1) {
|
||||
return { proxy: null, reason: 'ambiguous-name', candidates: exactMatches };
|
||||
}
|
||||
|
||||
const fuzzyMatches = activeProxies.filter((proxy) => buildProxySearchText(proxy).includes(normalizedPreference));
|
||||
if (fuzzyMatches.length === 1) {
|
||||
return { proxy: fuzzyMatches[0], reason: 'fuzzy', candidates: activeProxies };
|
||||
}
|
||||
if (fuzzyMatches.length > 1) {
|
||||
return { proxy: null, reason: 'ambiguous-fuzzy', candidates: fuzzyMatches };
|
||||
}
|
||||
|
||||
return { proxy: null, reason: 'missing-name', candidates: activeProxies };
|
||||
}
|
||||
|
||||
if (activeProxies.length === 1) {
|
||||
return { proxy: activeProxies[0], reason: 'single-active', candidates: activeProxies };
|
||||
}
|
||||
return {
|
||||
proxy: null,
|
||||
reason: activeProxies.length ? 'no-preference' : 'none-active',
|
||||
candidates: activeProxies,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveSub2ApiProxy(origin, token, preference = '', options = {}) {
|
||||
const proxies = await requestJson(origin, '/api/v1/admin/proxies/all?with_count=true', {
|
||||
method: 'GET',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
});
|
||||
if (!Array.isArray(proxies)) {
|
||||
throw new Error('SUB2API 代理列表返回格式异常,无法自动选择代理。');
|
||||
}
|
||||
|
||||
const { proxy, reason, candidates } = findSub2ApiProxy(proxies, preference);
|
||||
if (proxy) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
const configured = normalizeSub2ApiProxyPreference(preference) || '(未配置)';
|
||||
const available = (candidates || [])
|
||||
.slice(0, 8)
|
||||
.map(buildProxyDisplayName)
|
||||
.join(';') || '无可用代理';
|
||||
if (reason === 'ambiguous-name' || reason === 'ambiguous-fuzzy') {
|
||||
throw new Error(`SUB2API 默认代理“${configured}”匹配到多个代理,请改填代理 ID。候选:${available}`);
|
||||
}
|
||||
if (reason === 'missing-id') {
|
||||
throw new Error(`SUB2API 默认代理 ID “${configured}”不存在或未启用。可用代理:${available}`);
|
||||
}
|
||||
if (reason === 'missing-name') {
|
||||
throw new Error(`SUB2API 默认代理“${configured}”不存在或未启用。可用代理:${available}`);
|
||||
}
|
||||
if (reason === 'no-preference') {
|
||||
throw new Error(`SUB2API 存在多个可用代理,请在侧边栏填写默认代理名称或 ID;留空则不使用代理。可用代理:${available}`);
|
||||
}
|
||||
throw new Error('SUB2API 没有可用代理;请检查默认代理配置,或将其留空以禁用代理。');
|
||||
}
|
||||
|
||||
function buildDraftAccountName(groupName) {
|
||||
const prefix = normalizeString(groupName || DEFAULT_SUB2API_GROUP_NAME)
|
||||
.replace(/[^\w\u4e00-\u9fa5-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || DEFAULT_SUB2API_GROUP_NAME;
|
||||
const stamp = new Date().toISOString().replace(/\D/g, '').slice(2, 14);
|
||||
const random = Math.floor(Math.random() * 9000 + 1000);
|
||||
return `${prefix}-${stamp}-${random}`;
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl, visibleStep = 10) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效。`);
|
||||
}
|
||||
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
throw new Error('回调 URL 协议不正确。');
|
||||
}
|
||||
if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) {
|
||||
throw new Error(`步骤 ${visibleStep} 只接受 localhost / 127.0.0.1 回调地址。`);
|
||||
}
|
||||
if (parsed.pathname !== '/auth/callback') {
|
||||
throw new Error('回调 URL 路径必须是 /auth/callback。');
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const state = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !state) {
|
||||
throw new Error('回调 URL 中缺少 code 或 state。');
|
||||
}
|
||||
|
||||
return {
|
||||
url: parsed.toString(),
|
||||
code,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
function buildOpenAiCredentials(exchangeData) {
|
||||
const credentials = {};
|
||||
const allowedKeys = [
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'id_token',
|
||||
'expires_at',
|
||||
'email',
|
||||
'chatgpt_account_id',
|
||||
'chatgpt_user_id',
|
||||
'organization_id',
|
||||
'plan_type',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
for (const key of allowedKeys) {
|
||||
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
|
||||
credentials[key] = exchangeData[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!credentials.access_token) {
|
||||
throw new Error('SUB2API 交换授权码后未返回 access_token。');
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function buildOpenAiExtra(exchangeData) {
|
||||
const extra = {};
|
||||
const allowedKeys = ['email', 'name', 'privacy_mode'];
|
||||
|
||||
for (const key of allowedKeys) {
|
||||
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
|
||||
extra[key] = exchangeData[key];
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(extra).length ? extra : undefined;
|
||||
}
|
||||
|
||||
async function logWithOptions(message, level = 'info', options = {}) {
|
||||
await addLog(message, level, options.logOptions || {});
|
||||
}
|
||||
|
||||
async function generateOpenAiAuthUrl(state = {}, options = {}) {
|
||||
const logLabel = normalizeString(options.logLabel) || 'OAuth 刷新';
|
||||
const redirectUri = normalizeRedirectUri(options.redirectUri || DEFAULT_REDIRECT_URI);
|
||||
const groupNames = normalizeSub2ApiGroupNames(state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME);
|
||||
const groupName = groupNames[0] || DEFAULT_SUB2API_GROUP_NAME;
|
||||
|
||||
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口登录并生成 OpenAI Auth 链接...`, 'info', options);
|
||||
const { origin, token } = await loginSub2Api(state, options);
|
||||
const groups = await getGroupsByNames(origin, token, groupNames, options);
|
||||
const group = groups[0];
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(state);
|
||||
const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference, options) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const draftName = buildDraftAccountName(group.name || groupName);
|
||||
const groupLabel = groups.map((item) => `${item.name}(#${item.id})`).join('、');
|
||||
|
||||
await logWithOptions(`${logLabel}:已登录 SUB2API,使用分组 ${groupLabel}。`, 'info', options);
|
||||
if (proxy) {
|
||||
await logWithOptions(`${logLabel}:已选择 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`, 'info', options);
|
||||
} else {
|
||||
await logWithOptions(`${logLabel}:未配置 SUB2API 默认代理,本次将不使用代理。`, 'info', options);
|
||||
}
|
||||
|
||||
const authRequestBody = { redirect_uri: redirectUri };
|
||||
if (proxyId) {
|
||||
authRequestBody.proxy_id = proxyId;
|
||||
}
|
||||
|
||||
const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: authRequestBody,
|
||||
});
|
||||
|
||||
const oauthUrl = normalizeString(authData?.auth_url || authData?.authUrl);
|
||||
const sessionId = normalizeString(authData?.session_id || authData?.sessionId);
|
||||
const oauthState = normalizeString(authData?.state || extractStateFromAuthUrl(oauthUrl));
|
||||
|
||||
if (!oauthUrl || !sessionId) {
|
||||
throw new Error('SUB2API 未返回完整的 auth_url / session_id。');
|
||||
}
|
||||
|
||||
await logWithOptions(`${logLabel}:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok', options);
|
||||
return {
|
||||
oauthUrl,
|
||||
sub2apiSessionId: sessionId,
|
||||
sub2apiOAuthState: oauthState,
|
||||
sub2apiGroupId: group.id,
|
||||
sub2apiGroupIds: groups.map((item) => item.id),
|
||||
sub2apiDraftName: draftName,
|
||||
sub2apiProxyId: proxyId,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitOpenAiCallback(state = {}, options = {}) {
|
||||
const visibleStep = Number(options.visibleStep || state.visibleStep) || 10;
|
||||
const callback = parseLocalhostCallback(state.localhostUrl || '', visibleStep);
|
||||
const flowEmail = normalizeString(state.email);
|
||||
const sessionId = normalizeString(state.sub2apiSessionId);
|
||||
const expectedState = normalizeString(state.sub2apiOAuthState);
|
||||
const logLabel = normalizeString(options.logLabel) || `步骤 ${visibleStep}`;
|
||||
|
||||
if (!sessionId) {
|
||||
throw new Error('缺少 SUB2API session_id,请重新执行步骤 1。');
|
||||
}
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。');
|
||||
}
|
||||
|
||||
const { origin, token } = await loginSub2Api(state, options);
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(state);
|
||||
const preferredProxyId = normalizeProxyId(state.sub2apiProxyId);
|
||||
const proxySelector = preferredProxyId || proxyPreference;
|
||||
const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector, options) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const accountPriority = resolveSub2ApiAccountPriority(state);
|
||||
const storedGroupIds = Array.isArray(state.sub2apiGroupIds) ? state.sub2apiGroupIds : [];
|
||||
const groupIdsFromState = storedGroupIds
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
const groups = groupIdsFromState.length
|
||||
? groupIdsFromState.map((id) => ({ id }))
|
||||
: (state.sub2apiGroupId
|
||||
? [{ id: state.sub2apiGroupId, name: state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME }]
|
||||
: await getGroupsByNames(origin, token, state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME, options));
|
||||
|
||||
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口交换 OpenAI 授权码...`, 'info', options);
|
||||
if (proxy) {
|
||||
await logWithOptions(`${logLabel}:使用 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`, 'info', options);
|
||||
} else {
|
||||
await logWithOptions(`${logLabel}:未配置 SUB2API 默认代理,本次将不使用代理。`, 'info', options);
|
||||
}
|
||||
|
||||
const exchangeRequestBody = {
|
||||
session_id: sessionId,
|
||||
code: callback.code,
|
||||
state: callback.state,
|
||||
};
|
||||
if (proxyId) {
|
||||
exchangeRequestBody.proxy_id = proxyId;
|
||||
}
|
||||
|
||||
const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: exchangeRequestBody,
|
||||
});
|
||||
|
||||
const credentials = buildOpenAiCredentials(exchangeData);
|
||||
const extra = buildOpenAiExtra(exchangeData);
|
||||
const resolvedEmail = normalizeString(exchangeData?.email || credentials?.email);
|
||||
const groupIds = groups
|
||||
.map((group) => Number(group.id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
if (!groupIds.length) {
|
||||
throw new Error('SUB2API 返回的目标分组 ID 无效。');
|
||||
}
|
||||
|
||||
const accountName = resolvedEmail
|
||||
|| flowEmail
|
||||
|| normalizeString(state.sub2apiDraftName)
|
||||
|| buildDraftAccountName(state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME);
|
||||
const createPayload = {
|
||||
name: accountName,
|
||||
notes: '',
|
||||
platform: 'openai',
|
||||
type: 'oauth',
|
||||
credentials,
|
||||
concurrency: DEFAULT_CONCURRENCY,
|
||||
priority: accountPriority,
|
||||
rate_multiplier: DEFAULT_RATE_MULTIPLIER,
|
||||
group_ids: groupIds,
|
||||
auto_pause_on_expired: true,
|
||||
};
|
||||
if (proxyId) {
|
||||
createPayload.proxy_id = proxyId;
|
||||
}
|
||||
if (extra) {
|
||||
createPayload.extra = extra;
|
||||
}
|
||||
|
||||
await logWithOptions(`${logLabel}:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`, 'info', options);
|
||||
const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.createTimeoutMs,
|
||||
body: createPayload,
|
||||
});
|
||||
|
||||
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
|
||||
await logWithOptions(verifiedStatus, 'ok', options);
|
||||
return {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buildDraftAccountName,
|
||||
buildOpenAiCredentials,
|
||||
buildOpenAiExtra,
|
||||
buildProxyDisplayName,
|
||||
extractStateFromAuthUrl,
|
||||
generateOpenAiAuthUrl,
|
||||
getGroupsByNames,
|
||||
loginSub2Api,
|
||||
normalizeProxyId,
|
||||
normalizeRedirectUri,
|
||||
normalizeSub2ApiGroupNames,
|
||||
parseLocalhostCallback,
|
||||
requestJson,
|
||||
resolveSub2ApiAccountPriority,
|
||||
resolveSub2ApiProxy,
|
||||
submitOpenAiCallback,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createSub2ApiApi,
|
||||
};
|
||||
});
|
||||
+123
-35
@@ -11,6 +11,7 @@
|
||||
isRetryableContentScriptTransportError,
|
||||
LOG_PREFIX,
|
||||
matchesSourceUrlFamily,
|
||||
sourceRegistry = null,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
STOP_ERROR_MESSAGE,
|
||||
@@ -19,6 +20,65 @@
|
||||
|
||||
const pendingCommands = new Map();
|
||||
|
||||
function resolveCanonicalSource(source) {
|
||||
if (sourceRegistry?.resolveCanonicalSource) {
|
||||
return sourceRegistry.resolveCanonicalSource(source);
|
||||
}
|
||||
return String(source || '').trim();
|
||||
}
|
||||
|
||||
function getSourceKeys(source) {
|
||||
if (sourceRegistry?.getSourceKeys) {
|
||||
const registryKeys = sourceRegistry.getSourceKeys(source);
|
||||
if (Array.isArray(registryKeys) && registryKeys.length) {
|
||||
return registryKeys;
|
||||
}
|
||||
}
|
||||
const normalized = String(source || '').trim();
|
||||
return normalized ? [normalized] : [];
|
||||
}
|
||||
|
||||
function getSourceCommandKey(source) {
|
||||
const keys = getSourceKeys(source);
|
||||
return keys[0] || String(source || '').trim();
|
||||
}
|
||||
|
||||
function sourcesMatch(leftSource, rightSource) {
|
||||
const left = resolveCanonicalSource(leftSource);
|
||||
const right = resolveCanonicalSource(rightSource);
|
||||
return Boolean(left && right && left === right);
|
||||
}
|
||||
|
||||
function getSourceMapValue(record, source) {
|
||||
const map = record && typeof record === 'object' ? record : {};
|
||||
for (const key of getSourceKeys(source)) {
|
||||
if (Object.prototype.hasOwnProperty.call(map, key)) {
|
||||
return map[key];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setSourceMapValue(record, source, value) {
|
||||
const nextRecord = { ...(record || {}) };
|
||||
const keys = getSourceKeys(source);
|
||||
const canonicalKey = keys[0] || String(source || '').trim();
|
||||
for (const key of keys.slice(1)) {
|
||||
delete nextRecord[key];
|
||||
}
|
||||
if (canonicalKey) {
|
||||
nextRecord[canonicalKey] = value;
|
||||
}
|
||||
return nextRecord;
|
||||
}
|
||||
|
||||
function getCleanupOwnerSource(cleanupScope) {
|
||||
if (sourceRegistry?.getCleanupOwnerSource) {
|
||||
return sourceRegistry.getCleanupOwnerSource(cleanupScope);
|
||||
}
|
||||
return cleanupScope === 'oauth-localhost-callback' ? 'signup-page' : '';
|
||||
}
|
||||
|
||||
function normalizeAutomationWindowId(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
@@ -170,7 +230,7 @@
|
||||
}
|
||||
|
||||
async function registerTab(source, tabId) {
|
||||
const registry = await getTabRegistry();
|
||||
let registry = await getTabRegistry();
|
||||
let windowId = null;
|
||||
if (chrome?.tabs?.get && Number.isInteger(tabId)) {
|
||||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||
@@ -180,42 +240,42 @@
|
||||
}
|
||||
windowId = normalizeAutomationWindowId(tab?.windowId);
|
||||
}
|
||||
registry[source] = {
|
||||
registry = setSourceMapValue(registry, source, {
|
||||
tabId,
|
||||
ready: true,
|
||||
...(windowId !== null ? { windowId } : {}),
|
||||
};
|
||||
});
|
||||
await setState({ tabRegistry: registry });
|
||||
console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
|
||||
}
|
||||
|
||||
async function isTabAlive(source) {
|
||||
const registry = await getTabRegistry();
|
||||
const entry = registry[source];
|
||||
let registry = await getTabRegistry();
|
||||
const entry = getSourceMapValue(registry, source);
|
||||
if (!entry) return false;
|
||||
try {
|
||||
const tab = await chrome.tabs.get(entry.tabId);
|
||||
if (!(await isTabInAutomationWindow(tab))) {
|
||||
registry[source] = null;
|
||||
registry = setSourceMapValue(registry, source, null);
|
||||
await setState({ tabRegistry: registry });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
registry[source] = null;
|
||||
registry = setSourceMapValue(registry, source, null);
|
||||
await setState({ tabRegistry: registry });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTabId(source) {
|
||||
const registry = await getTabRegistry();
|
||||
const tabId = registry[source]?.tabId || null;
|
||||
let registry = await getTabRegistry();
|
||||
const tabId = getSourceMapValue(registry, source)?.tabId || null;
|
||||
if (!Number.isInteger(tabId)) {
|
||||
return null;
|
||||
}
|
||||
if (!(await isTabInAutomationWindow(tabId))) {
|
||||
registry[source] = null;
|
||||
registry = setSourceMapValue(registry, source, null);
|
||||
await setState({ tabRegistry: registry });
|
||||
return null;
|
||||
}
|
||||
@@ -225,8 +285,7 @@
|
||||
async function rememberSourceLastUrl(source, url) {
|
||||
if (!source || !url) return;
|
||||
const state = await getState();
|
||||
const sourceLastUrls = { ...(state.sourceLastUrls || {}) };
|
||||
sourceLastUrls[source] = url;
|
||||
const sourceLastUrls = setSourceMapValue(state.sourceLastUrls, source, url);
|
||||
await setState({ sourceLastUrls });
|
||||
}
|
||||
|
||||
@@ -234,7 +293,7 @@
|
||||
const { excludeTabIds = [] } = options;
|
||||
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||
const state = await getState();
|
||||
const lastUrl = state.sourceLastUrls?.[source];
|
||||
const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
|
||||
const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
|
||||
|
||||
if (!referenceUrls.length) return;
|
||||
@@ -249,9 +308,10 @@
|
||||
|
||||
await chrome.tabs.remove(matchedIds).catch(() => { });
|
||||
|
||||
const registry = await getTabRegistry();
|
||||
if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
|
||||
registry[source] = null;
|
||||
let registry = await getTabRegistry();
|
||||
const sourceEntry = getSourceMapValue(registry, source);
|
||||
if (sourceEntry?.tabId && matchedIds.includes(sourceEntry.tabId)) {
|
||||
registry = setSourceMapValue(registry, source, null);
|
||||
await setState({ tabRegistry: registry });
|
||||
}
|
||||
|
||||
@@ -274,7 +334,7 @@
|
||||
async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
|
||||
if (!isLocalhostOAuthCallbackUrl(callbackUrl)) return 0;
|
||||
|
||||
const { excludeTabIds = [] } = options;
|
||||
const { excludeTabIds = [], ownerSource = getCleanupOwnerSource('oauth-localhost-callback') } = options;
|
||||
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||
const tabs = await queryTabsInAutomationWindow({});
|
||||
const matchedIds = tabs
|
||||
@@ -286,9 +346,10 @@
|
||||
|
||||
await chrome.tabs.remove(matchedIds).catch(() => { });
|
||||
|
||||
const registry = await getTabRegistry();
|
||||
if (registry['signup-page']?.tabId && matchedIds.includes(registry['signup-page'].tabId)) {
|
||||
registry['signup-page'] = null;
|
||||
let registry = await getTabRegistry();
|
||||
const ownerEntry = getSourceMapValue(registry, ownerSource);
|
||||
if (ownerEntry?.tabId && matchedIds.includes(ownerEntry.tabId)) {
|
||||
registry = setSourceMapValue(registry, ownerSource, null);
|
||||
await setState({ tabRegistry: registry });
|
||||
}
|
||||
|
||||
@@ -468,7 +529,7 @@
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
attempt += 1;
|
||||
const pong = await pingContentScriptOnTab(tabId);
|
||||
if (pong?.ok && (!pong.source || pong.source === source)) {
|
||||
if (pong?.ok && (!pong.source || sourcesMatch(pong.source, source))) {
|
||||
console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
|
||||
await registerTab(source, tabId);
|
||||
return;
|
||||
@@ -478,9 +539,13 @@
|
||||
throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
|
||||
}
|
||||
|
||||
const registry = await getTabRegistry();
|
||||
if (registry[source]) {
|
||||
registry[source].ready = false;
|
||||
let registry = await getTabRegistry();
|
||||
const sourceEntry = getSourceMapValue(registry, source);
|
||||
if (sourceEntry) {
|
||||
registry = setSourceMapValue(registry, source, {
|
||||
...sourceEntry,
|
||||
ready: false,
|
||||
});
|
||||
await setState({ tabRegistry: registry });
|
||||
}
|
||||
|
||||
@@ -505,7 +570,7 @@
|
||||
}
|
||||
|
||||
const pongAfterInject = await pingContentScriptOnTab(tabId);
|
||||
if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
|
||||
if (pongAfterInject?.ok && (!pongAfterInject.source || sourcesMatch(pongAfterInject.source, source))) {
|
||||
console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
|
||||
await registerTab(source, tabId);
|
||||
return;
|
||||
@@ -609,14 +674,16 @@
|
||||
|
||||
function queueCommand(source, message, timeout = 15000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const commandKey = getSourceCommandKey(source);
|
||||
const timer = setTimeout(() => {
|
||||
pendingCommands.delete(source);
|
||||
pendingCommands.delete(commandKey);
|
||||
reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
|
||||
}, timeout);
|
||||
pendingCommands.set(source, {
|
||||
pendingCommands.set(commandKey, {
|
||||
message,
|
||||
resolve,
|
||||
reject,
|
||||
source,
|
||||
timer,
|
||||
responseTimeoutMs: timeout,
|
||||
});
|
||||
@@ -625,11 +692,16 @@
|
||||
}
|
||||
|
||||
function flushCommand(source, tabId) {
|
||||
const pending = pendingCommands.get(source);
|
||||
const pending = pendingCommands.get(getSourceCommandKey(source));
|
||||
if (pending) {
|
||||
clearTimeout(pending.timer);
|
||||
pendingCommands.delete(source);
|
||||
sendTabMessageWithTimeout(tabId, source, pending.message, pending.responseTimeoutMs).then(pending.resolve).catch(pending.reject);
|
||||
pendingCommands.delete(getSourceCommandKey(source));
|
||||
sendTabMessageWithTimeout(
|
||||
tabId,
|
||||
pending.source || source,
|
||||
pending.message,
|
||||
pending.responseTimeoutMs
|
||||
).then(pending.resolve).catch(pending.reject);
|
||||
console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
|
||||
}
|
||||
}
|
||||
@@ -677,18 +749,29 @@
|
||||
const sameUrl = currentTab.url === url;
|
||||
const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
|
||||
|
||||
const registry = await getTabRegistry();
|
||||
let registry = await getTabRegistry();
|
||||
const sourceEntry = getSourceMapValue(registry, source);
|
||||
if (sameUrl) {
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
if (shouldReloadOnReuse) {
|
||||
if (registry[source]) registry[source].ready = false;
|
||||
if (sourceEntry) {
|
||||
registry = setSourceMapValue(registry, source, {
|
||||
...sourceEntry,
|
||||
ready: false,
|
||||
});
|
||||
}
|
||||
await setState({ tabRegistry: registry });
|
||||
await chrome.tabs.reload(tabId);
|
||||
await waitForTabUpdateComplete(tabId);
|
||||
}
|
||||
|
||||
if (options.inject) {
|
||||
if (registry[source]) registry[source].ready = false;
|
||||
if (sourceEntry) {
|
||||
registry = setSourceMapValue(registry, source, {
|
||||
...sourceEntry,
|
||||
ready: false,
|
||||
});
|
||||
}
|
||||
await setState({ tabRegistry: registry });
|
||||
if (options.injectSource) {
|
||||
await chrome.scripting.executeScript({
|
||||
@@ -710,7 +793,12 @@
|
||||
return tabId;
|
||||
}
|
||||
|
||||
if (registry[source]) registry[source].ready = false;
|
||||
if (sourceEntry) {
|
||||
registry = setSourceMapValue(registry, source, {
|
||||
...sourceEntry,
|
||||
ready: false,
|
||||
});
|
||||
}
|
||||
await setState({ tabRegistry: registry });
|
||||
await chrome.tabs.update(tabId, { url, active: true });
|
||||
|
||||
@@ -765,7 +853,7 @@
|
||||
throwIfStopped();
|
||||
const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
|
||||
const registry = await getTabRegistry();
|
||||
const entry = registry[source];
|
||||
const entry = getSourceMapValue(registry, source);
|
||||
|
||||
if (!entry || !entry.ready) {
|
||||
throwIfStopped();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
function createVerificationFlowHelpers(deps = {}) {
|
||||
const {
|
||||
addLog: rawAddLog = async () => {},
|
||||
buildVerificationPollPayload: externalBuildVerificationPollPayload = null,
|
||||
chrome,
|
||||
closeConflictingTabsForSource,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
@@ -408,27 +409,25 @@
|
||||
}
|
||||
|
||||
function getVerificationPollPayload(step, state, overrides = {}) {
|
||||
if (typeof externalBuildVerificationPollPayload === 'function') {
|
||||
return externalBuildVerificationPollPayload(step, state, overrides);
|
||||
}
|
||||
const normalizedStep = Number(step) === 4 ? 4 : 8;
|
||||
const is2925Provider = state?.mailProvider === '2925';
|
||||
const mail2925MatchTargetEmail = is2925Provider
|
||||
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
|
||||
if (step === 4) {
|
||||
return {
|
||||
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(4, state),
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
|
||||
targetEmail: state.email,
|
||||
mail2925MatchTargetEmail,
|
||||
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
||||
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(8, state),
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
||||
targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email,
|
||||
flowId: String(state?.activeFlowId || '').trim(),
|
||||
step: normalizedStep,
|
||||
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(normalizedStep, state),
|
||||
senderFilters: [],
|
||||
subjectFilters: [],
|
||||
requiredKeywords: [],
|
||||
codePatterns: [],
|
||||
targetEmail: normalizedStep === 4
|
||||
? state.email
|
||||
: (String(state?.step8VerificationTargetEmail || '').trim() || state.email),
|
||||
targetEmailHints: [],
|
||||
mail2925MatchTargetEmail,
|
||||
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
||||
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
||||
|
||||
Reference in New Issue
Block a user