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 订阅链路
+
PayPal 账号
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index a6b2919..710a933 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -192,6 +192,9 @@ const rowPlusPaymentMethod = document.getElementById('row-plus-payment-method');
const selectPlusPaymentMethod = document.getElementById('select-plus-payment-method');
const btnGpcCardKeyPurchase = document.getElementById('btn-gpc-card-key-purchase');
const plusPaymentMethodCaption = document.getElementById('plus-payment-method-caption');
+const rowPlusAccountAccessStrategy = document.getElementById('row-plus-account-access-strategy');
+const selectPlusAccountAccessStrategy = document.getElementById('select-plus-account-access-strategy');
+const plusAccountAccessStrategyCaption = document.getElementById('plus-account-access-strategy-caption');
const rowPayPalAccount = document.getElementById('row-paypal-account');
const selectPayPalAccount = document.getElementById('select-paypal-account');
const payPalAccountPickerRoot = document.getElementById('paypal-account-picker');
@@ -542,6 +545,9 @@ const GPC_HELPER_PORTAL_URL = 'https://gpc.qlhazycoder.top/';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
@@ -551,6 +557,7 @@ const PHONE_SIGNUP_REUSE_LOCK_TITLE = '手机号注册流程不使用号码复
let latestState = null;
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = DEFAULT_PLUS_PAYMENT_METHOD;
+let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = DEFAULT_SIGNUP_METHOD;
let currentPhoneSignupReloginAfterBindEmailEnabled = DEFAULT_PHONE_SIGNUP_RELOGIN_AFTER_BIND_EMAIL_ENABLED;
let currentStepDefinitionFlowId = DEFAULT_ACTIVE_FLOW_ID;
@@ -573,11 +580,13 @@ let nexSmsCountryMenuSearchKeyword = '';
const nexSmsCountrySearchTextById = new Map();
let stepDefinitions = getStepDefinitionsForMode(false, {
plusPaymentMethod: currentPlusPaymentMethod,
+ plusAccountAccessStrategy: currentPlusAccountAccessStrategy,
signupMethod: currentSignupMethod,
phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled,
});
let workflowNodes = getWorkflowNodesForMode(false, {
plusPaymentMethod: currentPlusPaymentMethod,
+ plusAccountAccessStrategy: currentPlusAccountAccessStrategy,
signupMethod: currentSignupMethod,
phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled,
});
@@ -838,9 +847,13 @@ function initPhoneVerificationSectionExpandedState() {
function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
+ const defaultStrategy = typeof DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY !== 'undefined' ? DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY : 'oauth';
const rawPaymentMethod = typeof options === 'string'
? options
: (options.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
+ const rawPlusAccountAccessStrategy = typeof options === 'string'
+ ? currentPlusAccountAccessStrategy
+ : (options.plusAccountAccessStrategy || currentPlusAccountAccessStrategy || defaultStrategy);
const rawSignupMethod = typeof options === 'string'
? currentSignupMethod
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
@@ -854,6 +867,7 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
activeFlowId: String(activeFlowId || '').trim().toLowerCase() || defaultFlowId,
plusModeEnabled,
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
+ plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(rawPlusAccountAccessStrategy),
signupMethod: normalizeSignupMethod(rawSignupMethod),
phoneSignupReloginAfterBindEmailEnabled,
}) || [])
@@ -868,9 +882,13 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
function getWorkflowNodesForMode(plusModeEnabled = false, options = {}) {
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
+ const defaultStrategy = typeof DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY !== 'undefined' ? DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY : 'oauth';
const rawPaymentMethod = typeof options === 'string'
? options
: (options.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
+ const rawPlusAccountAccessStrategy = typeof options === 'string'
+ ? currentPlusAccountAccessStrategy
+ : (options.plusAccountAccessStrategy || currentPlusAccountAccessStrategy || defaultStrategy);
const rawSignupMethod = typeof options === 'string'
? currentSignupMethod
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
@@ -884,6 +902,7 @@ function getWorkflowNodesForMode(plusModeEnabled = false, options = {}) {
activeFlowId: String(activeFlowId || '').trim().toLowerCase() || defaultFlowId,
plusModeEnabled,
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
+ plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(rawPlusAccountAccessStrategy),
signupMethod: normalizeSignupMethod(rawSignupMethod),
phoneSignupReloginAfterBindEmailEnabled,
});
@@ -940,9 +959,13 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
currentPlusModeEnabled = Boolean(plusModeEnabled);
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
+ const defaultStrategy = typeof DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY !== 'undefined' ? DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY : 'oauth';
const rawPaymentMethod = typeof options === 'string'
? options
: (options.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
+ const rawPlusAccountAccessStrategy = typeof options === 'string'
+ ? currentPlusAccountAccessStrategy
+ : (options.plusAccountAccessStrategy || currentPlusAccountAccessStrategy || defaultStrategy);
const rawSignupMethod = typeof options === 'string'
? currentSignupMethod
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
@@ -950,6 +973,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
? currentPhoneSignupReloginAfterBindEmailEnabled
: Boolean(options.phoneSignupReloginAfterBindEmailEnabled ?? currentPhoneSignupReloginAfterBindEmailEnabled);
currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
+ currentPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(rawPlusAccountAccessStrategy);
currentSignupMethod = normalizeSignupMethod(rawSignupMethod);
currentPhoneSignupReloginAfterBindEmailEnabled = phoneSignupReloginAfterBindEmailEnabled;
const nextActiveFlowId = String(
@@ -963,6 +987,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, {
activeFlowId: nextActiveFlowId,
plusPaymentMethod: currentPlusPaymentMethod,
+ plusAccountAccessStrategy: currentPlusAccountAccessStrategy,
signupMethod: currentSignupMethod,
phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled,
});
@@ -970,6 +995,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
? getWorkflowNodesForMode(currentPlusModeEnabled, {
activeFlowId: nextActiveFlowId,
plusPaymentMethod: currentPlusPaymentMethod,
+ plusAccountAccessStrategy: currentPlusAccountAccessStrategy,
signupMethod: currentSignupMethod,
phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled,
})
@@ -1899,6 +1925,19 @@ async function openConfirmModalWithOption({
async function openPlusManualConfirmationDialog(options = {}) {
const method = String(options.method || '').trim().toLowerCase();
+ const activeFlowId = String(latestState?.activeFlowId || latestState?.flowId || 'openai').trim().toLowerCase();
+ const panelMode = String(latestState?.panelMode || latestState?.openaiIntegrationTargetId || '').trim().toLowerCase();
+ const signupMethod = String(latestState?.resolvedSignupMethod || latestState?.signupMethod || 'email').trim().toLowerCase();
+ const plusModeEnabled = latestState?.plusModeEnabled === undefined ? true : Boolean(latestState.plusModeEnabled);
+ const plusAccountAccessStrategy = String(latestState?.plusAccountAccessStrategy || 'oauth').trim().toLowerCase();
+ const useSub2ApiSessionImport = plusModeEnabled
+ && activeFlowId === 'openai'
+ && panelMode === 'sub2api'
+ && signupMethod === 'email'
+ && plusAccountAccessStrategy === 'sub2api_codex_session';
+ const continuationActionLabel = useSub2ApiSessionImport
+ ? '导入当前 ChatGPT 会话到 SUB2API'
+ : 'OAuth 登录';
const title = String(options.title || '').trim() || (method === 'gopay' ? 'GoPay 订阅确认' : '手动确认');
const message = String(options.message || '').trim()
|| (method === 'gopay'
@@ -1912,7 +1951,7 @@ async function openPlusManualConfirmationDialog(options = {}) {
{ id: 'confirm', label: '我已完成订阅', variant: 'btn-primary' },
],
alert: method === 'gopay'
- ? { text: '确认后流程会直接继续到 Plus 模式第 10 步 OAuth 登录。', tone: 'info' }
+ ? { text: `确认后流程会直接继续到 Plus 模式后续的${continuationActionLabel}。`, tone: 'info' }
: null,
});
}
@@ -1926,6 +1965,19 @@ async function syncPlusManualConfirmationDialog() {
const step = Number(latestState?.plusManualConfirmationStep) || 0;
const method = String(latestState?.plusManualConfirmationMethod || '').trim().toLowerCase();
+ const activeFlowId = String(latestState?.activeFlowId || latestState?.flowId || 'openai').trim().toLowerCase();
+ const panelMode = String(latestState?.panelMode || latestState?.openaiIntegrationTargetId || '').trim().toLowerCase();
+ const signupMethod = String(latestState?.resolvedSignupMethod || latestState?.signupMethod || 'email').trim().toLowerCase();
+ const plusModeEnabled = latestState?.plusModeEnabled === undefined ? true : Boolean(latestState.plusModeEnabled);
+ const plusAccountAccessStrategy = String(latestState?.plusAccountAccessStrategy || 'oauth').trim().toLowerCase();
+ const useSub2ApiSessionImport = plusModeEnabled
+ && activeFlowId === 'openai'
+ && panelMode === 'sub2api'
+ && signupMethod === 'email'
+ && plusAccountAccessStrategy === 'sub2api_codex_session';
+ const continuationActionLabel = useSub2ApiSessionImport
+ ? '导入当前 ChatGPT 会话到 SUB2API'
+ : 'OAuth 登录';
const title = latestState?.plusManualConfirmationTitle;
const message = latestState?.plusManualConfirmationMessage;
activePlusManualConfirmationRequestId = requestId;
@@ -1963,7 +2015,7 @@ async function syncPlusManualConfirmationDialog() {
throw new Error(response.error);
}
if (confirmed) {
- showToast(method === 'gopay' ? 'GoPay 订阅已确认,正在继续 OAuth 登录...' : '已确认,流程继续执行中...', 'info', 2200);
+ showToast(method === 'gopay' ? `GoPay 订阅已确认,正在继续${continuationActionLabel}...` : '已确认,流程继续执行中...', 'info', 2200);
} else {
showToast(method === 'gopay' ? '已取消 GoPay 订阅等待。' : '已取消当前手动确认。', 'warn', 2200);
}
@@ -2799,6 +2851,13 @@ function normalizePlusPaymentMethod(value = '') {
return normalized === gopayValue ? gopayValue : paypalValue;
}
+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 getSelectedPlusPaymentMethod(state = latestState) {
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod?.value) {
@@ -2807,6 +2866,26 @@ function getSelectedPlusPaymentMethod(state = latestState) {
return normalizePlusPaymentMethod(state?.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
}
+function getRequestedPlusAccountAccessStrategy(state = latestState) {
+ const defaultStrategy = typeof DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY !== 'undefined'
+ ? DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY
+ : 'oauth';
+ const fallbackStrategy = normalizePlusAccountAccessStrategy(
+ (typeof selectPlusAccountAccessStrategy !== 'undefined' && selectPlusAccountAccessStrategy?.dataset?.requestedValue)
+ || state?.plusAccountAccessStrategy
+ || currentPlusAccountAccessStrategy
+ || defaultStrategy
+ );
+ if (
+ typeof selectPlusAccountAccessStrategy !== 'undefined'
+ && selectPlusAccountAccessStrategy
+ && !selectPlusAccountAccessStrategy.disabled
+ ) {
+ return normalizePlusAccountAccessStrategy(selectPlusAccountAccessStrategy.value || fallbackStrategy);
+ }
+ return fallbackStrategy;
+}
+
function normalizeGpcHelperPhoneModeValue(value = '') {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
@@ -4150,6 +4229,9 @@ function collectSettingsPayload() {
const rawPlusModeEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
? Boolean(inputPlusModeEnabled.checked)
: Boolean(latestState?.plusModeEnabled);
+ const requestedPlusAccountAccessStrategy = typeof getRequestedPlusAccountAccessStrategy === 'function'
+ ? getRequestedPlusAccountAccessStrategy(latestState)
+ : normalizePlusAccountAccessStrategy(latestState?.plusAccountAccessStrategy || DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY);
const rawPhoneVerificationEnabled = Boolean(inputPhoneVerificationEnabled?.checked);
const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
? resolveCurrentSidepanelCapabilities({
@@ -4164,6 +4246,7 @@ function collectSettingsPayload() {
? { panelMode: rawPanelMode }
: { kiroTargetId: selectedTargetId }),
plusModeEnabled: rawPlusModeEnabled,
+ plusAccountAccessStrategy: requestedPlusAccountAccessStrategy,
phoneVerificationEnabled: rawPhoneVerificationEnabled,
signupMethod: selectedSignupMethod,
},
@@ -4186,6 +4269,7 @@ function collectSettingsPayload() {
? { panelMode: rawPanelMode }
: { kiroTargetId: selectedTargetId }),
plusModeEnabled: rawPlusModeEnabled,
+ plusAccountAccessStrategy: requestedPlusAccountAccessStrategy,
phoneVerificationEnabled: rawPhoneVerificationEnabled,
signupMethod: selectedSignupMethod,
},
@@ -4326,6 +4410,7 @@ function collectSettingsPayload() {
codex2apiAdminKey: inputCodex2ApiAdminKey.value.trim(),
plusModeEnabled: effectivePlusModeEnabled,
plusPaymentMethod,
+ plusAccountAccessStrategy: requestedPlusAccountAccessStrategy,
paypalEmail: String(currentPayPalAccount?.email || latestState?.paypalEmail || '').trim(),
paypalPassword: String(currentPayPalAccount?.password || latestState?.paypalPassword || ''),
currentPayPalAccountId: String(latestState?.currentPayPalAccountId || '').trim(),
@@ -8505,6 +8590,10 @@ function resolveStepDefinitionCapabilityState(state = latestState, options = {})
plusModeEnabled: capabilityState
? Boolean(capabilityState.runtimeLocks?.plusModeEnabled)
: Boolean(nextState?.plusModeEnabled),
+ plusAccountAccessStrategy: capabilityState?.effectivePlusAccountAccessStrategy
+ || normalizePlusAccountAccessStrategy(
+ nextState?.plusAccountAccessStrategy || DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY
+ ),
signupMethod: capabilityState?.effectiveSignupMethod
|| normalizeSignupMethod((options?.signupMethod ?? nextState?.signupMethod) || DEFAULT_SIGNUP_METHOD),
};
@@ -8905,7 +8994,16 @@ function updatePlusModeUI() {
const paypalValue = typeof PLUS_PAYMENT_METHOD_PAYPAL !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL : 'paypal';
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
const gpcValue = typeof PLUS_PAYMENT_METHOD_GPC_HELPER !== 'undefined' ? PLUS_PAYMENT_METHOD_GPC_HELPER : 'gpc-helper';
+ const oauthStrategyValue = typeof PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH !== 'undefined'
+ ? PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH
+ : 'oauth';
+ const sub2apiSessionStrategyValue = typeof PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION !== 'undefined'
+ ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
+ : 'sub2api_codex_session';
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : paypalValue;
+ const requestedPlusAccountAccessStrategy = typeof getRequestedPlusAccountAccessStrategy === 'function'
+ ? getRequestedPlusAccountAccessStrategy(latestState)
+ : normalizePlusAccountAccessStrategy(latestState?.plusAccountAccessStrategy || DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY);
const rawEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
? Boolean(inputPlusModeEnabled.checked)
: false;
@@ -8915,6 +9013,7 @@ function updatePlusModeUI() {
state: {
...(latestState || {}),
plusModeEnabled: rawEnabled,
+ plusAccountAccessStrategy: requestedPlusAccountAccessStrategy,
},
})
: (() => {
@@ -8929,6 +9028,7 @@ function updatePlusModeUI() {
state: {
...(latestState || {}),
plusModeEnabled: rawEnabled,
+ plusAccountAccessStrategy: requestedPlusAccountAccessStrategy,
},
})
: null;
@@ -8937,6 +9037,10 @@ function updatePlusModeUI() {
? Boolean(capabilityState.canShowPlusSettings)
: true;
const enabled = supportsPlusMode && rawEnabled;
+ const canEditPlusAccountAccessStrategy = Boolean(capabilityState?.canEditPlusAccountAccessStrategy);
+ const effectivePlusAccountAccessStrategy = capabilityState?.effectivePlusAccountAccessStrategy
+ || requestedPlusAccountAccessStrategy
+ || oauthStrategyValue;
const method = enabled ? getSelectedPlusPaymentMethod() : defaultMethod;
const gpcPhoneMode = normalizeGpcHelperPhoneModeValue(
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
@@ -8990,6 +9094,33 @@ function updatePlusModeUI() {
}
row.style.display = enabled ? '' : 'none';
});
+ [
+ typeof rowPlusAccountAccessStrategy !== 'undefined' ? rowPlusAccountAccessStrategy : null,
+ ].forEach((row) => {
+ if (!row) {
+ return;
+ }
+ row.style.display = enabled ? '' : 'none';
+ });
+ if (typeof selectPlusAccountAccessStrategy !== 'undefined' && selectPlusAccountAccessStrategy) {
+ selectPlusAccountAccessStrategy.dataset.requestedValue = requestedPlusAccountAccessStrategy;
+ selectPlusAccountAccessStrategy.value = effectivePlusAccountAccessStrategy;
+ selectPlusAccountAccessStrategy.disabled = !enabled || !canEditPlusAccountAccessStrategy;
+ selectPlusAccountAccessStrategy.setAttribute('aria-disabled', String(selectPlusAccountAccessStrategy.disabled));
+ }
+ if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
+ if (!enabled) {
+ plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
+ } else if (!canEditPlusAccountAccessStrategy) {
+ plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
+ } else if (effectivePlusAccountAccessStrategy === sub2apiSessionStrategyValue) {
+ plusAccountAccessStrategyCaption.textContent = '复用当前 Plus 已登录会话,直接导入到 SUB2API';
+ } else if (effectivePlusAccountAccessStrategy === oauthStrategyValue) {
+ plusAccountAccessStrategyCaption.textContent = '通过 OAuth 回调创建 SUB2API 账号';
+ } else {
+ plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
+ }
+ }
[
typeof rowPayPalAccount !== 'undefined' ? rowPayPalAccount : null,
].forEach((row) => {
@@ -9362,6 +9493,19 @@ async function ensureGpcApiKeyReadyForStart(options = {}) {
async function openPlusManualConfirmationDialog(options = {}) {
const method = String(options.method || '').trim().toLowerCase();
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
+ const activeFlowId = String(latestState?.activeFlowId || latestState?.flowId || 'openai').trim().toLowerCase();
+ const panelMode = String(latestState?.panelMode || latestState?.openaiIntegrationTargetId || '').trim().toLowerCase();
+ const signupMethod = String(latestState?.resolvedSignupMethod || latestState?.signupMethod || 'email').trim().toLowerCase();
+ const plusModeEnabled = latestState?.plusModeEnabled === undefined ? true : Boolean(latestState.plusModeEnabled);
+ const plusAccountAccessStrategy = String(latestState?.plusAccountAccessStrategy || 'oauth').trim().toLowerCase();
+ const useSub2ApiSessionImport = plusModeEnabled
+ && activeFlowId === 'openai'
+ && panelMode === 'sub2api'
+ && signupMethod === 'email'
+ && plusAccountAccessStrategy === 'sub2api_codex_session';
+ const continuationActionLabel = useSub2ApiSessionImport
+ ? '导入当前 ChatGPT 会话到 SUB2API'
+ : 'OAuth 登录';
if (method === 'gopay-otp') {
if (!sharedFormDialog?.open) {
return null;
@@ -9405,7 +9549,7 @@ async function openPlusManualConfirmationDialog(options = {}) {
{ id: 'confirm', label: '我已完成订阅', variant: 'btn-primary' },
],
alert: method === gopayValue
- ? { text: '确认后流程会直接继续到 Plus 模式第 10 步 OAuth 登录。', tone: 'info' }
+ ? { text: `确认后流程会直接继续到 Plus 模式后续的${continuationActionLabel}。`, tone: 'info' }
: null,
});
}
@@ -9420,6 +9564,19 @@ async function syncPlusManualConfirmationDialog() {
const step = Number(latestState?.plusManualConfirmationStep) || 0;
const method = String(latestState?.plusManualConfirmationMethod || '').trim().toLowerCase();
+ const activeFlowId = String(latestState?.activeFlowId || latestState?.flowId || 'openai').trim().toLowerCase();
+ const panelMode = String(latestState?.panelMode || latestState?.openaiIntegrationTargetId || '').trim().toLowerCase();
+ const signupMethod = String(latestState?.resolvedSignupMethod || latestState?.signupMethod || 'email').trim().toLowerCase();
+ const plusModeEnabled = latestState?.plusModeEnabled === undefined ? true : Boolean(latestState.plusModeEnabled);
+ const plusAccountAccessStrategy = String(latestState?.plusAccountAccessStrategy || 'oauth').trim().toLowerCase();
+ const useSub2ApiSessionImport = plusModeEnabled
+ && activeFlowId === 'openai'
+ && panelMode === 'sub2api'
+ && signupMethod === 'email'
+ && plusAccountAccessStrategy === 'sub2api_codex_session';
+ const continuationActionLabel = useSub2ApiSessionImport
+ ? '导入当前 ChatGPT 会话到 SUB2API'
+ : 'OAuth 登录';
const title = latestState?.plusManualConfirmationTitle;
const message = latestState?.plusManualConfirmationMessage;
activePlusManualConfirmationRequestId = requestId;
@@ -9461,7 +9618,7 @@ async function syncPlusManualConfirmationDialog() {
showToast(
method === 'gopay-otp'
? 'GPC OTP 已提交,正在继续验证...'
- : (method === gopayValue ? 'GoPay 订阅已确认,正在继续 OAuth 登录...' : '已确认,流程继续执行中...'),
+ : (method === gopayValue ? `GoPay 订阅已确认,正在继续${continuationActionLabel}...` : '已确认,流程继续执行中...'),
'info',
2200
);
@@ -9776,6 +9933,7 @@ function renderStepsList() {
function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOrOptions = {}, maybeOptions = {}) {
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
+ const defaultStrategy = typeof DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY !== 'undefined' ? DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY : 'oauth';
const nextPlusModeEnabled = Boolean(plusModeEnabled);
const options = typeof plusPaymentMethodOrOptions === 'string'
? maybeOptions
@@ -9783,6 +9941,11 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
const rawPaymentMethod = typeof plusPaymentMethodOrOptions === 'string'
? plusPaymentMethodOrOptions
: (options.plusPaymentMethod || getSelectedPlusPaymentMethod(latestState));
+ const nextPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(
+ options.plusAccountAccessStrategy
+ || currentPlusAccountAccessStrategy
+ || defaultStrategy
+ );
const nextSignupMethod = normalizeSignupMethod(options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
const nextPhoneSignupReloginAfterBindEmailEnabled = Boolean(
options.phoneSignupReloginAfterBindEmailEnabled
@@ -9805,6 +9968,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
activeFlowId: nextActiveFlowId,
plusModeEnabled: nextPlusModeEnabled,
plusPaymentMethod: nextPaymentMethod,
+ plusAccountAccessStrategy: nextPlusAccountAccessStrategy,
signupMethod: nextSignupMethod,
phoneSignupReloginAfterBindEmailEnabled: nextPhoneSignupReloginAfterBindEmailEnabled,
});
@@ -9812,6 +9976,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
const shouldRender = Boolean(options.render)
|| nextPlusModeEnabled !== currentPlusModeEnabled
|| nextPaymentMethod !== currentPlusPaymentMethod
+ || nextPlusAccountAccessStrategy !== currentPlusAccountAccessStrategy
|| nextSignupMethod !== currentSignupMethod
|| nextPhoneSignupReloginAfterBindEmailEnabled !== currentPhoneSignupReloginAfterBindEmailEnabled
|| nextActiveFlowId !== currentFlowId
@@ -9823,6 +9988,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
rebuildStepDefinitionState(nextPlusModeEnabled, {
activeFlowId: nextActiveFlowId,
plusPaymentMethod: nextPaymentMethod,
+ plusAccountAccessStrategy: nextPlusAccountAccessStrategy,
signupMethod: nextSignupMethod,
phoneSignupReloginAfterBindEmailEnabled: nextPhoneSignupReloginAfterBindEmailEnabled,
});
@@ -9848,6 +10014,7 @@ function syncStepDefinitionsFromUiState(stateOverrides = {}) {
syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, {
activeFlowId: nextState?.activeFlowId || nextState?.flowId || DEFAULT_ACTIVE_FLOW_ID,
plusPaymentMethod: getSelectedPlusPaymentMethod(nextState),
+ plusAccountAccessStrategy: stepDefinitionState.plusAccountAccessStrategy,
signupMethod: stepDefinitionState.signupMethod,
phoneSignupReloginAfterBindEmailEnabled: Boolean(nextState?.phoneSignupReloginAfterBindEmailEnabled),
});
@@ -9949,6 +10116,13 @@ function applySettingsState(state) {
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(state?.plusPaymentMethod);
}
+ currentPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(
+ state?.plusAccountAccessStrategy || DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY
+ );
+ if (typeof selectPlusAccountAccessStrategy !== 'undefined' && selectPlusAccountAccessStrategy) {
+ selectPlusAccountAccessStrategy.dataset.requestedValue = currentPlusAccountAccessStrategy;
+ selectPlusAccountAccessStrategy.value = currentPlusAccountAccessStrategy;
+ }
if (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi) {
const defaultGpcHelperApiUrl = typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined'
? DEFAULT_GPC_HELPER_API_URL
@@ -14213,6 +14387,13 @@ selectPanelMode.addEventListener('change', async () => {
saveSettings({ silent: true }).catch(() => { });
});
+selectPlusAccountAccessStrategy?.addEventListener('change', () => {
+ selectPlusAccountAccessStrategy.value = normalizePlusAccountAccessStrategy(selectPlusAccountAccessStrategy.value);
+ updatePlusModeUI();
+ markSettingsDirty(true);
+ saveSettings({ silent: true }).catch(() => { });
+});
+
[inputKiroRsUrl, inputKiroRsKey].forEach((input) => {
input?.addEventListener('input', () => {
markSettingsDirty(true);
@@ -16157,6 +16338,13 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.plusPaymentMethod !== undefined && selectPlusPaymentMethod) {
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(message.payload.plusPaymentMethod);
}
+ if (message.payload.plusAccountAccessStrategy !== undefined && selectPlusAccountAccessStrategy) {
+ currentPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(message.payload.plusAccountAccessStrategy);
+ selectPlusAccountAccessStrategy.dataset.requestedValue = currentPlusAccountAccessStrategy;
+ if (!selectPlusAccountAccessStrategy.disabled) {
+ selectPlusAccountAccessStrategy.value = currentPlusAccountAccessStrategy;
+ }
+ }
if (message.payload.gopayHelperPhoneMode !== undefined && selectGpcHelperPhoneMode) {
selectGpcHelperPhoneMode.value = normalizeGpcHelperPhoneModeValue(message.payload.gopayHelperPhoneMode);
}
@@ -16181,6 +16369,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (
message.payload.plusModeEnabled !== undefined
|| message.payload.plusPaymentMethod !== undefined
+ || message.payload.plusAccountAccessStrategy !== undefined
|| message.payload.gopayHelperPhoneMode !== undefined
|| message.payload.gopayHelperAutoModeEnabled !== undefined
|| message.payload.gopayHelperOtpChannel !== undefined
diff --git a/tests/auto-run-step6-restart.test.js b/tests/auto-run-step6-restart.test.js
index 88dca20..c39f906 100644
--- a/tests/auto-run-step6-restart.test.js
+++ b/tests/auto-run-step6-restart.test.js
@@ -173,8 +173,8 @@ const defaultStepDefinitions = {
4: { key: 'verify-email' },
5: { key: 'profile-basic' },
6: { key: 'profile-finish' },
- 7: { key: 'auth-login' },
- 8: { key: 'auth-email-code' },
+ 7: { key: 'oauth-login' },
+ 8: { key: 'fetch-login-code' },
9: { key: 'confirm-oauth' },
10: { key: 'platform-verify' },
};
@@ -407,7 +407,7 @@ test('auto-run keeps restarting from step 7 after post-login failures without a
7, 8, 9, 10,
]
);
- assert.ok(events.logs.some(({ message }) => /回到节点 auth-login 重新开始授权流程/.test(message)));
+ assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
});
test('auto-run restarts the current step after five minutes without new logs', async () => {
@@ -964,3 +964,42 @@ test('auto-run does not restart GPC checkout when account already has a ChatGPT
assert.equal(result.events.invalidations.length, 0);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
});
+
+test('auto-run does not reroute SUB2API session import failures into OAuth restarts', async () => {
+ const plusSessionSteps = {
+ 6: { key: 'plus-checkout-create' },
+ 7: { key: 'plus-checkout-billing' },
+ 8: { key: 'paypal-approve' },
+ 9: { key: 'plus-checkout-return' },
+ 10: { key: 'sub2api-session-import' },
+ };
+ const harness = createHarness({
+ startStep: 10,
+ failureStep: 10,
+ failureBudget: 1,
+ failureMessage: '步骤 10:当前页面未读取到有效的 ChatGPT session。',
+ stepDefinitions: plusSessionSteps,
+ finalOAuthChainStartStep: 10,
+ customState: {
+ stepStatuses: {
+ 3: 'completed',
+ 6: 'completed',
+ 7: 'completed',
+ 8: 'completed',
+ 9: 'completed',
+ },
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ panelMode: 'sub2api',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ },
+ });
+
+ const result = await harness.runAndCaptureError();
+
+ assert.ok(result?.error);
+ assert.match(result.error.message, /未读取到有效的 ChatGPT session/);
+ assert.deepStrictEqual(result.events.steps, [10]);
+ assert.equal(result.events.invalidations.length, 0);
+ assert.ok(!result.events.logs.some(({ message }) => /回到节点 oauth-login|回到节点 confirm-oauth|重新开始授权流程/.test(message)));
+});
diff --git a/tests/background-account-history-settings.test.js b/tests/background-account-history-settings.test.js
index 425efd4..dcb40ab 100644
--- a/tests/background-account-history-settings.test.js
+++ b/tests/background-account-history-settings.test.js
@@ -227,6 +227,8 @@ return {
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gpc-helper'), 'gpc-helper');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal'), 'paypal');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'unknown'), 'paypal');
+ assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'sub2api_codex_session'), 'sub2api_codex_session');
+ assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'unknown'), 'oauth');
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/checkout/start '),
'https://gpc.qlhazycoder.top'
diff --git a/tests/background-auth-chain-guard.test.js b/tests/background-auth-chain-guard.test.js
index 31a9ee6..0aecc9e 100644
--- a/tests/background-auth-chain-guard.test.js
+++ b/tests/background-auth-chain-guard.test.js
@@ -48,6 +48,15 @@ function extractFunction(name) {
return source.slice(start, end);
}
+test('background auth chain set does not include SUB2API session import node', () => {
+ const authChainStart = source.indexOf('const AUTH_CHAIN_NODE_IDS = new Set([');
+ const authChainEnd = source.indexOf(']);', authChainStart);
+ const authChainBlock = source.slice(authChainStart, authChainEnd);
+
+ assert.ok(authChainStart >= 0, 'expected AUTH_CHAIN_NODE_IDS block to exist');
+ assert.doesNotMatch(authChainBlock, /sub2api-session-import/);
+});
+
const NODE_EXECUTE_COMPAT_HELPERS = `
const AUTH_CHAIN_NODE_IDS = new Set(['oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify']);
const STEP_NODE_IDS = {
diff --git a/tests/background-effective-plus-account-access-strategy.test.js b/tests/background-effective-plus-account-access-strategy.test.js
new file mode 100644
index 0000000..b841976
--- /dev/null
+++ b/tests/background-effective-plus-account-access-strategy.test.js
@@ -0,0 +1,185 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('background.js', 'utf8');
+
+function extractFunction(name) {
+ const markers = [`async function ${name}(`, `function ${name}(`];
+ const start = markers
+ .map((marker) => source.indexOf(marker))
+ .find((index) => index >= 0);
+ if (start < 0) {
+ throw new Error(`missing function ${name}`);
+ }
+
+ let parenDepth = 0;
+ let signatureEnded = false;
+ let braceStart = -1;
+ for (let i = start; i < source.length; i += 1) {
+ const ch = source[i];
+ if (ch === '(') {
+ parenDepth += 1;
+ } else if (ch === ')') {
+ parenDepth -= 1;
+ if (parenDepth === 0) {
+ signatureEnded = true;
+ }
+ } else if (ch === '{' && signatureEnded) {
+ braceStart = i;
+ break;
+ }
+ }
+
+ if (braceStart < 0) {
+ throw new Error(`missing body for function ${name}`);
+ }
+
+ let depth = 0;
+ let end = braceStart;
+ for (; end < source.length; end += 1) {
+ const ch = source[end];
+ if (ch === '{') depth += 1;
+ if (ch === '}') {
+ depth -= 1;
+ if (depth === 0) {
+ end += 1;
+ break;
+ }
+ }
+ }
+
+ return source.slice(start, end);
+}
+
+function createHarness() {
+ return new Function(`
+const DEFAULT_ACTIVE_FLOW_ID = 'openai';
+const workflowEngine = null;
+const self = {
+ MultiPageStepDefinitions: {
+ getSteps(options = {}) {
+ return String(options.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
+ ? [
+ { id: 1, key: 'open-chatgpt' },
+ { id: 2, key: 'plus-checkout-create' },
+ { id: 3, key: 'plus-checkout-billing' },
+ { id: 4, key: 'paypal-approve' },
+ { id: 5, key: 'plus-checkout-return' },
+ { id: 6, key: 'sub2api-session-import' },
+ ]
+ : [
+ { id: 1, key: 'open-chatgpt' },
+ { id: 2, key: 'plus-checkout-create' },
+ { id: 3, key: 'plus-checkout-billing' },
+ { id: 4, key: 'paypal-approve' },
+ { id: 5, key: 'plus-checkout-return' },
+ { id: 6, key: 'oauth-login' },
+ { id: 7, key: 'fetch-login-code' },
+ { id: 8, key: 'confirm-oauth' },
+ { id: 9, key: 'platform-verify' },
+ ];
+ },
+ getNodes(options = {}) {
+ return this.getSteps(options).map((definition) => ({
+ nodeId: String(definition.key || '').trim(),
+ displayOrder: Number(definition.id) || 0,
+ }));
+ },
+ },
+};
+function normalizeSignupMethod(value = '') {
+ return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
+}
+function normalizePlusPaymentMethod(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
+}
+function normalizePlusAccountAccessStrategy(value = '') {
+ return String(value || '').trim().toLowerCase() === 'sub2api_codex_session'
+ ? 'sub2api_codex_session'
+ : 'oauth';
+}
+function isPlusModeState(state = {}) {
+ return Boolean(state?.plusModeEnabled);
+}
+function resolveCurrentFlowCapabilities(state = {}, options = {}) {
+ const effectiveStrategy = String(options.panelMode || '').trim().toLowerCase() === 'sub2api'
+ ? normalizePlusAccountAccessStrategy(state.plusAccountAccessStrategy)
+ : 'oauth';
+ return {
+ effectivePanelMode: options.panelMode,
+ effectivePlusAccountAccessStrategy: effectiveStrategy,
+ effectiveSignupMethod: 'email',
+ stepDefinitionOptions: {
+ activeFlowId: 'openai',
+ panelMode: options.panelMode,
+ plusModeEnabled: Boolean(state.plusModeEnabled),
+ plusPaymentMethod: normalizePlusPaymentMethod(state.plusPaymentMethod),
+ plusAccountAccessStrategy: effectiveStrategy,
+ signupMethod: 'email',
+ },
+ };
+}
+${extractFunction('getSignupMethodForStepDefinitions')}
+${extractFunction('buildResolvedStepDefinitionState')}
+${extractFunction('getStepDefinitionsForState')}
+${extractFunction('getNodeDefinitionsForState')}
+${extractFunction('getNodeIdsForState')}
+return {
+ buildResolvedStepDefinitionState,
+ getNodeIdsForState,
+};
+`)();
+}
+
+test('background step resolution keeps SUB2API session tail only when the effective Plus target supports it', () => {
+ const api = createHarness();
+ const state = {
+ activeFlowId: 'openai',
+ flowId: 'openai',
+ panelMode: 'sub2api',
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ signupMethod: 'email',
+ };
+
+ const resolvedState = api.buildResolvedStepDefinitionState(state);
+ const nodeIds = api.getNodeIdsForState(state);
+
+ assert.equal(resolvedState.plusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.deepStrictEqual(nodeIds, [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'paypal-approve',
+ 'plus-checkout-return',
+ 'sub2api-session-import',
+ ]);
+});
+
+test('background step resolution falls back to OAuth tail when the requested session strategy is not effective for the current panel mode', () => {
+ const api = createHarness();
+ const state = {
+ activeFlowId: 'openai',
+ flowId: 'openai',
+ panelMode: 'cpa',
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ signupMethod: 'email',
+ };
+
+ const resolvedState = api.buildResolvedStepDefinitionState(state);
+ const nodeIds = api.getNodeIdsForState(state);
+
+ assert.equal(resolvedState.plusAccountAccessStrategy, 'oauth');
+ assert.equal(nodeIds.includes('sub2api-session-import'), false);
+ assert.deepStrictEqual(nodeIds.slice(-4), [
+ 'oauth-login',
+ 'fetch-login-code',
+ 'confirm-oauth',
+ 'platform-verify',
+ ]);
+});
diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js
index da226af..cc0908e 100644
--- a/tests/background-message-router-module.test.js
+++ b/tests/background-message-router-module.test.js
@@ -297,6 +297,299 @@ test('SAVE_SETTING broadcasts operation delay setting without background success
assert.equal(logs.length, 0);
});
+test('SAVE_SETTING rebuilds Plus node statuses when the account access strategy changes', async () => {
+ const source = fs.readFileSync('background/message-router.js', 'utf8');
+ const globalScope = { console };
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
+ const broadcasts = [];
+ let state = {
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'oauth',
+ oauthUrl: 'https://oauth.example/current',
+ localhostUrl: 'http://localhost:38080/callback',
+ oauthFlowDeadlineAt: Date.now() + 60000,
+ oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
+ cpaOAuthState: 'cpa-state',
+ cpaManagementOrigin: 'https://cpa.example.com',
+ sub2apiSessionId: 'sub-session',
+ sub2apiOAuthState: 'sub-oauth-state',
+ sub2apiGroupId: 'group-id',
+ sub2apiGroupIds: ['group-id'],
+ sub2apiDraftName: 'draft-name',
+ sub2apiProxyId: 'proxy-id',
+ codex2apiSessionId: 'codex-session',
+ codex2apiOAuthState: 'codex-oauth-state',
+ plusManualConfirmationPending: true,
+ plusManualConfirmationRequestId: 'gopay-req',
+ plusManualConfirmationStep: 9,
+ plusManualConfirmationMethod: 'gopay',
+ plusManualConfirmationTitle: 'GoPay 订阅确认',
+ plusManualConfirmationMessage: '完成后继续 OAuth 登录。',
+ currentNodeId: 'confirm-oauth',
+ nodeStatuses: {
+ 'open-chatgpt': 'completed',
+ 'plus-checkout-create': 'completed',
+ 'plus-checkout-billing': 'completed',
+ 'paypal-approve': 'completed',
+ 'plus-checkout-return': 'completed',
+ 'oauth-login': 'completed',
+ 'fetch-login-code': 'completed',
+ 'post-login-phone-verification': 'completed',
+ 'confirm-oauth': 'running',
+ 'platform-verify': 'pending',
+ },
+ };
+
+ const router = api.createMessageRouter({
+ addLog: async () => {},
+ buildLuckmailSessionSettingsPayload: () => ({}),
+ buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'plusAccountAccessStrategy')
+ ? { plusAccountAccessStrategy: input.plusAccountAccessStrategy }
+ : {},
+ broadcastDataUpdate: (payload) => broadcasts.push(payload),
+ getNodeIdsForState: (nextState = {}) => (
+ String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
+ ? [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'paypal-approve',
+ 'plus-checkout-return',
+ 'sub2api-session-import',
+ ]
+ : [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'paypal-approve',
+ 'plus-checkout-return',
+ 'oauth-login',
+ 'fetch-login-code',
+ 'post-login-phone-verification',
+ 'confirm-oauth',
+ 'platform-verify',
+ ]
+ ),
+ getState: async () => ({ ...state }),
+ getStepIdsForState: () => [],
+ setPersistentSettings: async (updates) => ({ ...updates }),
+ setState: async (updates) => {
+ state = { ...state, ...updates };
+ },
+ });
+
+ const response = await router.handleMessage({
+ type: 'SAVE_SETTING',
+ payload: {
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ },
+ });
+
+ assert.equal(response.ok, true);
+ assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.equal(state.currentNodeId, '');
+ assert.equal(state.oauthUrl, null);
+ assert.equal(state.localhostUrl, null);
+ assert.equal(state.oauthFlowDeadlineAt, null);
+ assert.equal(state.oauthFlowDeadlineSourceUrl, null);
+ assert.equal(state.cpaOAuthState, null);
+ assert.equal(state.cpaManagementOrigin, null);
+ assert.equal(state.sub2apiSessionId, null);
+ assert.equal(state.sub2apiOAuthState, null);
+ assert.equal(state.sub2apiGroupId, null);
+ assert.deepStrictEqual(state.sub2apiGroupIds, []);
+ assert.equal(state.sub2apiDraftName, null);
+ assert.equal(state.sub2apiProxyId, null);
+ assert.equal(state.codex2apiSessionId, null);
+ assert.equal(state.codex2apiOAuthState, null);
+ assert.equal(state.plusManualConfirmationPending, false);
+ assert.equal(state.plusManualConfirmationRequestId, '');
+ assert.equal(state.plusManualConfirmationStep, 0);
+ assert.equal(state.plusManualConfirmationMethod, '');
+ assert.equal(state.plusManualConfirmationTitle, '');
+ assert.equal(state.plusManualConfirmationMessage, '');
+ assert.deepStrictEqual(state.nodeStatuses, {
+ 'open-chatgpt': 'pending',
+ 'plus-checkout-create': 'pending',
+ 'plus-checkout-billing': 'pending',
+ 'paypal-approve': 'pending',
+ 'plus-checkout-return': 'pending',
+ 'sub2api-session-import': 'pending',
+ });
+ assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'oauth-login'), false);
+ assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'platform-verify'), false);
+ assert.deepStrictEqual(broadcasts.at(-1), {
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ 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: '',
+ nodeStatuses: {
+ 'open-chatgpt': 'pending',
+ 'plus-checkout-create': 'pending',
+ 'plus-checkout-billing': 'pending',
+ 'paypal-approve': 'pending',
+ 'plus-checkout-return': 'pending',
+ 'sub2api-session-import': 'pending',
+ },
+ currentNodeId: '',
+ });
+});
+
+test('SAVE_SETTING rebuilds Plus node statuses when panel mode forces the effective strategy back to OAuth', async () => {
+ const source = fs.readFileSync('background/message-router.js', 'utf8');
+ const globalScope = { console };
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
+ const broadcasts = [];
+ let state = {
+ panelMode: 'sub2api',
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ oauthUrl: 'https://oauth.example/current',
+ localhostUrl: 'http://localhost:38080/callback',
+ sub2apiSessionId: 'sub-session',
+ plusManualConfirmationPending: true,
+ plusManualConfirmationRequestId: 'gopay-req',
+ plusManualConfirmationStep: 9,
+ plusManualConfirmationMethod: 'gopay',
+ plusManualConfirmationTitle: 'GoPay 订阅确认',
+ plusManualConfirmationMessage: '完成后继续导入当前 ChatGPT 会话到 SUB2API。',
+ currentNodeId: 'sub2api-session-import',
+ nodeStatuses: {
+ 'open-chatgpt': 'completed',
+ 'plus-checkout-create': 'completed',
+ 'plus-checkout-billing': 'completed',
+ 'paypal-approve': 'completed',
+ 'plus-checkout-return': 'completed',
+ 'sub2api-session-import': 'running',
+ },
+ };
+
+ const router = api.createMessageRouter({
+ addLog: async () => {},
+ buildLuckmailSessionSettingsPayload: () => ({}),
+ buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'panelMode')
+ ? { panelMode: input.panelMode }
+ : {},
+ broadcastDataUpdate: (payload) => broadcasts.push(payload),
+ getNodeIdsForState: (nextState = {}) => (
+ String(nextState.panelMode || '').trim() === 'sub2api'
+ && String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
+ ? [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'paypal-approve',
+ 'plus-checkout-return',
+ 'sub2api-session-import',
+ ]
+ : [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'paypal-approve',
+ 'plus-checkout-return',
+ 'oauth-login',
+ 'fetch-login-code',
+ 'post-login-phone-verification',
+ 'confirm-oauth',
+ 'platform-verify',
+ ]
+ ),
+ getState: async () => ({ ...state }),
+ getStepIdsForState: () => [],
+ setPersistentSettings: async (updates) => ({ ...updates }),
+ setState: async (updates) => {
+ state = { ...state, ...updates };
+ },
+ });
+
+ const response = await router.handleMessage({
+ type: 'SAVE_SETTING',
+ payload: {
+ panelMode: 'cpa',
+ },
+ });
+
+ assert.equal(response.ok, true);
+ assert.equal(state.panelMode, 'cpa');
+ assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.equal(state.currentNodeId, '');
+ assert.equal(state.oauthUrl, null);
+ assert.equal(state.localhostUrl, null);
+ assert.equal(state.sub2apiSessionId, null);
+ assert.equal(state.plusManualConfirmationPending, false);
+ assert.equal(state.plusManualConfirmationMessage, '');
+ assert.deepStrictEqual(state.nodeStatuses, {
+ 'open-chatgpt': 'pending',
+ 'plus-checkout-create': 'pending',
+ 'plus-checkout-billing': 'pending',
+ 'paypal-approve': 'pending',
+ 'plus-checkout-return': 'pending',
+ 'oauth-login': 'pending',
+ 'fetch-login-code': 'pending',
+ 'post-login-phone-verification': 'pending',
+ 'confirm-oauth': 'pending',
+ 'platform-verify': 'pending',
+ });
+ assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'sub2api-session-import'), false);
+ assert.deepStrictEqual(broadcasts.at(-1), {
+ panelMode: 'cpa',
+ signupMethod: 'email',
+ 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: '',
+ nodeStatuses: {
+ 'open-chatgpt': 'pending',
+ 'plus-checkout-create': 'pending',
+ 'plus-checkout-billing': 'pending',
+ 'paypal-approve': 'pending',
+ 'plus-checkout-return': 'pending',
+ 'oauth-login': 'pending',
+ 'fetch-login-code': 'pending',
+ 'post-login-phone-verification': 'pending',
+ 'confirm-oauth': 'pending',
+ 'platform-verify': 'pending',
+ },
+ currentNodeId: '',
+ });
+});
+
test('SAVE_SETTING mirrors activeFlowId into flowId when switching to kiro flow', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
diff --git a/tests/background-message-router-plus-final-step.test.js b/tests/background-message-router-plus-final-step.test.js
index cb6e645..4587aa6 100644
--- a/tests/background-message-router-plus-final-step.test.js
+++ b/tests/background-message-router-plus-final-step.test.js
@@ -6,8 +6,25 @@ const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
-test('message router appends success record on Plus final step instead of hard-coded step 10', async () => {
+function createRouterWithFinalNode(options = {}) {
+ const finalNodeId = String(options.finalNodeId || 'platform-verify').trim();
+ const nodeIds = Array.isArray(options.nodeIds) ? options.nodeIds.slice() : [
+ 'open-chatgpt',
+ 'oauth-login',
+ 'fetch-login-code',
+ 'confirm-oauth',
+ finalNodeId,
+ ];
+ const nodeStepMap = {
+ 'oauth-login': 10,
+ 'fetch-login-code': 11,
+ 'confirm-oauth': 12,
+ 'platform-verify': 13,
+ 'sub2api-session-import': 10,
+ ...(options.nodeStepMap || {}),
+ };
const appendCalls = [];
+
const router = api.createMessageRouter({
addLog: async () => {},
appendAccountRunRecord: async (...args) => {
@@ -43,12 +60,15 @@ test('message router appends success record on Plus final step instead of hard-c
getCurrentLuckmailPurchase: () => null,
getPendingAutoRunTimerPlan: () => null,
getSourceLabel: () => '',
- getState: async () => ({ plusModeEnabled: true, nodeStatuses: { 'platform-verify': 'pending' } }),
- getNodeIdsForState: () => ['open-chatgpt', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify'],
- getStepIdByNodeIdForState: (nodeId) => ({ 'oauth-login': 10, 'fetch-login-code': 11, 'confirm-oauth': 12, 'platform-verify': 13 }[nodeId] || 0),
- getLastStepIdForState: () => 13,
- getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'oauth-login' : 'platform-verify' }),
- getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
+ getState: async () => ({ plusModeEnabled: true, nodeStatuses: { [finalNodeId]: 'pending' } }),
+ getNodeIdsForState: () => nodeIds.slice(),
+ getStepIdByNodeIdForState: (nodeId) => nodeStepMap[nodeId] || 0,
+ getLastStepIdForState: () => Math.max(...Object.values(nodeStepMap)),
+ getStepDefinitionForState: (step) => ({
+ id: step,
+ key: Object.entries(nodeStepMap).find(([, mappedStep]) => mappedStep === step)?.[0] || finalNodeId,
+ }),
+ getStepIdsForState: () => Object.values(nodeStepMap),
getTabId: async () => null,
getStopRequested: () => false,
handleAutoRunLoopUnhandledError: async () => {},
@@ -100,8 +120,56 @@ test('message router appends success record on Plus final step instead of hard-c
verifyHotmailAccount: async () => {},
});
+ return {
+ appendCalls,
+ router,
+ };
+}
+
+test('message router appends success record on Plus final step instead of hard-coded step 10', async () => {
+ const { appendCalls, router } = createRouterWithFinalNode({
+ finalNodeId: 'platform-verify',
+ nodeIds: ['open-chatgpt', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify'],
+ nodeStepMap: {
+ 'oauth-login': 10,
+ 'fetch-login-code': 11,
+ 'confirm-oauth': 12,
+ 'platform-verify': 13,
+ },
+ });
+
await router.handleMessage({ type: 'NODE_COMPLETE', nodeId: 'platform-verify', payload: { nodeId: 'platform-verify' } }, {});
assert.equal(appendCalls.length, 1);
assert.equal(appendCalls[0][0], 'success');
});
+
+test('message router appends success record when SUB2API session import is the final Plus node', async () => {
+ const { appendCalls, router } = createRouterWithFinalNode({
+ finalNodeId: 'sub2api-session-import',
+ nodeIds: [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'paypal-approve',
+ 'plus-checkout-return',
+ 'sub2api-session-import',
+ ],
+ nodeStepMap: {
+ 'plus-checkout-create': 6,
+ 'plus-checkout-billing': 7,
+ 'paypal-approve': 8,
+ 'plus-checkout-return': 9,
+ 'sub2api-session-import': 10,
+ },
+ });
+
+ await router.handleMessage({
+ type: 'NODE_COMPLETE',
+ nodeId: 'sub2api-session-import',
+ payload: { nodeId: 'sub2api-session-import' },
+ }, {});
+
+ assert.equal(appendCalls.length, 1);
+ assert.equal(appendCalls[0][0], 'success');
+});
diff --git a/tests/background-settings-schema-persistence.test.js b/tests/background-settings-schema-persistence.test.js
index daf5b37..4a4bee5 100644
--- a/tests/background-settings-schema-persistence.test.js
+++ b/tests/background-settings-schema-persistence.test.js
@@ -80,6 +80,7 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'phoneSignupReloginAfterBindEmailEnabled',
'plusModeEnabled',
'plusPaymentMethod',
+ 'plusAccountAccessStrategy',
'mailProvider',
'ipProxyEnabled',
'ipProxyService',
@@ -95,6 +96,7 @@ const PERSISTED_SETTING_DEFAULTS = {
signupMethod: 'email',
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'oauth',
phoneVerificationEnabled: false,
mailProvider: '163',
ipProxyEnabled: false,
@@ -249,6 +251,7 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'oauth',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
@@ -276,6 +279,7 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown
assert.equal(payload.kiroRsKey, 'schema-only-key');
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
assert.equal(payload.settingsSchemaVersion, 4);
+ assert.equal(payload.settingsState.flows.openai.plus.plusAccountAccessStrategy, 'oauth');
});
test('getPersistedSettings reads schema keys alongside legacy flat settings keys', async () => {
@@ -300,6 +304,7 @@ function getRequestedKeys() {
assert.ok(api.getRequestedKeys().includes('settingsSchemaVersion'));
assert.ok(api.getRequestedKeys().includes('settingsState'));
+ assert.ok(api.getRequestedKeys().includes('plusAccountAccessStrategy'));
assert.equal(state.settingsSchemaVersion, 4);
assert.equal(state.settingsState.activeFlowId, 'openai');
});
@@ -350,6 +355,7 @@ const chrome = {
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
@@ -384,6 +390,7 @@ const chrome = {
assert.equal(state.ipProxyEnabled, true);
assert.equal(state.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(state.kiroRsKey, 'stored-key');
+ assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(Object.prototype.hasOwnProperty.call(state, 'kiroRegion'), false);
assert.deepEqual(state.stepExecutionRangeByFlow.kiro, {
enabled: true,
@@ -459,6 +466,7 @@ function getRemovedKeys() {
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
@@ -486,6 +494,7 @@ function getRemovedKeys() {
assert.equal(persisted.kiroTargetId, 'kiro-rs');
assert.equal(persisted.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(persisted.kiroRsKey, 'nested-only-key');
+ assert.equal(persisted.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(Object.prototype.hasOwnProperty.call(persisted, 'kiroRegion'), false);
assert.equal(persisted.settingsSchemaVersion, 4);
assert.equal(Object.prototype.hasOwnProperty.call(write, 'activeFlowId'), false);
@@ -494,6 +503,7 @@ function getRemovedKeys() {
assert.equal(Object.prototype.hasOwnProperty.call(write, 'kiroRegion'), false);
assert.equal(write.settingsSchemaVersion, 4);
assert.equal(write.settingsState.activeFlowId, 'kiro');
+ assert.equal(write.settingsState.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(write.settingsState.flows.kiro.targetId, 'kiro-rs');
assert.ok(api.getRemovedKeys().includes('panelMode'));
assert.ok(api.getRemovedKeys().includes('kiroRsUrl'));
diff --git a/tests/background-sub2api-session-import.test.js b/tests/background-sub2api-session-import.test.js
new file mode 100644
index 0000000..82ef552
--- /dev/null
+++ b/tests/background-sub2api-session-import.test.js
@@ -0,0 +1,289 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const test = require('node:test');
+
+function createJsonResponse(payload, status = 200) {
+ return {
+ ok: status >= 200 && status < 300,
+ status,
+ text: async () => JSON.stringify(payload),
+ };
+}
+
+function loadSub2ApiApiModule() {
+ const source = fs.readFileSync('background/sub2api-api.js', 'utf8');
+ return new Function('self', `${source}; return self.MultiPageBackgroundSub2ApiApi;`)({});
+}
+
+function loadSub2ApiSessionImportModule() {
+ const source = fs.readFileSync('background/steps/sub2api-session-import.js', 'utf8');
+ return new Function('self', `${source}; return self.MultiPageBackgroundSub2ApiSessionImport;`)({});
+}
+
+test('sub2api api imports current ChatGPT session through codex-session endpoint', async () => {
+ const apiModule = loadSub2ApiApiModule();
+ const fetchCalls = [];
+ const logs = [];
+ const importExpiresAt = Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000);
+
+ const api = apiModule.createSub2ApiApi({
+ addLog: async (message, level = 'info', options = {}) => {
+ logs.push({ message, level, step: options.step, stepKey: options.stepKey });
+ },
+ normalizeSub2ApiUrl: (value) => value,
+ DEFAULT_SUB2API_GROUP_NAME: 'codex',
+ fetchImpl: async (url, options = {}) => {
+ const parsed = new URL(url);
+ const body = options.body ? JSON.parse(options.body) : null;
+ fetchCalls.push({ path: parsed.pathname, search: parsed.search, method: options.method || 'GET', body });
+
+ if (parsed.pathname === '/api/v1/auth/login') {
+ return createJsonResponse({
+ code: 0,
+ data: {
+ access_token: 'admin-token',
+ },
+ });
+ }
+ if (parsed.pathname === '/api/v1/admin/groups/all') {
+ return createJsonResponse({
+ code: 0,
+ data: [
+ { id: 5, name: 'codex', platform: 'openai' },
+ ],
+ });
+ }
+ if (parsed.pathname === '/api/v1/admin/proxies/all') {
+ return createJsonResponse({
+ code: 0,
+ data: [{
+ id: 7,
+ name: 'shadowrocket',
+ protocol: 'socks5',
+ host: '127.0.0.1',
+ port: 1080,
+ status: 'active',
+ }],
+ });
+ }
+ if (parsed.pathname === '/api/v1/admin/accounts/import/codex-session') {
+ const parsedContent = JSON.parse(body.content);
+ assert.equal(parsedContent.accessToken, 'access-token-from-state');
+ assert.equal(parsedContent.user?.email, 'flow@example.com');
+ assert.equal(body.priority, 3);
+ assert.equal(body.proxy_id, 7);
+ assert.deepStrictEqual(body.group_ids, [5]);
+ assert.equal(body.auto_pause_on_expired, true);
+ assert.equal(body.update_existing, true);
+ assert.equal(body.expires_at, importExpiresAt);
+ return createJsonResponse({
+ code: 0,
+ data: {
+ total: 1,
+ created: 0,
+ updated: 1,
+ skipped: 0,
+ failed: 0,
+ warnings: [{
+ index: 1,
+ message: '未包含 refresh_token,accessToken 过期后无法自动续期',
+ }],
+ },
+ });
+ }
+
+ return createJsonResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404);
+ },
+ });
+
+ const result = await api.importCurrentChatGptSession({
+ sub2apiUrl: 'https://sub.example/admin/accounts',
+ sub2apiEmail: 'admin@example.com',
+ sub2apiPassword: 'secret',
+ sub2apiGroupName: 'codex',
+ sub2apiDefaultProxyName: 'shadowrocket',
+ sub2apiAccountPriority: 3,
+ session: {
+ accessToken: 'access-token-from-session',
+ expires: '2026-05-20T12:34:56.000Z',
+ user: {
+ email: 'flow@example.com',
+ },
+ },
+ accessToken: 'access-token-from-state',
+ }, {
+ logLabel: '步骤 10',
+ logOptions: { step: 10, stepKey: 'sub2api-session-import' },
+ timeoutMs: 120000,
+ });
+
+ const importCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts/import/codex-session');
+ assert.ok(importCall, 'expected codex-session import call');
+ assert.equal(result.verifiedStatus, 'SUB2API 会话导入完成:新建 0,更新 1,跳过 0,失败 0');
+ assert.equal(result.sub2apiImportUpdated, 1);
+ assert.equal(
+ logs.some((entry) => entry.level === 'warn' && /refresh_token/.test(entry.message)),
+ true
+ );
+});
+
+test('session import step reads current ChatGPT session and completes node', async () => {
+ const moduleApi = loadSub2ApiSessionImportModule();
+ const completed = [];
+ const logs = [];
+ const ensureCalls = [];
+ const sentMessages = [];
+ const importedPayloads = [];
+
+ const executor = moduleApi.createSub2ApiSessionImportExecutor({
+ addLog: async (message, level = 'info', options = {}) => {
+ logs.push({ message, level, step: options.step, stepKey: options.stepKey });
+ },
+ chrome: {
+ tabs: {
+ get: async (tabId) => ({
+ id: tabId,
+ url: 'https://chatgpt.com/?model=gpt-4o',
+ }),
+ update: async () => {},
+ },
+ },
+ completeNodeFromBackground: async (nodeId, payload) => {
+ completed.push({ nodeId, payload });
+ },
+ createSub2ApiApi: () => ({
+ importCurrentChatGptSession: async (state, options) => {
+ importedPayloads.push({ state, options });
+ return {
+ verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
+ sub2apiImportCreated: 1,
+ sub2apiImportUpdated: 0,
+ sub2apiImportSkipped: 0,
+ sub2apiImportFailed: 0,
+ };
+ },
+ }),
+ ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options = {}) => {
+ ensureCalls.push({ source, tabId, options });
+ },
+ getTabId: async () => null,
+ isTabAlive: async () => false,
+ normalizeSub2ApiUrl: (value) => value,
+ registerTab: async () => {},
+ sendTabMessageUntilStopped: async (tabId, source, message) => {
+ sentMessages.push({ tabId, source, message });
+ return {
+ session: {
+ accessToken: 'session-access-token',
+ expires: '2026-05-20T12:34:56.000Z',
+ user: {
+ email: 'flow@example.com',
+ },
+ },
+ accessToken: 'session-access-token',
+ };
+ },
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ waitForTabCompleteUntilStopped: async () => {},
+ DEFAULT_SUB2API_GROUP_NAME: 'codex',
+ });
+
+ await executor.executeSub2ApiSessionImport({
+ nodeId: 'sub2api-session-import',
+ visibleStep: 10,
+ plusCheckoutTabId: 91,
+ sub2apiUrl: 'https://sub.example/admin/accounts',
+ sub2apiEmail: 'admin@example.com',
+ sub2apiPassword: 'secret',
+ sub2apiGroupName: 'codex',
+ });
+
+ assert.equal(ensureCalls.length, 1);
+ assert.equal(ensureCalls[0].source, 'plus-checkout');
+ assert.deepStrictEqual(ensureCalls[0].options.inject, [
+ 'content/utils.js',
+ 'content/operation-delay.js',
+ 'content/plus-checkout.js',
+ ]);
+ assert.deepStrictEqual(sentMessages, [{
+ tabId: 91,
+ source: 'plus-checkout',
+ message: {
+ type: 'PLUS_CHECKOUT_GET_STATE',
+ source: 'background',
+ payload: {
+ includeSession: true,
+ includeAccessToken: true,
+ },
+ },
+ }]);
+ assert.equal(importedPayloads.length, 1);
+ assert.equal(importedPayloads[0].state.accessToken, 'session-access-token');
+ assert.equal(importedPayloads[0].state.session.user.email, 'flow@example.com');
+ assert.equal(completed.length, 1);
+ assert.deepStrictEqual(completed[0], {
+ nodeId: 'sub2api-session-import',
+ payload: {
+ verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
+ sub2apiImportCreated: 1,
+ sub2apiImportUpdated: 0,
+ sub2apiImportSkipped: 0,
+ sub2apiImportFailed: 0,
+ },
+ });
+ assert.equal(
+ logs.some((entry) => entry.stepKey === 'sub2api-session-import' && /读取当前 ChatGPT 登录会话/.test(entry.message)),
+ true
+ );
+});
+
+test('session import step rejects unsupported non-chatgpt tabs before reading session', async () => {
+ const moduleApi = loadSub2ApiSessionImportModule();
+ let sendCalled = false;
+
+ const executor = moduleApi.createSub2ApiSessionImportExecutor({
+ addLog: async () => {},
+ chrome: {
+ tabs: {
+ get: async (tabId) => ({
+ id: tabId,
+ url: 'https://example.com/not-chatgpt',
+ }),
+ update: async () => {},
+ },
+ },
+ completeNodeFromBackground: async () => {},
+ createSub2ApiApi: () => ({
+ importCurrentChatGptSession: async () => ({}),
+ }),
+ ensureContentScriptReadyOnTabUntilStopped: async () => {},
+ getTabId: async () => 91,
+ isTabAlive: async () => true,
+ normalizeSub2ApiUrl: (value) => value,
+ sendTabMessageUntilStopped: async () => {
+ sendCalled = true;
+ return {};
+ },
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ waitForTabCompleteUntilStopped: async () => {},
+ });
+
+ await assert.rejects(
+ () => executor.executeSub2ApiSessionImport({
+ nodeId: 'sub2api-session-import',
+ visibleStep: 10,
+ }),
+ /当前标签页不在 ChatGPT \/ OpenAI 页面/
+ );
+
+ assert.equal(sendCalled, false);
+});
+
+test('background wires sub2api session import executor into the workflow runtime', () => {
+ const source = fs.readFileSync('background.js', 'utf8');
+ assert.match(source, /background\/steps\/sub2api-session-import\.js/);
+ assert.match(source, /'sub2api-session-import': \(state\) => sub2ApiSessionImportExecutor\.executeSub2ApiSessionImport\(state\)/);
+ assert.match(source, /'sub2api-session-import',\s*\n\s*'oauth-login'/);
+});
diff --git a/tests/flow-capabilities-module.test.js b/tests/flow-capabilities-module.test.js
index cad9aa4..c85c6f3 100644
--- a/tests/flow-capabilities-module.test.js
+++ b/tests/flow-capabilities-module.test.js
@@ -191,3 +191,48 @@ test('flow capability registry normalizes unsupported mode switches back to the
]
);
});
+
+test('flow capability registry exposes editable Plus account access strategies for SUB2API', () => {
+ const api = loadApi();
+ const registry = api.createFlowCapabilityRegistry();
+
+ const capabilityState = registry.resolveSidepanelCapabilities({
+ state: {
+ activeFlowId: 'openai',
+ openaiIntegrationTargetId: 'sub2api',
+ signupMethod: 'email',
+ plusModeEnabled: true,
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ },
+ });
+
+ assert.deepEqual(
+ capabilityState.availablePlusAccountAccessStrategies,
+ ['oauth', 'sub2api_codex_session']
+ );
+ assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.equal(capabilityState.canEditPlusAccountAccessStrategy, true);
+ assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'sub2api_codex_session');
+});
+
+test('flow capability registry preserves requested Plus account strategy while forcing OAuth on unsupported targets', () => {
+ const api = loadApi();
+ const registry = api.createFlowCapabilityRegistry();
+
+ const capabilityState = registry.resolveSidepanelCapabilities({
+ state: {
+ activeFlowId: 'openai',
+ openaiIntegrationTargetId: 'cpa',
+ signupMethod: 'email',
+ plusModeEnabled: true,
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ },
+ });
+
+ assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['oauth']);
+ assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'oauth');
+ assert.equal(capabilityState.canEditPlusAccountAccessStrategy, false);
+ assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'oauth');
+});
diff --git a/tests/flow-registry-settings-schema.test.js b/tests/flow-registry-settings-schema.test.js
index a73e1d5..c509678 100644
--- a/tests/flow-registry-settings-schema.test.js
+++ b/tests/flow-registry-settings-schema.test.js
@@ -34,6 +34,10 @@ test('flow registry exposes canonical flow and target metadata', () => {
flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
['cpa', 'sub2api', 'codex2api']
);
+ assert.deepEqual(
+ flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
+ ['row-plus-mode', 'row-plus-payment-method', 'row-plus-account-access-strategy']
+ );
assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
});
@@ -48,6 +52,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
ipProxyEnabled: true,
ipProxyService: '711proxy',
customPassword: 'SharedSecret123!',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'secret-key',
stepExecutionRangeByFlow: {
@@ -61,6 +66,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
assert.equal(normalized.services.proxy.enabled, true);
assert.equal(normalized.services.account.customPassword, 'SharedSecret123!');
assert.equal(normalized.flows.openai.integrationTargetId, 'sub2api');
+ assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(normalized.flows.kiro.targetId, 'kiro-rs');
assert.equal(normalized.flows.kiro.targets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin');
assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key');
@@ -79,6 +85,7 @@ test('settings schema can project canonical state into a read view without legac
kiroTargetId: 'kiro-rs',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'key-123',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
});
const view = schema.buildSettingsView(normalized);
@@ -88,6 +95,7 @@ test('settings schema can project canonical state into a read view without legac
assert.equal(view.panelMode, 'cpa');
assert.equal(view.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(view.kiroRsKey, 'key-123');
+ assert.equal(view.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(view.settingsSchemaVersion, 4);
assert.equal(view.settingsState.activeFlowId, 'kiro');
});
diff --git a/tests/gopay-manual-confirm.test.js b/tests/gopay-manual-confirm.test.js
index 6bd7368..11384ac 100644
--- a/tests/gopay-manual-confirm.test.js
+++ b/tests/gopay-manual-confirm.test.js
@@ -6,7 +6,7 @@ const source = fs.readFileSync('background/steps/gopay-manual-confirm.js', 'utf8
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundGoPayManualConfirm;`)(globalScope);
-test('GoPay manual confirm executor publishes pending manual confirmation state for step 7', async () => {
+test('GoPay manual confirm executor publishes pending manual confirmation state for OAuth continuation', async () => {
const stateUpdates = [];
const broadcasts = [];
const events = [];
@@ -24,6 +24,13 @@ test('GoPay manual confirm executor publishes pending manual confirmation state
},
},
},
+ getNodeIdsForState: () => [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'gopay-subscription-confirm',
+ 'oauth-login',
+ ],
getTabId: async () => 42,
isTabAlive: async () => true,
registerTab: async () => {},
@@ -32,17 +39,70 @@ test('GoPay manual confirm executor publishes pending manual confirmation state
},
});
- await executor.executeGoPayManualConfirm({ plusCheckoutTabId: 42, plusCheckoutUrl: 'https://chatgpt.com/checkout/openai_llc/session' });
+ await executor.executeGoPayManualConfirm({
+ nodeId: 'gopay-subscription-confirm',
+ plusCheckoutTabId: 42,
+ plusCheckoutUrl: 'https://chatgpt.com/checkout/openai_llc/session',
+ });
assert.equal(stateUpdates.length, 1);
assert.equal(stateUpdates[0].plusManualConfirmationPending, true);
assert.equal(stateUpdates[0].plusManualConfirmationMethod, 'gopay');
assert.equal(stateUpdates[0].plusManualConfirmationStep, 7);
assert.match(String(stateUpdates[0].plusManualConfirmationRequestId || ''), /^gopay-/);
+ assert.match(stateUpdates[0].plusManualConfirmationMessage, /OAuth 登录/);
assert.equal(broadcasts.length, 1);
assert.equal(broadcasts[0].plusManualConfirmationPending, true);
assert.deepStrictEqual(
events.find((event) => event.type === 'tab-update'),
{ type: 'tab-update', tabId: 42, payload: { active: true } }
);
+ assert.match(
+ events.find((event) => event.type === 'log')?.message || '',
+ /确认后继续OAuth 登录/
+ );
+});
+
+test('GoPay manual confirm executor switches continuation copy to SUB2API session import when the effective tail is session-based', async () => {
+ const stateUpdates = [];
+ const events = [];
+ const executor = api.createGoPayManualConfirmExecutor({
+ addLog: async (message, level = 'info') => {
+ events.push({ type: 'log', message, level });
+ },
+ broadcastDataUpdate: () => {},
+ chrome: {
+ tabs: {
+ update: async () => {},
+ },
+ },
+ getNodeIdsForState: () => [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'gopay-subscription-confirm',
+ 'sub2api-session-import',
+ ],
+ getTabId: async () => 42,
+ isTabAlive: async () => true,
+ registerTab: async () => {},
+ setState: async (payload) => {
+ stateUpdates.push(payload);
+ },
+ });
+
+ await executor.executeGoPayManualConfirm({
+ nodeId: 'gopay-subscription-confirm',
+ visibleStep: 9,
+ plusCheckoutTabId: 42,
+ plusCheckoutUrl: 'https://chatgpt.com/checkout/openai_llc/session',
+ });
+
+ assert.equal(stateUpdates.length, 1);
+ assert.equal(stateUpdates[0].plusManualConfirmationStep, 9);
+ assert.match(stateUpdates[0].plusManualConfirmationMessage, /导入当前 ChatGPT 会话到 SUB2API/);
+ assert.match(
+ events.find((event) => event.type === 'log')?.message || '',
+ /确认后继续导入当前 ChatGPT 会话到 SUB2API/
+ );
});
diff --git a/tests/plus-account-access-strategy.test.js b/tests/plus-account-access-strategy.test.js
new file mode 100644
index 0000000..13501c7
--- /dev/null
+++ b/tests/plus-account-access-strategy.test.js
@@ -0,0 +1,270 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
+
+function extractFunction(name) {
+ const markers = [`async function ${name}(`, `function ${name}(`];
+ const start = markers
+ .map((marker) => sidepanelSource.indexOf(marker))
+ .find((index) => index >= 0);
+ if (start < 0) {
+ throw new Error(`missing function ${name}`);
+ }
+
+ let parenDepth = 0;
+ let signatureEnded = false;
+ let braceStart = -1;
+ for (let i = start; i < sidepanelSource.length; i += 1) {
+ const ch = sidepanelSource[i];
+ if (ch === '(') parenDepth += 1;
+ if (ch === ')') {
+ parenDepth -= 1;
+ if (parenDepth === 0) signatureEnded = true;
+ }
+ if (ch === '{' && signatureEnded) {
+ braceStart = i;
+ break;
+ }
+ }
+ if (braceStart < 0) {
+ throw new Error(`missing body for function ${name}`);
+ }
+
+ let depth = 0;
+ let end = braceStart;
+ for (; end < sidepanelSource.length; end += 1) {
+ const ch = sidepanelSource[end];
+ if (ch === '{') depth += 1;
+ if (ch === '}') {
+ depth -= 1;
+ if (depth === 0) {
+ end += 1;
+ break;
+ }
+ }
+ }
+ return sidepanelSource.slice(start, end);
+}
+
+function buildHarness(capabilityStateSource, stateSource) {
+ return new Function(`
+const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
+const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
+const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
+const DEFAULT_PLUS_PAYMENT_METHOD = 'paypal';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
+const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
+const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+${extractFunction('normalizePlusAccountAccessStrategy')}
+${extractFunction('getRequestedPlusAccountAccessStrategy')}
+${extractFunction('updatePlusModeUI')}
+function normalizePlusPaymentMethod(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
+}
+function getSelectedPlusPaymentMethod() {
+ return normalizePlusPaymentMethod(selectPlusPaymentMethod.value || latestState?.plusPaymentMethod || currentPlusPaymentMethod || 'paypal');
+}
+function normalizeGpcHelperPhoneModeValue(value = '') {
+ return String(value || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual';
+}
+function normalizeGpcOtpChannelValue(value = '') {
+ return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
+}
+function isGpcAutoModePermissionDenied() {
+ return false;
+}
+function getSelectedPanelMode() {
+ return latestState?.panelMode || 'cpa';
+}
+function resolveCurrentSidepanelCapabilities() {
+ return ${capabilityStateSource};
+}
+let latestState = ${stateSource};
+let currentPlusPaymentMethod = 'paypal';
+let currentPlusAccountAccessStrategy = latestState.plusAccountAccessStrategy || 'oauth';
+const inputPlusModeEnabled = { checked: true };
+const rowPlusMode = { style: { display: '' } };
+const rowPlusPaymentMethod = { style: { display: 'none' } };
+const rowPlusAccountAccessStrategy = { style: { display: 'none' } };
+const rowPayPalAccount = { style: { display: 'none' } };
+const selectPlusPaymentMethod = { value: latestState.plusPaymentMethod || 'paypal', style: { display: 'none' } };
+const selectPlusAccountAccessStrategy = {
+ value: latestState.plusAccountAccessStrategy || 'oauth',
+ disabled: false,
+ dataset: { requestedValue: latestState.plusAccountAccessStrategy || 'oauth' },
+ setAttribute(name, value) {
+ this[name] = value;
+ },
+};
+const plusPaymentMethodCaption = { textContent: '' };
+const plusAccountAccessStrategyCaption = { textContent: '' };
+return {
+ getRequestedPlusAccountAccessStrategy,
+ plusAccountAccessStrategyCaption,
+ rowPlusAccountAccessStrategy,
+ selectPlusAccountAccessStrategy,
+ updatePlusModeUI,
+};
+`)();
+}
+
+test('sidepanel keeps requested Plus account strategy while OAuth-only targets force the effective value', () => {
+ const api = buildHarness(
+ `{
+ canShowPlusSettings: true,
+ runtimeLocks: { plusModeEnabled: true },
+ canEditPlusAccountAccessStrategy: false,
+ effectivePlusAccountAccessStrategy: 'oauth',
+ }`,
+ `{
+ activeFlowId: 'openai',
+ panelMode: 'cpa',
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ }`
+ );
+
+ api.updatePlusModeUI();
+
+ assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
+ assert.equal(api.selectPlusAccountAccessStrategy.disabled, true);
+ assert.equal(api.selectPlusAccountAccessStrategy.dataset.requestedValue, 'sub2api_codex_session');
+ assert.equal(api.selectPlusAccountAccessStrategy.value, 'oauth');
+ assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'sub2api_codex_session');
+ assert.match(api.plusAccountAccessStrategyCaption.textContent, /OAuth/);
+});
+
+test('sidepanel enables SUB2API session strategy selection when the current Plus target supports it', () => {
+ const api = buildHarness(
+ `{
+ canShowPlusSettings: true,
+ runtimeLocks: { plusModeEnabled: true },
+ canEditPlusAccountAccessStrategy: true,
+ effectivePlusAccountAccessStrategy: 'sub2api_codex_session',
+ }`,
+ `{
+ activeFlowId: 'openai',
+ panelMode: 'sub2api',
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ }`
+ );
+
+ api.updatePlusModeUI();
+
+ assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
+ assert.equal(api.selectPlusAccountAccessStrategy.disabled, false);
+ assert.equal(api.selectPlusAccountAccessStrategy.value, 'sub2api_codex_session');
+ assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'sub2api_codex_session');
+ assert.match(api.plusAccountAccessStrategyCaption.textContent, /SUB2API/);
+});
+
+test('sidepanel rebuilds step definitions and workflow nodes with the effective Plus account strategy', () => {
+ const bundle = [
+ extractFunction('normalizeSignupMethod'),
+ extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
+ extractFunction('getStepDefinitionsForMode'),
+ extractFunction('getWorkflowNodesForMode'),
+ extractFunction('rebuildStepDefinitionState'),
+ extractFunction('syncStepDefinitionsForMode'),
+ ].join('\n');
+
+ const api = new Function(`
+const calls = [];
+const window = {
+ MultiPageStepDefinitions: {
+ getSteps(options) {
+ calls.push({ type: 'getSteps', options });
+ return [{ id: 10, order: 100, key: 'sub2api-session-import', title: 'session-import' }];
+ },
+ getNodes(options) {
+ calls.push({ type: 'getNodes', options });
+ return [{ nodeId: 'sub2api-session-import', displayOrder: 10, next: [] }];
+ },
+ },
+};
+let latestState = { activeFlowId: 'openai' };
+let currentPlusModeEnabled = false;
+let currentPlusPaymentMethod = 'paypal';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+let currentPlusAccountAccessStrategy = 'oauth';
+let currentSignupMethod = 'email';
+let currentPhoneSignupReloginAfterBindEmailEnabled = false;
+let currentStepDefinitionFlowId = 'openai';
+const DEFAULT_ACTIVE_FLOW_ID = 'openai';
+const DEFAULT_PLUS_PAYMENT_METHOD = 'paypal';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
+const DEFAULT_SIGNUP_METHOD = 'email';
+let stepDefinitions = [];
+let workflowNodes = [];
+let STEP_IDS = [];
+let STEP_DEFAULT_STATUSES = {};
+let SKIPPABLE_STEPS = new Set();
+let NODE_IDS = [];
+let NODE_DEFAULT_STATUSES = {};
+function getSelectedPlusPaymentMethod() {
+ return currentPlusPaymentMethod;
+}
+function renderStepsList() {}
+${bundle}
+return {
+ calls,
+ syncStepDefinitionsForMode,
+ snapshot() {
+ return {
+ currentPlusAccountAccessStrategy,
+ workflowNodes,
+ stepDefinitions,
+ STEP_IDS,
+ NODE_IDS,
+ };
+ },
+};
+`)();
+
+ api.syncStepDefinitionsForMode(true, {
+ activeFlowId: 'openai',
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ signupMethod: 'email',
+ render: true,
+ });
+
+ const snapshot = api.snapshot();
+ assert.equal(snapshot.currentPlusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.deepStrictEqual(snapshot.STEP_IDS, [10]);
+ assert.deepStrictEqual(snapshot.NODE_IDS, ['sub2api-session-import']);
+ assert.equal(snapshot.stepDefinitions[0]?.key, 'sub2api-session-import');
+ assert.equal(snapshot.workflowNodes[0]?.nodeId, 'sub2api-session-import');
+ assert.deepStrictEqual(api.calls, [
+ {
+ type: 'getSteps',
+ options: {
+ activeFlowId: 'openai',
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ signupMethod: 'email',
+ phoneSignupReloginAfterBindEmailEnabled: false,
+ },
+ },
+ {
+ type: 'getNodes',
+ options: {
+ activeFlowId: 'openai',
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ signupMethod: 'email',
+ phoneSignupReloginAfterBindEmailEnabled: false,
+ },
+ },
+ ]);
+});
diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js
index ab8328a..0088740 100644
--- a/tests/sidepanel-plus-payment-method.test.js
+++ b/tests/sidepanel-plus-payment-method.test.js
@@ -65,6 +65,7 @@ test('sidepanel step definitions keep the selected Plus payment method', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getStepDefinitionsForMode'),
extractFunction('rebuildStepDefinitionState'),
extractFunction('syncStepDefinitionsForMode'),
@@ -82,6 +83,10 @@ const window = {
};
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
+let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email';
@@ -107,7 +112,7 @@ return {
assert.deepEqual(api.getStepIds(), [7]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
- options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
+ options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
});
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
});
@@ -141,7 +146,9 @@ test('sidepanel applies restored signup method when rebuilding shared step defin
test('sidepanel Plus UI hides PayPal account selector while GoPay is selected', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
+ extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -156,10 +163,14 @@ test('sidepanel Plus UI hides PayPal account selector while GoPay is selected',
const api = new Function(`
let latestState = { plusPaymentMethod: 'gopay' };
let currentPlusPaymentMethod = 'paypal';
+let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gopay', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const rowPayPalAccount = { style: { display: '' } };
${bundle}
return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
@@ -178,7 +189,9 @@ return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
test('sidepanel Plus UI can hide Plus controls when the shared flow capability registry disables them', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
+ extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -206,6 +219,7 @@ const window = {
},
};
let latestState = { plusPaymentMethod: 'paypal' };
+let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const rowPlusMode = { style: { display: '' } };
const selectPlusPaymentMethod = { value: 'paypal', style: { display: '' } };
@@ -213,6 +227,9 @@ const rowPlusPaymentMethod = { style: { display: '' } };
const rowPayPalAccount = { style: { display: '' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
${bundle}
return {
rowPlusMode,
@@ -235,6 +252,7 @@ test('sidepanel step definitions keep GPC helper mode distinct', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getStepDefinitionsForMode'),
extractFunction('rebuildStepDefinitionState'),
extractFunction('syncStepDefinitionsForMode'),
@@ -252,6 +270,10 @@ const window = {
};
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
+let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email';
@@ -277,14 +299,16 @@ return {
assert.deepEqual(api.getStepIds(), [13]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
- options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
+ options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
});
});
test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
+ extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -299,10 +323,14 @@ test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () =
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperAutoModeEnabled: true };
let currentPlusPaymentMethod = 'paypal';
+let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
@@ -380,7 +408,9 @@ return {
test('sidepanel keeps selected GPC auto mode when API Key has no auto permission', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
+ extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -395,10 +425,14 @@ test('sidepanel keeps selected GPC auto mode when API Key has no auto permission
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: { auto_mode_enabled: false } };
let currentPlusPaymentMethod = 'gpc-helper';
+let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
@@ -432,7 +466,9 @@ return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, r
test('sidepanel keeps selected GPC auto mode when persisted permission survives stop refresh', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
+ extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -452,10 +488,14 @@ let latestState = {
gopayHelperBalancePayload: { auto_mode_enabled: true },
};
let currentPlusPaymentMethod = 'gpc-helper';
+let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
@@ -513,7 +553,9 @@ return {
test('sidepanel keeps selected GPC auto mode before permission has been queried', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
+ extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -528,10 +570,14 @@ test('sidepanel keeps selected GPC auto mode before permission has been queried'
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: null };
let currentPlusPaymentMethod = 'gpc-helper';
+let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js
index 761554f..f6e6099 100644
--- a/tests/step-definitions-module.test.js
+++ b/tests/step-definitions-module.test.js
@@ -254,6 +254,87 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(gpcSteps[6].title, '等待 GPC 任务完成');
});
+test('Plus session strategy swaps the OAuth tail for a single SUB2API import node', () => {
+ const source = fs.readFileSync('data/step-definitions.js', 'utf8');
+ const globalScope = {};
+ const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope);
+ const forbiddenTailKeys = [
+ 'oauth-login',
+ 'fetch-login-code',
+ 'post-login-phone-verification',
+ 'confirm-oauth',
+ 'platform-verify',
+ ];
+
+ [
+ {
+ label: 'paypal',
+ options: {
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ },
+ previousNodeId: 'plus-checkout-return',
+ expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ },
+ {
+ label: 'gopay',
+ options: {
+ plusModeEnabled: true,
+ plusPaymentMethod: 'gopay',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ },
+ previousNodeId: 'gopay-subscription-confirm',
+ expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
+ },
+ {
+ label: 'gpc-helper',
+ options: {
+ plusModeEnabled: true,
+ plusPaymentMethod: 'gpc-helper',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ },
+ previousNodeId: 'plus-checkout-billing',
+ expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
+ },
+ ].forEach(({ label, options, previousNodeId, expectedStepIds }) => {
+ const steps = api.getSteps(options);
+ const nodes = api.getNodes(options);
+ const stepKeys = steps.map((step) => step.key);
+ const nodeIds = nodes.map((node) => node.nodeId);
+ const previousNode = nodes.find((node) => node.nodeId === previousNodeId);
+ const sessionImportNode = nodes.find((node) => node.nodeId === 'sub2api-session-import');
+
+ assert.equal(stepKeys.at(-1), 'sub2api-session-import', `${label} should end with session import`);
+ assert.equal(nodeIds.at(-1), 'sub2api-session-import', `${label} node order should end with session import`);
+ forbiddenTailKeys.forEach((key) => {
+ assert.equal(stepKeys.includes(key), false, `${label} should not keep ${key} in session mode`);
+ assert.equal(nodeIds.includes(key), false, `${label} nodes should not keep ${key} in session mode`);
+ });
+ assert.deepStrictEqual(api.getStepIds(options), expectedStepIds, `${label} step ids should follow the new tail`);
+ assert.equal(api.getLastStepId(options), expectedStepIds.at(-1), `${label} last step id should match session import`);
+ assert.deepStrictEqual(previousNode?.next, ['sub2api-session-import'], `${label} previous node should link to session import`);
+ assert.deepStrictEqual(sessionImportNode?.next, [], `${label} session import should be terminal`);
+ });
+});
+
+test('Plus phone signup never switches to SUB2API session tail even if the requested strategy is session import', () => {
+ const source = fs.readFileSync('data/step-definitions.js', 'utf8');
+ const globalScope = {};
+ const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope);
+ const steps = api.getSteps({
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ signupMethod: 'phone',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
+ });
+ const stepKeys = steps.map((step) => step.key);
+
+ assert.equal(stepKeys.includes('sub2api-session-import'), false);
+ assert.equal(stepKeys.includes('oauth-login'), true);
+ assert.equal(stepKeys.includes('platform-verify'), true);
+});
+
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const definitionsIndex = html.indexOf('');