diff --git a/background.js b/background.js index 5dc8522..c35af28 100644 --- a/background.js +++ b/background.js @@ -55,6 +55,7 @@ importScripts( 'background/steps/paypal-approve.js', 'background/steps/gopay-approve.js', 'background/steps/plus-return-confirm.js', + 'background/steps/sub2api-session-import.js', 'background/steps/oauth-login.js', 'background/steps/fetch-login-code.js', 'background/steps/confirm-oauth.js', @@ -94,6 +95,12 @@ const PLUS_PAYPAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true, plusPaymentMethod: 'paypal', }) || NORMAL_STEP_DEFINITIONS; +const PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + plusModeEnabled: true, + plusPaymentMethod: 'paypal', + plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION, +}) || PLUS_PAYPAL_STEP_DEFINITIONS; const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ activeFlowId: DEFAULT_ACTIVE_FLOW_ID, plusModeEnabled: true, @@ -112,6 +119,12 @@ const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true, plusPaymentMethod: 'gopay', }) || PLUS_PAYPAL_STEP_DEFINITIONS; +const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + plusModeEnabled: true, + plusPaymentMethod: 'gopay', + plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION, +}) || PLUS_GOPAY_STEP_DEFINITIONS; const PLUS_GOPAY_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ activeFlowId: DEFAULT_ACTIVE_FLOW_ID, plusModeEnabled: true, @@ -130,6 +143,12 @@ const PLUS_GPC_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', }) || PLUS_GOPAY_STEP_DEFINITIONS; +const PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + plusModeEnabled: true, + plusPaymentMethod: 'gpc-helper', + plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION, +}) || PLUS_GPC_STEP_DEFINITIONS; const PLUS_GPC_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ activeFlowId: DEFAULT_ACTIVE_FLOW_ID, plusModeEnabled: true, @@ -165,12 +184,15 @@ const ALL_STEP_DEFINITIONS = (() => { ...NORMAL_PHONE_STEP_DEFINITIONS, ...NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ...PLUS_PAYPAL_STEP_DEFINITIONS, + ...PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS, ...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, ...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ...PLUS_GOPAY_STEP_DEFINITIONS, + ...PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS, ...PLUS_GOPAY_PHONE_STEP_DEFINITIONS, ...PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ...PLUS_GPC_STEP_DEFINITIONS, + ...PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS, ...PLUS_GPC_PHONE_STEP_DEFINITIONS, ...PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ]; @@ -632,6 +654,8 @@ const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; +const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth'; +const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session'; const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; const MICROSOFT_TOKEN_DNR_RULE_ID = 1001; @@ -722,34 +746,108 @@ function getSignupMethodForStepDefinitions(state = {}) { return normalizeSignupMethod(state?.resolvedSignupMethod || state?.signupMethod); } +function buildResolvedStepDefinitionState(state = {}) { + const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai'; + const requestedActiveFlowId = String(state?.activeFlowId || state?.flowId || '').trim().toLowerCase() || defaultFlowId; + const requestedSignupMethod = getSignupMethodForStepDefinitions(state); + const plusModeEnabled = isPlusModeState(state); + const plusPaymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); + const capabilityState = typeof resolveCurrentFlowCapabilities === 'function' + ? resolveCurrentFlowCapabilities({ + ...state, + activeFlowId: requestedActiveFlowId, + flowId: requestedActiveFlowId, + plusModeEnabled, + plusPaymentMethod, + signupMethod: requestedSignupMethod, + }, { + activeFlowId: requestedActiveFlowId, + panelMode: state?.panelMode, + signupMethod: requestedSignupMethod, + }) + : null; + const stepDefinitionOptions = capabilityState?.stepDefinitionOptions || {}; + const resolvedActiveFlowId = String(stepDefinitionOptions.activeFlowId || requestedActiveFlowId).trim().toLowerCase() || defaultFlowId; + const resolvedSignupMethod = normalizeSignupMethod( + stepDefinitionOptions.signupMethod + || capabilityState?.effectiveSignupMethod + || requestedSignupMethod + ); + + return { + ...state, + activeFlowId: resolvedActiveFlowId, + flowId: resolvedActiveFlowId, + panelMode: stepDefinitionOptions.panelMode || capabilityState?.effectivePanelMode || state?.panelMode, + targetId: stepDefinitionOptions.targetId || capabilityState?.effectiveTargetId || state?.targetId, + plusModeEnabled: stepDefinitionOptions.plusModeEnabled === undefined + ? plusModeEnabled + : Boolean(stepDefinitionOptions.plusModeEnabled), + plusPaymentMethod, + plusAccountAccessStrategy: normalizePlusAccountAccessStrategy( + stepDefinitionOptions.plusAccountAccessStrategy + ?? capabilityState?.effectivePlusAccountAccessStrategy + ?? state?.plusAccountAccessStrategy + ), + signupMethod: resolvedSignupMethod, + resolvedSignupMethod: resolvedSignupMethod, + phoneSignupReloginAfterBindEmailEnabled: Boolean(state?.phoneSignupReloginAfterBindEmailEnabled), + }; +} + function getStepDefinitionsForState(state = {}) { + const resolvedState = buildResolvedStepDefinitionState(state); const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.MultiPageStepDefinitions?.getSteps) { const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai'; - const activeFlowId = String(state?.activeFlowId || '').trim().toLowerCase() || defaultFlowId; + const activeFlowId = String(resolvedState?.activeFlowId || '').trim().toLowerCase() || defaultFlowId; const definitions = rootScope.MultiPageStepDefinitions.getSteps({ activeFlowId, - plusModeEnabled: isPlusModeState(state), - plusPaymentMethod: normalizePlusPaymentMethod(state?.plusPaymentMethod), - signupMethod: getSignupMethodForStepDefinitions(state), - phoneSignupReloginAfterBindEmailEnabled: Boolean(state?.phoneSignupReloginAfterBindEmailEnabled), + plusModeEnabled: Boolean(resolvedState?.plusModeEnabled), + plusPaymentMethod: normalizePlusPaymentMethod(resolvedState?.plusPaymentMethod), + plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(resolvedState?.plusAccountAccessStrategy), + signupMethod: getSignupMethodForStepDefinitions(resolvedState), + phoneSignupReloginAfterBindEmailEnabled: Boolean(resolvedState?.phoneSignupReloginAfterBindEmailEnabled), }); if (Array.isArray(definitions)) { return definitions; } } - const activeFlowId = String(state?.activeFlowId || '').trim().toLowerCase(); + const activeFlowId = String(resolvedState?.activeFlowId || '').trim().toLowerCase(); if (activeFlowId && activeFlowId !== DEFAULT_ACTIVE_FLOW_ID) { return []; } - if (!isPlusModeState(state)) { + if (!Boolean(resolvedState?.plusModeEnabled)) { return NORMAL_STEP_DEFINITIONS; } - const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); + const paymentMethod = normalizePlusPaymentMethod(resolvedState?.plusPaymentMethod); + const signupMethod = getSignupMethodForStepDefinitions(resolvedState); + const plusAccountAccessStrategy = normalizePlusAccountAccessStrategy(resolvedState?.plusAccountAccessStrategy); if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { + if ( + signupMethod === SIGNUP_METHOD_EMAIL + && plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + ) { + return PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS; + } return PLUS_GPC_STEP_DEFINITIONS; } - return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS; + if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY) { + if ( + signupMethod === SIGNUP_METHOD_EMAIL + && plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + ) { + return PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS; + } + return PLUS_GOPAY_STEP_DEFINITIONS; + } + if ( + signupMethod === SIGNUP_METHOD_EMAIL + && plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + ) { + return PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS; + } + return PLUS_PAYPAL_STEP_DEFINITIONS; } function getStepIdsForState(state = {}) { @@ -806,17 +904,18 @@ function getStepIdByKeyForState(stepKey, state = {}) { } function getNodeDefinitionsForState(state = {}) { + const resolvedState = buildResolvedStepDefinitionState(state); if (workflowEngine?.getNodesForState) { - return workflowEngine.getNodesForState(state); + return workflowEngine.getNodesForState(resolvedState); } if (self.MultiPageStepDefinitions?.getNodes) { return self.MultiPageStepDefinitions.getNodes({ - ...state, - activeFlowId: state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID, - flowId: state?.flowId || state?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID, + ...resolvedState, + activeFlowId: resolvedState?.activeFlowId || resolvedState?.flowId || DEFAULT_ACTIVE_FLOW_ID, + flowId: resolvedState?.flowId || resolvedState?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID, }); } - return getStepDefinitionsForState(state) + return getStepDefinitionsForState(resolvedState) .map((definition) => ({ nodeId: String(definition?.key || '').trim(), displayOrder: Number.isFinite(Number(definition?.id)) ? Number(definition.id) : Number(definition?.order), @@ -827,19 +926,21 @@ function getNodeDefinitionsForState(state = {}) { } function getNodeIdsForState(state = {}) { + const resolvedState = buildResolvedStepDefinitionState(state); if (workflowEngine?.getNodeIdsForState) { - return workflowEngine.getNodeIdsForState(state); + return workflowEngine.getNodeIdsForState(resolvedState); } - return getNodeDefinitionsForState(state).map((definition) => definition.nodeId).filter(Boolean); + return getNodeDefinitionsForState(resolvedState).map((definition) => definition.nodeId).filter(Boolean); } function getNodeDefinitionForState(nodeId, state = {}) { const normalizedNodeId = String(nodeId || '').trim(); if (!normalizedNodeId) return null; + const resolvedState = buildResolvedStepDefinitionState(state); if (workflowEngine?.getNodeById) { - return workflowEngine.getNodeById(normalizedNodeId, state); + return workflowEngine.getNodeById(normalizedNodeId, resolvedState); } - return getNodeDefinitionsForState(state).find((definition) => definition.nodeId === normalizedNodeId) || null; + return getNodeDefinitionsForState(resolvedState).find((definition) => definition.nodeId === normalizedNodeId) || null; } function getLastNodeIdForState(state = {}) { @@ -947,6 +1048,7 @@ const PERSISTED_SETTING_DEFAULTS = { customPassword: '', plusModeEnabled: false, plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD, + plusAccountAccessStrategy: 'oauth', paypalEmail: '', paypalPassword: '', currentPayPalAccountId: '', @@ -1120,6 +1222,7 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([ 'phoneSignupReloginAfterBindEmailEnabled', 'plusModeEnabled', 'plusPaymentMethod', + 'plusAccountAccessStrategy', 'mailProvider', 'ipProxyEnabled', 'ipProxyService', @@ -1693,6 +1796,13 @@ function normalizePlusPaymentMethod(value = '') { return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL; } +function normalizePlusAccountAccessStrategy(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; +} + function normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) { const rootScope = typeof self !== 'undefined' ? self : globalThis; const rawNormalized = rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryId @@ -2866,6 +2976,10 @@ function normalizePersistentSettingValue(key, value) { return normalizeSignupMethod(value); case 'plusPaymentMethod': return normalizePlusPaymentMethod(value); + case 'plusAccountAccessStrategy': + return String(value || '').trim().toLowerCase() === 'sub2api_codex_session' + ? 'sub2api_codex_session' + : 'oauth'; case 'paypalEmail': return String(value || '').trim(); case 'paypalPassword': @@ -10241,6 +10355,7 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'plus-checkout-billing', 'paypal-approve', 'plus-checkout-return', + 'sub2api-session-import', 'oauth-login', 'fetch-login-code', 'post-login-phone-verification', @@ -11339,6 +11454,7 @@ const AUTO_RUN_NODE_DELAYS = Object.freeze({ 'gopay-subscription-confirm': 2000, 'paypal-approve': 2000, 'plus-checkout-return': 1000, + 'sub2api-session-import': 0, 'oauth-login': 2000, 'fetch-login-code': 2000, 'confirm-oauth': 1000, @@ -13096,6 +13212,7 @@ const goPayManualConfirmExecutor = self.MultiPageBackgroundGoPayManualConfirm?.c broadcastDataUpdate, chrome, getTabId, + getNodeIdsForState, isTabAlive, registerTab, createAutomationTab, @@ -13143,6 +13260,21 @@ const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.cre sleepWithStop, waitForTabUrlMatchUntilStopped, }); +const sub2ApiSessionImportExecutor = self.MultiPageBackgroundSub2ApiSessionImport?.createSub2ApiSessionImportExecutor({ + addLog, + chrome, + completeNodeFromBackground, + ensureContentScriptReadyOnTabUntilStopped, + getTabId, + isTabAlive, + normalizeSub2ApiUrl, + registerTab, + sendTabMessageUntilStopped, + sleepWithStop, + throwIfStopped, + waitForTabCompleteUntilStopped, + DEFAULT_SUB2API_GROUP_NAME, +}); const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKiroRegisterRunner({ addLog, chrome, @@ -13290,6 +13422,7 @@ const stepExecutorsByKey = { ? goPayApproveExecutor.executeGoPayApprove(state) : payPalApproveExecutor.executePayPalApprove(state), 'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state), + 'sub2api-session-import': (state) => sub2ApiSessionImportExecutor.executeSub2ApiSessionImport(state), 'oauth-login': (state) => step7Executor.executeStep7(state), 'fetch-login-code': (state) => step8Executor.executeStep8(state), 'post-login-phone-verification': (state) => step8Executor.executePostLoginPhoneVerification(state), @@ -13956,6 +14089,7 @@ async function getPostStep6AutoRestartDecision(step, error) { const errorMessage = getErrorMessage(error); const shouldForceRestartFromStep7 = /restart step 7 with a new number/i.test(errorMessage); const latestState = await getState(); + const explicitAuthChainStartStep = findStepIdByKeyForState('oauth-login', latestState); const authChainStartStep = typeof getAuthChainStartStepId === 'function' ? getAuthChainStartStepId(latestState) : FINAL_OAUTH_CHAIN_START_STEP; @@ -13963,6 +14097,20 @@ async function getPostStep6AutoRestartDecision(step, error) { ? getLastStepIdForState(latestState) : (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10); const currentNodeKey = resolveStepKey(normalizedStep, latestState); + const currentNodeIsAuthChain = typeof isAuthChainNode === 'function' + ? isAuthChainNode(currentNodeKey) + : [ + 'oauth-login', + 'fetch-login-code', + 'post-login-phone-verification', + 'bind-email', + 'fetch-bind-email-code', + 'relogin-bound-email', + 'fetch-bound-email-login-code', + 'post-bound-email-phone-verification', + 'confirm-oauth', + 'platform-verify', + ].includes(currentNodeKey); const confirmOauthStep = findStepIdByKeyForState('confirm-oauth', latestState); const boundEmailReloginStep = findStepIdByKeyForState('relogin-bound-email', latestState); const isBoundEmailReloginTailStep = [ @@ -13991,6 +14139,17 @@ async function getPostStep6AutoRestartDecision(step, error) { }; } + if (!Number.isFinite(explicitAuthChainStartStep) || explicitAuthChainStartStep <= 0 || !currentNodeIsAuthChain) { + return { + shouldRestart: false, + blockedByAddPhone: false, + forcedByPhoneVerificationTimeout: false, + restartStep: authChainStartStep, + errorMessage, + authState: null, + }; + } + if (!Number.isFinite(normalizedStep) || normalizedStep < authChainStartStep || normalizedStep > lastStepId) { return { shouldRestart: false, diff --git a/background/message-router.js b/background/message-router.js index ed6f4f4..be5fe15 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -592,6 +592,18 @@ return method === 'gopay' ? 'GoPay' : 'PayPal'; } + function normalizePlusAccountAccessStrategyForDisplay(value = '') { + return String(value || '').trim().toLowerCase() === 'sub2api_codex_session' + ? 'sub2api_codex_session' + : 'oauth'; + } + + function getPlusAccountAccessStrategyLabel(value = '') { + return normalizePlusAccountAccessStrategyForDisplay(value) === 'sub2api_codex_session' + ? '导入当前 ChatGPT 会话到 SUB2API' + : 'OAuth'; + } + async function handlePlatformVerifyStepData(payload) { if (payload.localhostUrl) { await closeLocalhostCallbackTabs(payload.localhostUrl); @@ -1421,6 +1433,9 @@ const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod') && normalizePlusPaymentMethodForDisplay(currentState?.plusPaymentMethod || 'paypal') !== normalizePlusPaymentMethodForDisplay(updates.plusPaymentMethod || 'paypal'); + const plusAccountAccessStrategyChanged = Object.prototype.hasOwnProperty.call(updates, 'plusAccountAccessStrategy') + && normalizePlusAccountAccessStrategyForDisplay(currentState?.plusAccountAccessStrategy || 'oauth') + !== normalizePlusAccountAccessStrategyForDisplay(updates.plusAccountAccessStrategy || 'oauth'); const phoneSignupReloginAfterBindEmailChanged = Object.prototype.hasOwnProperty.call(updates, 'phoneSignupReloginAfterBindEmailEnabled') && Boolean(currentState?.phoneSignupReloginAfterBindEmailEnabled) !== Boolean(updates.phoneSignupReloginAfterBindEmailEnabled); const nextPlusModeEnabled = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled') @@ -1428,6 +1443,7 @@ : Boolean(currentState?.plusModeEnabled); const stepModeChanged = modeChanged || (nextPlusModeEnabled && plusPaymentChanged) + || (nextPlusModeEnabled && plusAccountAccessStrategyChanged) || phoneSignupReloginAfterBindEmailChanged; const oauthFlowTimeoutDisabled = Object.prototype.hasOwnProperty.call(updates, 'oauthFlowTimeoutEnabled') && updates.oauthFlowTimeoutEnabled === false; @@ -1450,11 +1466,45 @@ ? nextHostPreference : ''; } - if (stepModeChanged && typeof getStepIdsForState === 'function') { - const nextStateForSteps = { ...currentState, ...stateUpdates }; - const nextNodeIds = typeof getNodeIdsForState === 'function' - ? getNodeIdsForState(nextStateForSteps) - : getStepIdsForState(nextStateForSteps).map((stepId) => getStepKeyForState(stepId, nextStateForSteps)).filter(Boolean); + const currentNodeIds = typeof getNodeIdsForState === 'function' + ? getNodeIdsForState(currentState) + : (typeof getStepIdsForState === 'function' + ? getStepIdsForState(currentState).map((stepId) => getStepKeyForState(stepId, currentState)).filter(Boolean) + : []); + const nextStateForSteps = { ...currentState, ...stateUpdates }; + const nextNodeIds = typeof getNodeIdsForState === 'function' + ? getNodeIdsForState(nextStateForSteps) + : (typeof getStepIdsForState === 'function' + ? getStepIdsForState(nextStateForSteps).map((stepId) => getStepKeyForState(stepId, nextStateForSteps)).filter(Boolean) + : []); + const nodeTopologyChanged = currentNodeIds.length !== nextNodeIds.length + || currentNodeIds.some((nodeId, index) => nodeId !== nextNodeIds[index]); + const shouldRebuildNodeStatuses = stepModeChanged || nodeTopologyChanged; + if (shouldRebuildNodeStatuses && nextNodeIds.length > 0) { + Object.assign(stateUpdates, { + oauthUrl: null, + localhostUrl: null, + oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, + cpaOAuthState: null, + cpaManagementOrigin: null, + sub2apiSessionId: null, + sub2apiOAuthState: null, + sub2apiGroupId: null, + sub2apiGroupIds: [], + sub2apiDraftName: null, + sub2apiProxyId: null, + codex2apiSessionId: null, + codex2apiOAuthState: null, + plusManualConfirmationPending: false, + plusManualConfirmationRequestId: '', + plusManualConfirmationStep: 0, + plusManualConfirmationMethod: '', + plusManualConfirmationTitle: '', + plusManualConfirmationMessage: '', + }); + } + if (shouldRebuildNodeStatuses && nextNodeIds.length > 0) { stateUpdates.nodeStatuses = Object.fromEntries(nextNodeIds.map((nodeId) => [nodeId, 'pending'])); stateUpdates.currentNodeId = ''; } @@ -1510,9 +1560,12 @@ const selectedPlusPaymentMethod = getPlusPaymentMethodLabel( stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal' ); + const selectedPlusAccountAccessStrategy = getPlusAccountAccessStrategyLabel( + stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth' + ); await addLog( Boolean(updates.plusModeEnabled) - ? `Plus 模式已开启,已切换为 Plus Checkout 步骤,当前支付方式:${selectedPlusPaymentMethod}。` + ? `Plus 模式已开启,已切换为 Plus Checkout 步骤,当前支付方式:${selectedPlusPaymentMethod},账号接入策略:${selectedPlusAccountAccessStrategy}。` : 'Plus 模式已关闭,已恢复普通注册授权步骤。', 'info' ); @@ -1521,6 +1574,11 @@ stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal' ); await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info'); + } else if (plusAccountAccessStrategyChanged && nextPlusModeEnabled) { + const selectedPlusAccountAccessStrategy = getPlusAccountAccessStrategyLabel( + stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth' + ); + await addLog(`Plus 账号接入策略已切换为 ${selectedPlusAccountAccessStrategy},已更新对应的 Plus 尾链。`, 'info'); } return { ok: true, diff --git a/background/steps/gopay-manual-confirm.js b/background/steps/gopay-manual-confirm.js index 0fe4a7a..e9942cf 100644 --- a/background/steps/gopay-manual-confirm.js +++ b/background/steps/gopay-manual-confirm.js @@ -2,8 +2,42 @@ root.MultiPageBackgroundGoPayManualConfirm = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGoPayManualConfirmModule() { const PLUS_CHECKOUT_SOURCE = 'plus-checkout'; + const GOPAY_CONFIRM_NODE_ID = 'gopay-subscription-confirm'; + const SUB2API_SESSION_IMPORT_NODE_ID = 'sub2api-session-import'; const DEFAULT_CONFIRM_TITLE = 'GoPay 订阅确认'; - const DEFAULT_CONFIRM_MESSAGE = 'GoPay 订阅页已打开。请先手动完成订阅,完成后确认继续 OAuth 登录。'; + const OAUTH_CONTINUATION_LABEL = 'OAuth 登录'; + const SUB2API_SESSION_CONTINUATION_LABEL = '导入当前 ChatGPT 会话到 SUB2API'; + + function normalizeString(value = '') { + return String(value || '').trim(); + } + + function getContinuationActionLabel(state = {}, options = {}) { + const { getNodeIdsForState = null } = options; + if (typeof getNodeIdsForState === 'function') { + const nodeIds = getNodeIdsForState(state) + .map((nodeId) => normalizeString(nodeId)) + .filter(Boolean); + const currentNodeId = normalizeString(state?.nodeId || GOPAY_CONFIRM_NODE_ID) || GOPAY_CONFIRM_NODE_ID; + const currentNodeIndex = nodeIds.indexOf(currentNodeId); + const nextNodeId = currentNodeIndex >= 0 + ? normalizeString(nodeIds[currentNodeIndex + 1]) + : ''; + + if ( + nextNodeId === SUB2API_SESSION_IMPORT_NODE_ID + || (currentNodeIndex < 0 && nodeIds.includes(SUB2API_SESSION_IMPORT_NODE_ID)) + ) { + return SUB2API_SESSION_CONTINUATION_LABEL; + } + } + return OAUTH_CONTINUATION_LABEL; + } + + function buildDefaultConfirmMessage(state = {}, options = {}) { + const continuationActionLabel = getContinuationActionLabel(state, options); + return `GoPay 订阅页已打开。请先手动完成订阅,完成后确认继续${continuationActionLabel}。`; + } function createGoPayManualConfirmExecutor(deps = {}) { const { @@ -12,6 +46,7 @@ chrome, createAutomationTab = null, getTabId, + getNodeIdsForState = null, isTabAlive, registerTab, setState, @@ -40,7 +75,7 @@ } } - const checkoutUrl = String(state?.plusCheckoutUrl || '').trim(); + const checkoutUrl = normalizeString(state?.plusCheckoutUrl); if (!checkoutUrl) { throw new Error('步骤 7:未检测到 GoPay 订阅页,请先执行步骤 6。'); } @@ -63,19 +98,21 @@ } async function executeGoPayManualConfirm(state = {}) { + const visibleStep = Number(state?.visibleStep) || 7; const tabId = await resolveCheckoutTabId(state); if (chrome?.tabs?.update && tabId) { await chrome.tabs.update(tabId, { active: true }).catch(() => {}); } + const continuationActionLabel = getContinuationActionLabel(state, { getNodeIdsForState }); const payload = { plusCheckoutTabId: tabId, plusManualConfirmationPending: true, plusManualConfirmationRequestId: buildRequestId(), - plusManualConfirmationStep: 7, + plusManualConfirmationStep: visibleStep, plusManualConfirmationMethod: 'gopay', plusManualConfirmationTitle: DEFAULT_CONFIRM_TITLE, - plusManualConfirmationMessage: DEFAULT_CONFIRM_MESSAGE, + plusManualConfirmationMessage: buildDefaultConfirmMessage(state, { getNodeIdsForState }), }; await setState(payload); @@ -83,7 +120,10 @@ broadcastDataUpdate(payload); } - await addLog('步骤 7:正在等待手动完成 GoPay 订阅,确认后继续 OAuth 登录。', 'info'); + await addLog( + `步骤 ${visibleStep}:正在等待手动完成 GoPay 订阅,确认后继续${continuationActionLabel}。`, + 'info' + ); } return { diff --git a/background/steps/sub2api-session-import.js b/background/steps/sub2api-session-import.js new file mode 100644 index 0000000..80640c0 --- /dev/null +++ b/background/steps/sub2api-session-import.js @@ -0,0 +1,185 @@ +(function attachBackgroundSub2ApiSessionImport(root, factory) { + root.MultiPageBackgroundSub2ApiSessionImport = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundSub2ApiSessionImportModule() { + const PLUS_CHECKOUT_SOURCE = 'plus-checkout'; + const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js']; + + function createSub2ApiSessionImportExecutor(deps = {}) { + const { + addLog: rawAddLog = async () => {}, + chrome, + completeNodeFromBackground, + ensureContentScriptReadyOnTabUntilStopped, + getTabId, + isTabAlive, + normalizeSub2ApiUrl = (value) => value, + registerTab, + sendTabMessageUntilStopped, + sleepWithStop = async () => {}, + throwIfStopped = () => {}, + waitForTabCompleteUntilStopped = async () => {}, + DEFAULT_SUB2API_GROUP_NAME = 'codex', + } = deps; + + let sub2ApiApi = null; + + function addStepLog(step, message, level = 'info') { + return rawAddLog(message, level, { + step, + stepKey: 'sub2api-session-import', + }); + } + + function getSub2ApiApi() { + if (sub2ApiApi) { + return sub2ApiApi; + } + const factory = deps.createSub2ApiApi + || self.MultiPageBackgroundSub2ApiApi?.createSub2ApiApi; + if (typeof factory !== 'function') { + throw new Error('SUB2API 接口模块未加载,无法导入当前 ChatGPT 会话。'); + } + sub2ApiApi = factory({ + addLog: rawAddLog, + normalizeSub2ApiUrl, + DEFAULT_SUB2API_GROUP_NAME, + }); + return sub2ApiApi; + } + + function normalizeString(value = '') { + return String(value || '').trim(); + } + + function resolveVisibleStep(state = {}) { + const visibleStep = Math.floor(Number(state?.visibleStep) || 0); + return visibleStep > 0 ? visibleStep : 10; + } + + function isSupportedChatGptSessionUrl(url = '') { + try { + const parsed = new URL(String(url || '')); + if (!/^https?:$/i.test(parsed.protocol)) { + return false; + } + const hostname = String(parsed.hostname || '').trim().toLowerCase(); + return /(^|\.)chatgpt\.com$/.test(hostname) + || hostname === 'chat.openai.com' + || /(^|\.)openai\.com$/.test(hostname); + } catch { + return false; + } + } + + async function resolveSessionTabId(state = {}) { + const registeredTabId = typeof getTabId === 'function' + ? await getTabId(PLUS_CHECKOUT_SOURCE) + : null; + if (registeredTabId && typeof isTabAlive === 'function' && await isTabAlive(PLUS_CHECKOUT_SOURCE)) { + return Number(registeredTabId) || 0; + } + + const storedTabId = Number(state?.plusCheckoutTabId) || 0; + if (storedTabId && chrome?.tabs?.get) { + const tab = await chrome.tabs.get(storedTabId).catch(() => null); + if (tab?.id) { + if (typeof registerTab === 'function') { + await registerTab(PLUS_CHECKOUT_SOURCE, tab.id); + } + return tab.id; + } + } + + throw new Error('未找到可读取 ChatGPT 会话的 Plus 标签页,请先完成当前 Plus 支付链路。'); + } + + async function getResolvedSessionTab(tabId, visibleStep) { + const tab = await chrome?.tabs?.get?.(tabId).catch(() => null); + if (!tab?.id) { + throw new Error(`步骤 ${visibleStep}:Plus 会话标签页不存在或已关闭,无法继续导入 SUB2API。`); + } + if (!isSupportedChatGptSessionUrl(tab.url)) { + throw new Error(`步骤 ${visibleStep}:当前标签页不在 ChatGPT / OpenAI 页面,无法读取当前登录会话。`); + } + return tab; + } + + async function readCurrentChatGptSession(tabId, visibleStep) { + await waitForTabCompleteUntilStopped(tabId); + await sleepWithStop(1000); + await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, { + inject: PLUS_CHECKOUT_INJECT_FILES, + injectSource: PLUS_CHECKOUT_SOURCE, + logMessage: `步骤 ${visibleStep}:正在等待 ChatGPT 会话页完成加载,再继续读取当前登录会话...`, + }); + + const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, { + type: 'PLUS_CHECKOUT_GET_STATE', + source: 'background', + payload: { + includeSession: true, + includeAccessToken: true, + }, + }); + if (sessionResult?.error) { + throw new Error(sessionResult.error); + } + + const session = sessionResult?.session && typeof sessionResult.session === 'object' && !Array.isArray(sessionResult.session) + ? sessionResult.session + : null; + const accessToken = normalizeString( + sessionResult?.accessToken + || session?.accessToken + ); + if (!session && !accessToken) { + throw new Error(`步骤 ${visibleStep}:未读取到有效的 ChatGPT 会话或 accessToken,请确认当前标签页仍处于已登录状态。`); + } + + return { + session, + accessToken, + }; + } + + async function executeSub2ApiSessionImport(state = {}) { + throwIfStopped(); + const visibleStep = resolveVisibleStep(state); + const api = getSub2ApiApi(); + + await addStepLog(visibleStep, '正在定位当前 Plus 会话页并准备导入 SUB2API...', 'info'); + const tabId = await resolveSessionTabId(state); + const tab = await getResolvedSessionTab(tabId, visibleStep); + if (chrome?.tabs?.update) { + await chrome.tabs.update(tab.id, { active: true }).catch(() => {}); + } + + await addStepLog(visibleStep, '正在读取当前 ChatGPT 登录会话...', 'info'); + const sessionState = await readCurrentChatGptSession(tab.id, visibleStep); + throwIfStopped(); + + const result = await api.importCurrentChatGptSession({ + ...state, + session: sessionState.session, + accessToken: sessionState.accessToken, + }, { + visibleStep, + logLabel: `步骤 ${visibleStep}`, + logOptions: { step: visibleStep, stepKey: 'sub2api-session-import' }, + timeoutMs: 120000, + importTimeoutMs: 120000, + }); + + await completeNodeFromBackground(state?.nodeId || 'sub2api-session-import', result); + } + + return { + executeSub2ApiSessionImport, + isSupportedChatGptSessionUrl, + }; + } + + return { + createSub2ApiSessionImportExecutor, + }; +}); diff --git a/background/sub2api-api.js b/background/sub2api-api.js index b23ade1..2c8c97d 100644 --- a/background/sub2api-api.js +++ b/background/sub2api-api.js @@ -345,6 +345,83 @@ return `${prefix}-${stamp}-${random}`; } + function normalizeCodexSessionObject(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : null; + } + + function buildCodexSessionImportContent(session, accessToken = '') { + const normalizedAccessToken = normalizeString(accessToken); + const sessionObject = normalizeCodexSessionObject(session); + + if (sessionObject) { + const contentObject = normalizedAccessToken + ? { + ...sessionObject, + accessToken: normalizedAccessToken, + } + : sessionObject; + return JSON.stringify(contentObject); + } + + if (normalizedAccessToken) { + return normalizedAccessToken; + } + + throw new Error('未读取到可导入的 ChatGPT 会话或 accessToken。'); + } + + function resolveCodexSessionImportExpiresAt(session) { + const sessionObject = normalizeCodexSessionObject(session); + const expiresValue = normalizeString(sessionObject?.expires); + if (!expiresValue) { + return null; + } + const expiresAtMs = Date.parse(expiresValue); + if (!Number.isFinite(expiresAtMs) || expiresAtMs <= 0) { + return null; + } + return Math.floor(expiresAtMs / 1000); + } + + function normalizeCodexSessionImportMessages(messages) { + return (Array.isArray(messages) ? messages : []) + .map((item, index) => ({ + index: Number(item?.index) || index + 1, + name: normalizeString(item?.name), + message: normalizeString(item?.message), + })) + .filter((item) => item.message); + } + + function normalizeCodexSessionImportResult(result) { + return { + total: Math.max(0, Number(result?.total) || 0), + created: Math.max(0, Number(result?.created) || 0), + updated: Math.max(0, Number(result?.updated) || 0), + skipped: Math.max(0, Number(result?.skipped) || 0), + failed: Math.max(0, Number(result?.failed) || 0), + items: Array.isArray(result?.items) ? result.items : [], + warnings: normalizeCodexSessionImportMessages(result?.warnings), + errors: normalizeCodexSessionImportMessages(result?.errors), + }; + } + + function buildCodexSessionImportSummary(result) { + const normalized = normalizeCodexSessionImportResult(result); + return `SUB2API 会话导入完成:新建 ${normalized.created},更新 ${normalized.updated},跳过 ${normalized.skipped},失败 ${normalized.failed}`; + } + + function getCodexSessionImportFailureMessage(result) { + const normalized = normalizeCodexSessionImportResult(result); + const detail = normalized.errors.map((item) => item.message).find(Boolean) + || normalized.warnings.map((item) => item.message).find(Boolean) + || normalized.items + .map((item) => normalizeString(item?.message)) + .find(Boolean) + || buildCodexSessionImportSummary(normalized); + return detail || 'SUB2API 会话导入失败。'; + } + function parseLocalhostCallback(rawUrl, visibleStep = 10) { let parsed; try { @@ -580,14 +657,93 @@ }; } + async function importCurrentChatGptSession(state = {}, options = {}) { + const logLabel = normalizeString(options.logLabel) || 'SUB2API 会话导入'; + const session = normalizeCodexSessionObject(state?.session); + const accessToken = normalizeString( + state?.accessToken + || session?.accessToken + ); + const importContent = buildCodexSessionImportContent(session, accessToken); + const importExpiresAt = resolveCodexSessionImportExpiresAt(session); + + await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口登录并准备导入当前 ChatGPT 会话...`, 'info', options); + const { origin, token } = await loginSub2Api(state, options); + const groupNames = state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME; + const groups = await getGroupsByNames(origin, token, groupNames, options); + const groupLabel = groups.map((item) => `${item.name}(${item.id})`).join('、'); + const proxyPreference = resolveSub2ApiProxyPreference(state); + const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference, options) : null; + const proxyId = normalizeProxyId(proxy?.id); + const accountPriority = resolveSub2ApiAccountPriority(state); + + 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 importPayload = { + content: importContent, + group_ids: groups + .map((group) => Number(group?.id)) + .filter((id) => Number.isFinite(id) && id > 0), + priority: accountPriority, + auto_pause_on_expired: true, + update_existing: true, + }; + if (!importPayload.group_ids.length) { + throw new Error('SUB2API 返回的目标分组 ID 无效。'); + } + if (proxyId) { + importPayload.proxy_id = proxyId; + } + if (importExpiresAt) { + importPayload.expires_at = importExpiresAt; + } + + await logWithOptions(`${logLabel}:正在导入当前 ChatGPT 会话到 SUB2API...`, 'info', options); + const importResult = normalizeCodexSessionImportResult(await requestJson(origin, '/api/v1/admin/accounts/import/codex-session', { + method: 'POST', + token, + timeoutMs: options.importTimeoutMs || options.timeoutMs, + body: importPayload, + })); + + for (const warning of importResult.warnings) { + await logWithOptions(`${logLabel}:${warning.message}`, 'warn', options); + } + + if (importResult.failed > 0) { + throw new Error(getCodexSessionImportFailureMessage(importResult)); + } + if (importResult.created <= 0 && importResult.updated <= 0) { + throw new Error(getCodexSessionImportFailureMessage(importResult)); + } + + const verifiedStatus = buildCodexSessionImportSummary(importResult); + await logWithOptions(verifiedStatus, 'ok', options); + return { + verifiedStatus, + sub2apiImportTotal: importResult.total, + sub2apiImportCreated: importResult.created, + sub2apiImportUpdated: importResult.updated, + sub2apiImportSkipped: importResult.skipped, + sub2apiImportFailed: importResult.failed, + }; + } + return { buildDraftAccountName, + buildCodexSessionImportContent, buildOpenAiCredentials, buildOpenAiExtra, buildProxyDisplayName, extractStateFromAuthUrl, generateOpenAiAuthUrl, getGroupsByNames, + importCurrentChatGptSession, loginSub2Api, normalizeProxyId, normalizeRedirectUri, diff --git a/data/step-definitions.js b/data/step-definitions.js index 1291776..7a6048f 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -5,6 +5,8 @@ const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; + const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth'; + const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session'; const PLUS_PAYMENT_STEP_KEY = 'paypal-approve'; const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_PHONE = 'phone'; @@ -92,10 +94,41 @@ ]; } + function createSub2ApiSessionImportTail(startId, startOrder) { + const id = Number(startId) || 10; + const order = Number(startOrder) || id * 10; + return [ + { + id, + order, + key: 'sub2api-session-import', + title: '导入当前 ChatGPT 会话到 SUB2API', + sourceId: 'sub2api-panel', + driverId: 'background/sub2api-session-import', + command: 'sub2api-session-import', + }, + ]; + } + + function normalizePlusAccountAccessStrategy(value = '') { + return String(value || '').trim().toLowerCase() === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; + } + + function shouldUseSub2ApiSessionImportTail(options = {}, signupMethod = SIGNUP_METHOD_EMAIL) { + return signupMethod === SIGNUP_METHOD_EMAIL + && normalizePlusAccountAccessStrategy(options?.plusAccountAccessStrategy) + === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION; + } + function createOpenAiSteps(prefixSteps, startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL, options = {}) { + const tailSteps = shouldUseSub2ApiSessionImportTail(options, signupMethod) + ? createSub2ApiSessionImportTail(startId, startOrder) + : createOpenAiAuthTail(startId, startOrder, signupMethod, options); return [ ...prefixSteps, - ...createOpenAiAuthTail(startId, startOrder, signupMethod, options), + ...tailSteps, ]; } @@ -103,12 +136,33 @@ const NORMAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE); const NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); const PLUS_PAYPAL_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); + const PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps( + PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, + 10, + 100, + SIGNUP_METHOD_EMAIL, + { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION } + ); const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); const PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); const PLUS_GOPAY_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); + const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps( + PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, + 10, + 100, + SIGNUP_METHOD_EMAIL, + { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION } + ); const PLUS_GOPAY_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); const PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); const PLUS_GPC_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GPC_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); + const PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps( + PLUS_GPC_PREFIX_STEP_DEFINITIONS, + 10, + 100, + SIGNUP_METHOD_EMAIL, + { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION } + ); const PLUS_GPC_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GPC_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); const PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GPC_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); const KIRO_STEP_DEFINITIONS = [ @@ -244,12 +298,16 @@ return NORMAL_STEP_DEFINITIONS; } const paymentMethod = normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod); + const plusAccountAccessStrategy = normalizePlusAccountAccessStrategy(options?.plusAccountAccessStrategy); if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { if (signupMethod === SIGNUP_METHOD_PHONE) { return reloginAfterBindEmail ? PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS : PLUS_GPC_PHONE_STEP_DEFINITIONS; } + if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) { + return PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS; + } return PLUS_GPC_STEP_DEFINITIONS; } if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY) { @@ -258,6 +316,9 @@ ? PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS : PLUS_GOPAY_PHONE_STEP_DEFINITIONS; } + if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) { + return PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS; + } return PLUS_GOPAY_STEP_DEFINITIONS; } if (signupMethod === SIGNUP_METHOD_PHONE) { @@ -265,6 +326,9 @@ ? PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS : PLUS_PAYPAL_PHONE_STEP_DEFINITIONS; } + if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) { + return PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS; + } return PLUS_PAYPAL_STEP_DEFINITIONS; } @@ -299,12 +363,15 @@ ...NORMAL_PHONE_STEP_DEFINITIONS, ...NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ...PLUS_PAYPAL_STEP_DEFINITIONS, + ...PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS, ...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, ...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ...PLUS_GOPAY_STEP_DEFINITIONS, + ...PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS, ...PLUS_GOPAY_PHONE_STEP_DEFINITIONS, ...PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ...PLUS_GPC_STEP_DEFINITIONS, + ...PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS, ...PLUS_GPC_PHONE_STEP_DEFINITIONS, ...PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ]) { @@ -497,12 +564,15 @@ NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, PLUS_STEP_DEFINITIONS: PLUS_PAYPAL_STEP_DEFINITIONS, PLUS_PAYPAL_STEP_DEFINITIONS, + PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS, PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, PLUS_GOPAY_STEP_DEFINITIONS, + PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS, PLUS_GOPAY_PHONE_STEP_DEFINITIONS, PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, PLUS_GPC_STEP_DEFINITIONS, + PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS, PLUS_GPC_PHONE_STEP_DEFINITIONS, PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, SIGNUP_METHOD_EMAIL, diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js index e9c47db..67eb5dc 100644 --- a/shared/flow-capabilities.js +++ b/shared/flow-capabilities.js @@ -8,6 +8,8 @@ const DEFAULT_OPENAI_TARGET_ID = flowRegistryApi.DEFAULT_OPENAI_TARGET_ID || 'cpa'; const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_PHONE = 'phone'; + const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth'; + const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session'; const VALID_OPENAI_TARGET_IDS = Array.isArray(flowRegistryApi.OPENAI_TARGET_IDS) ? flowRegistryApi.OPENAI_TARGET_IDS.slice() : ['cpa', 'sub2api', 'codex2api']; @@ -50,6 +52,7 @@ const DEFAULT_TARGET_CAPABILITIES = Object.freeze({ supportsPhoneSignup: true, requiresPhoneSignupWarning: false, + supportedPlusAccountAccessStrategies: Object.freeze([PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]), }); const MODE_SWITCH_RELEVANT_KEYS = Object.freeze([ @@ -59,6 +62,7 @@ 'phoneVerificationEnabled', 'plusModeEnabled', 'signupMethod', + 'plusAccountAccessStrategy', 'openaiIntegrationTargetId', 'kiroTargetId', ]); @@ -67,14 +71,20 @@ cpa: Object.freeze({ supportsPhoneSignup: true, requiresPhoneSignupWarning: true, + supportedPlusAccountAccessStrategies: Object.freeze([PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]), }), sub2api: Object.freeze({ supportsPhoneSignup: true, requiresPhoneSignupWarning: false, + supportedPlusAccountAccessStrategies: Object.freeze([ + PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH, + PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION, + ]), }), codex2api: Object.freeze({ supportsPhoneSignup: true, requiresPhoneSignupWarning: false, + supportedPlusAccountAccessStrategies: Object.freeze([PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]), }), }); @@ -116,6 +126,12 @@ : SIGNUP_METHOD_EMAIL; } + function normalizePlusAccountAccessStrategy(value = '') { + return String(value || '').trim().toLowerCase() === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; + } + function normalizeOpenAiTargetList(values = []) { if (!Array.isArray(values)) { return []; @@ -336,6 +352,28 @@ : (effectiveSignupMethods.includes(SIGNUP_METHOD_EMAIL) ? SIGNUP_METHOD_EMAIL : effectiveSignupMethods[0]); + const requestedPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy( + options?.plusAccountAccessStrategy ?? state?.plusAccountAccessStrategy + ); + const availablePlusAccountAccessStrategies = activeFlowId === 'openai' + && Boolean(flowState.supportsPlusMode) + && Boolean(runtimeLocks.plusModeEnabled) + && effectiveSignupMethod === SIGNUP_METHOD_EMAIL + ? ( + Array.isArray(targetState.supportedPlusAccountAccessStrategies) + && targetState.supportedPlusAccountAccessStrategies.length > 0 + ? targetState.supportedPlusAccountAccessStrategies.slice() + : [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH] + ) + : [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]; + const effectivePlusAccountAccessStrategy = availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy) + ? requestedPlusAccountAccessStrategy + : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; + const canEditPlusAccountAccessStrategy = activeFlowId === 'openai' + && Boolean(flowState.supportsPlusMode) + && Boolean(runtimeLocks.plusModeEnabled) + && effectiveSignupMethod === SIGNUP_METHOD_EMAIL + && availablePlusAccountAccessStrategies.length > 1; const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function' && isRegisteredFlowId(activeFlowId) ? flowRegistryApi.getVisibleGroupIds(activeFlowId, effectiveTargetId) @@ -348,8 +386,10 @@ canShowPhoneSettings: activeFlowId === 'openai' && Boolean(flowState.supportsPhoneVerificationSettings), canShowPlusSettings: activeFlowId === 'openai' && Boolean(flowState.supportsPlusMode), canSwitchFlow: Boolean(flowState.canSwitchFlow), + canEditPlusAccountAccessStrategy, canUsePhoneSignup: canSelectPhoneSignup, canUseSelectedTarget: targetSupported, + effectivePlusAccountAccessStrategy, effectivePanelMode: effectiveTargetId, effectiveSignupMethod, effectiveSignupMethods, @@ -357,9 +397,11 @@ flowCapabilities: flowState, panelCapabilities: targetState, panelMode: effectiveTargetId, + requestedPlusAccountAccessStrategy, requestedSignupMethod, requestedTargetId, runtimeLocks, + availablePlusAccountAccessStrategies, shouldWarnCpaPhoneSignup: effectiveSignupMethod === SIGNUP_METHOD_PHONE && Boolean(targetState.requiresPhoneSignupWarning), stepDefinitionOptions: { @@ -367,6 +409,7 @@ integrationTargetId: effectiveTargetId, panelMode: effectiveTargetId, targetId: effectiveTargetId, + plusAccountAccessStrategy: effectivePlusAccountAccessStrategy, plusModeEnabled: runtimeLocks.plusModeEnabled, signupMethod: effectiveSignupMethod, }, @@ -556,6 +599,7 @@ getOpenAiTargetCapabilities, normalizeFlowId, normalizeOpenAiTargetId, + normalizePlusAccountAccessStrategy, normalizeSignupMethod, resolveSidepanelCapabilities, resolveSignupMethod, @@ -572,11 +616,14 @@ DEFAULT_OPENAI_TARGET_ID, FLOW_CAPABILITIES, OPENAI_TARGET_CAPABILITIES, + PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH, + PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION, SIGNUP_METHOD_EMAIL, SIGNUP_METHOD_PHONE, VALID_OPENAI_TARGET_IDS, normalizeFlowId, normalizeOpenAiTargetId, + normalizePlusAccountAccessStrategy, normalizeSignupMethod, }; }); diff --git a/shared/flow-registry.js b/shared/flow-registry.js index 49c7dc5..d917f7f 100644 --- a/shared/flow-registry.js +++ b/shared/flow-registry.js @@ -336,7 +336,7 @@ 'openai-plus': { id: 'openai-plus', label: 'Plus', - rowIds: ['row-plus-mode'], + rowIds: ['row-plus-mode', 'row-plus-payment-method', 'row-plus-account-access-strategy'], }, 'openai-phone': { id: 'openai-phone', diff --git a/shared/settings-schema.js b/shared/settings-schema.js index dd5a1bf..6e6dadf 100644 --- a/shared/settings-schema.js +++ b/shared/settings-schema.js @@ -47,6 +47,11 @@ const normalizeTargetId = typeof flowRegistry.normalizeTargetId === 'function' ? flowRegistry.normalizeTargetId : ((_flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase()); + const normalizePlusAccountAccessStrategy = (value = '') => ( + String(value || '').trim().toLowerCase() === 'sub2api_codex_session' + ? 'sub2api_codex_session' + : 'oauth' + ); function buildDefaultSettingsState() { return { @@ -96,6 +101,7 @@ plus: { plusModeEnabled: false, plusPaymentMethod: 'paypal', + plusAccountAccessStrategy: 'oauth', }, autoRun: { stepExecutionRange: { @@ -301,6 +307,11 @@ ?? nested?.flows?.openai?.plus?.plusPaymentMethod ?? defaults.flows.openai.plus.plusPaymentMethod ).trim() || defaults.flows.openai.plus.plusPaymentMethod, + plusAccountAccessStrategy: normalizePlusAccountAccessStrategy( + input?.plusAccountAccessStrategy + ?? nested?.flows?.openai?.plus?.plusAccountAccessStrategy + ?? defaults.flows.openai.plus.plusAccountAccessStrategy + ), }, autoRun: { stepExecutionRange: normalizeStepExecutionRangeEntry( @@ -439,6 +450,7 @@ next.phoneSignupReloginAfterBindEmailEnabled = openaiState.signup.phoneSignupReloginAfterBindEmailEnabled; next.plusModeEnabled = openaiState.plus.plusModeEnabled; next.plusPaymentMethod = openaiState.plus.plusPaymentMethod; + next.plusAccountAccessStrategy = openaiState.plus.plusAccountAccessStrategy; next.mailProvider = normalizedState.services.email.provider; next.ipProxyEnabled = normalizedState.services.proxy.enabled; next.ipProxyService = normalizedState.services.proxy.provider; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 161aa40..e0b453e 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -313,6 +313,16 @@ PayPal 订阅链路 +