From 40b9cd397db966ed4337678926caf3eb88614b78 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Tue, 19 May 2026 16:56:08 +0800
Subject: [PATCH] feat: add CPA plus session import strategy
---
background.js | 72 ++-
background/cpa-api.js | 507 ++++++++++++++++++
background/message-router.js | 46 +-
background/steps/cpa-session-import.js | 276 ++++++++++
background/steps/gopay-manual-confirm.js | 29 +-
data/step-definitions.js | 84 ++-
shared/flow-capabilities.js | 18 +-
shared/settings-schema.js | 15 +-
sidepanel/sidepanel.html | 1 +
sidepanel/sidepanel.js | 187 ++++++-
...ackground-account-history-settings.test.js | 6 +
tests/background-auth-chain-guard.test.js | 3 +-
tests/background-cpa-api.test.js | 184 +++++++
tests/background-cpa-session-import.test.js | 247 +++++++++
...ctive-plus-account-access-strategy.test.js | 62 ++-
...und-message-router-plus-final-step.test.js | 30 ++
...ground-settings-schema-persistence.test.js | 5 +
.../background-sub2api-session-import.test.js | 2 +-
tests/flow-capabilities-module.test.js | 30 +-
tests/flow-registry-settings-schema.test.js | 12 +
tests/gopay-manual-confirm.test.js | 52 +-
tests/plus-account-access-strategy.test.js | 37 ++
tests/sidepanel-plus-payment-method.test.js | 22 +
tests/step-definitions-module.test.js | 64 +++
24 files changed, 1936 insertions(+), 55 deletions(-)
create mode 100644 background/cpa-api.js
create mode 100644 background/steps/cpa-session-import.js
create mode 100644 tests/background-cpa-api.test.js
create mode 100644 tests/background-cpa-session-import.test.js
diff --git a/background.js b/background.js
index bddd89f..f6000d9 100644
--- a/background.js
+++ b/background.js
@@ -21,6 +21,7 @@ importScripts(
'background/ip-proxy-provider-711proxy.js',
'background/ip-proxy-core.js',
'background/sub2api-api.js',
+ 'background/cpa-api.js',
'background/panel-bridge.js',
'background/registration-email-state.js',
'background/workflow-engine.js',
@@ -56,6 +57,7 @@ importScripts(
'background/steps/gopay-approve.js',
'background/steps/plus-return-confirm.js',
'background/steps/sub2api-session-import.js',
+ 'background/steps/cpa-session-import.js',
'background/steps/oauth-login.js',
'background/steps/fetch-login-code.js',
'background/steps/confirm-oauth.js',
@@ -77,6 +79,7 @@ importScripts(
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const NORMAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
plusModeEnabled: false,
@@ -103,6 +106,12 @@ const PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitio
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
}) || PLUS_PAYPAL_STEP_DEFINITIONS;
+const PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
+}) || PLUS_PAYPAL_STEP_DEFINITIONS;
const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
plusModeEnabled: true,
@@ -127,6 +136,12 @@ const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinition
plusPaymentMethod: 'gopay',
plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
}) || PLUS_GOPAY_STEP_DEFINITIONS;
+const PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ plusModeEnabled: true,
+ plusPaymentMethod: 'gopay',
+ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
+}) || PLUS_GOPAY_STEP_DEFINITIONS;
const PLUS_GOPAY_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
plusModeEnabled: true,
@@ -151,6 +166,12 @@ const PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?
plusPaymentMethod: 'gpc-helper',
plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
}) || PLUS_GPC_STEP_DEFINITIONS;
+const PLUS_GPC_CPA_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ plusModeEnabled: true,
+ plusPaymentMethod: 'gpc-helper',
+ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
+}) || PLUS_GPC_STEP_DEFINITIONS;
const PLUS_GPC_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
plusModeEnabled: true,
@@ -187,14 +208,17 @@ const ALL_STEP_DEFINITIONS = (() => {
...NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
...PLUS_PAYPAL_STEP_DEFINITIONS,
...PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_CPA_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_CPA_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_CPA_SESSION_STEP_DEFINITIONS,
...PLUS_GPC_PHONE_STEP_DEFINITIONS,
...PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
];
@@ -830,6 +854,12 @@ function getStepDefinitionsForState(state = {}) {
) {
return PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS;
}
+ if (
+ signupMethod === SIGNUP_METHOD_EMAIL
+ && plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION
+ ) {
+ return PLUS_GPC_CPA_SESSION_STEP_DEFINITIONS;
+ }
return PLUS_GPC_STEP_DEFINITIONS;
}
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY) {
@@ -839,6 +869,12 @@ function getStepDefinitionsForState(state = {}) {
) {
return PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS;
}
+ if (
+ signupMethod === SIGNUP_METHOD_EMAIL
+ && plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION
+ ) {
+ return PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS;
+ }
return PLUS_GOPAY_STEP_DEFINITIONS;
}
if (
@@ -847,6 +883,12 @@ function getStepDefinitionsForState(state = {}) {
) {
return PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS;
}
+ if (
+ signupMethod === SIGNUP_METHOD_EMAIL
+ && plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION
+ ) {
+ return PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS;
+ }
return PLUS_PAYPAL_STEP_DEFINITIONS;
}
@@ -1798,9 +1840,13 @@ function normalizePlusPaymentMethod(value = '') {
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;
+ if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION;
+ }
+ if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
+ }
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
}
function normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) {
@@ -2977,9 +3023,7 @@ function normalizePersistentSettingValue(key, value) {
case 'plusPaymentMethod':
return normalizePlusPaymentMethod(value);
case 'plusAccountAccessStrategy':
- return String(value || '').trim().toLowerCase() === 'sub2api_codex_session'
- ? 'sub2api_codex_session'
- : 'oauth';
+ return normalizePlusAccountAccessStrategy(value);
case 'paypalEmail':
return String(value || '').trim();
case 'paypalPassword':
@@ -10361,6 +10405,7 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
'paypal-approve',
'plus-checkout-return',
'sub2api-session-import',
+ 'cpa-session-import',
'oauth-login',
'fetch-login-code',
'post-login-phone-verification',
@@ -11460,6 +11505,7 @@ const AUTO_RUN_NODE_DELAYS = Object.freeze({
'paypal-approve': 2000,
'plus-checkout-return': 1000,
'sub2api-session-import': 0,
+ 'cpa-session-import': 0,
'oauth-login': 2000,
'fetch-login-code': 2000,
'confirm-oauth': 1000,
@@ -13280,6 +13326,19 @@ const sub2ApiSessionImportExecutor = self.MultiPageBackgroundSub2ApiSessionImpor
waitForTabCompleteUntilStopped,
DEFAULT_SUB2API_GROUP_NAME,
});
+const cpaSessionImportExecutor = self.MultiPageBackgroundCpaSessionImport?.createCpaSessionImportExecutor({
+ addLog,
+ chrome,
+ completeNodeFromBackground,
+ ensureContentScriptReadyOnTabUntilStopped,
+ getTabId,
+ isTabAlive,
+ registerTab,
+ sendTabMessageUntilStopped,
+ sleepWithStop,
+ throwIfStopped,
+ waitForTabCompleteUntilStopped,
+});
const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKiroRegisterRunner({
addLog,
chrome,
@@ -13429,6 +13488,7 @@ const stepExecutorsByKey = {
: payPalApproveExecutor.executePayPalApprove(state),
'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state),
'sub2api-session-import': (state) => sub2ApiSessionImportExecutor.executeSub2ApiSessionImport(state),
+ 'cpa-session-import': (state) => cpaSessionImportExecutor.executeCpaSessionImport(state),
'oauth-login': (state) => step7Executor.executeStep7(state),
'fetch-login-code': (state) => step8Executor.executeStep8(state),
'post-login-phone-verification': (state) => step8Executor.executePostLoginPhoneVerification(state),
diff --git a/background/cpa-api.js b/background/cpa-api.js
new file mode 100644
index 0000000..7cfd6b2
--- /dev/null
+++ b/background/cpa-api.js
@@ -0,0 +1,507 @@
+(function attachBackgroundCpaApi(root, factory) {
+ root.MultiPageBackgroundCpaApi = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaApiModule() {
+ function createCpaApi(deps = {}) {
+ const {
+ addLog = async () => {},
+ fetchImpl = (...args) => fetch(...args),
+ } = deps;
+
+ function normalizeString(value = '') {
+ return String(value || '').trim();
+ }
+
+ function isPlainObject(value) {
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
+ }
+
+ function firstNonEmpty(...values) {
+ for (const value of values) {
+ const normalized = normalizeString(value);
+ if (normalized) {
+ return normalized;
+ }
+ }
+ return '';
+ }
+
+ function normalizeEmailValue(value = '') {
+ const email = normalizeString(value);
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : '';
+ }
+
+ function extractStateFromAuthUrl(authUrl = '') {
+ try {
+ return new URL(authUrl).searchParams.get('state') || '';
+ } catch {
+ return '';
+ }
+ }
+
+ function deriveCpaManagementOrigin(vpsUrl) {
+ const normalizedUrl = normalizeString(vpsUrl);
+ if (!normalizedUrl) {
+ throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
+ }
+ let parsed;
+ try {
+ parsed = new URL(normalizedUrl);
+ } catch {
+ throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
+ }
+ return parsed.origin;
+ }
+
+ function getCpaApiErrorMessage(payload, responseStatus = 500) {
+ const candidates = [
+ payload?.error,
+ payload?.message,
+ payload?.detail,
+ payload?.reason,
+ ];
+ const message = candidates.map(normalizeString).find(Boolean);
+ return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
+ }
+
+ async function fetchCpaManagementJson(origin, path, options = {}) {
+ const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
+
+ try {
+ const managementKey = normalizeString(options.managementKey);
+ const headers = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ };
+ if (managementKey) {
+ headers.Authorization = `Bearer ${managementKey}`;
+ headers['X-Management-Key'] = managementKey;
+ }
+
+ const response = await fetchImpl(`${origin}${path}`, {
+ method: options.method || 'POST',
+ headers,
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
+ signal: controller.signal,
+ });
+
+ let payload = {};
+ try {
+ payload = await response.json();
+ } catch {
+ payload = {};
+ }
+
+ if (!response.ok) {
+ throw new Error(getCpaApiErrorMessage(payload, response.status));
+ }
+
+ return payload;
+ } catch (error) {
+ if (error?.name === 'AbortError') {
+ throw new Error('CPA 管理接口请求超时,请稍后重试。');
+ }
+ throw error;
+ } finally {
+ clearTimeout(timer);
+ }
+ }
+
+ function decodeBase64UrlSegment(segment = '') {
+ const normalized = normalizeString(segment)
+ .replace(/-/g, '+')
+ .replace(/_/g, '/');
+ if (!normalized) {
+ return '';
+ }
+ const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
+ try {
+ if (typeof Buffer !== 'undefined') {
+ return Buffer.from(padded, 'base64').toString('utf8');
+ }
+ if (typeof atob === 'function') {
+ const binary = atob(padded);
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
+ if (typeof TextDecoder !== 'undefined') {
+ return new TextDecoder().decode(bytes);
+ }
+ return binary;
+ }
+ } catch {
+ return '';
+ }
+ return '';
+ }
+
+ function encodeBase64UrlJson(value) {
+ const json = JSON.stringify(value);
+ if (typeof Buffer !== 'undefined') {
+ return Buffer.from(json, 'utf8')
+ .toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/g, '');
+ }
+ const bytes = new TextEncoder().encode(json);
+ let binary = '';
+ bytes.forEach((byte) => {
+ binary += String.fromCharCode(byte);
+ });
+ return btoa(binary)
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/g, '');
+ }
+
+ function parseJwtPayload(token = '') {
+ const normalized = normalizeString(token);
+ if (!normalized) {
+ return null;
+ }
+ const parts = normalized.split('.');
+ if (parts.length < 2) {
+ return null;
+ }
+ try {
+ return JSON.parse(decodeBase64UrlSegment(parts[1]));
+ } catch {
+ return null;
+ }
+ }
+
+ function getOpenAiAuthSection(payload) {
+ if (!isPlainObject(payload)) {
+ return {};
+ }
+ const auth = payload['https://api.openai.com/auth'];
+ return isPlainObject(auth) ? auth : {};
+ }
+
+ function getOpenAiProfileSection(payload) {
+ if (!isPlainObject(payload)) {
+ return {};
+ }
+ const profile = payload['https://api.openai.com/profile'];
+ return isPlainObject(profile) ? profile : {};
+ }
+
+ function normalizeTimestamp(value) {
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
+ return value.toISOString();
+ }
+ if (typeof value === 'number' && Number.isFinite(value)) {
+ const milliseconds = value > 1e11 ? value : value * 1000;
+ const date = new Date(milliseconds);
+ return Number.isNaN(date.getTime()) ? '' : date.toISOString();
+ }
+ if (typeof value !== 'string' || !value.trim()) {
+ return '';
+ }
+ const date = new Date(value);
+ return Number.isNaN(date.getTime()) ? '' : date.toISOString();
+ }
+
+ function timestampFromUnixSeconds(value) {
+ const numeric = Number(value);
+ if (!Number.isFinite(numeric)) {
+ return '';
+ }
+ const date = new Date(numeric * 1000);
+ return Number.isNaN(date.getTime()) ? '' : date.toISOString();
+ }
+
+ function epochSecondsFromValue(value) {
+ if (value === undefined || value === null || value === '') {
+ return 0;
+ }
+ const numeric = Number(value);
+ if (Number.isFinite(numeric)) {
+ return Math.trunc(numeric > 1e11 ? numeric / 1000 : numeric);
+ }
+ const parsed = Date.parse(String(value));
+ return Number.isFinite(parsed) ? Math.trunc(parsed / 1000) : 0;
+ }
+
+ function buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt) {
+ const normalizedAccountId = normalizeString(accountId);
+ if (!normalizedAccountId) {
+ return '';
+ }
+ const now = Math.trunc(Date.now() / 1000);
+ const expires = epochSecondsFromValue(expiresAt) || now + 90 * 24 * 60 * 60;
+ const authInfo = { chatgpt_account_id: normalizedAccountId };
+
+ if (planType) {
+ authInfo.chatgpt_plan_type = normalizeString(planType);
+ }
+ if (userId) {
+ authInfo.chatgpt_user_id = normalizeString(userId);
+ authInfo.user_id = normalizeString(userId);
+ }
+
+ const payload = {
+ iat: now,
+ exp: expires,
+ 'https://api.openai.com/auth': authInfo,
+ };
+ if (email) {
+ payload.email = normalizeString(email);
+ }
+
+ return `${encodeBase64UrlJson({ alg: 'none', typ: 'JWT', cpa_synthetic: true })}.${encodeBase64UrlJson(payload)}.synthetic`;
+ }
+
+ function normalizePlanTypeForFileName(planType = '') {
+ return normalizeString(planType)
+ .split(/[^a-zA-Z0-9]+/)
+ .map((part) => part.trim().toLowerCase())
+ .filter(Boolean)
+ .join('-');
+ }
+
+ function sanitizeFileSegment(value = '', fallback = 'chatgpt-session') {
+ const normalized = normalizeString(value)
+ .replace(/[\\/:*?"<>|]+/g, '-')
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/^-+|-+$/g, '');
+ return normalized || fallback;
+ }
+
+ function buildCpaAuthFileName(metadata = {}) {
+ const email = sanitizeFileSegment(metadata.email || '');
+ const planType = normalizePlanTypeForFileName(metadata.planType || '');
+ const accountId = sanitizeFileSegment(metadata.accountId || '');
+ if (email && planType) {
+ return `codex-${email}-${planType}.json`;
+ }
+ if (email) {
+ return `codex-${email}.json`;
+ }
+ if (accountId && planType) {
+ return `codex-${accountId}-${planType}.json`;
+ }
+ if (accountId) {
+ return `codex-${accountId}.json`;
+ }
+ return `codex-${Date.now()}.json`;
+ }
+
+ function buildCpaSessionAuthJson(state = {}, options = {}) {
+ const session = isPlainObject(state?.session) ? state.session : {};
+ const accessToken = normalizeString(state?.accessToken || session?.accessToken);
+ if (!accessToken) {
+ throw new Error('未读取到可导入的 ChatGPT accessToken。');
+ }
+
+ const inputIdToken = firstNonEmpty(
+ state?.idToken,
+ state?.id_token,
+ session?.idToken,
+ session?.id_token
+ );
+ const refreshToken = firstNonEmpty(
+ state?.refreshToken,
+ state?.refresh_token,
+ session?.refreshToken,
+ session?.refresh_token
+ );
+ const sessionToken = firstNonEmpty(
+ state?.sessionToken,
+ state?.session_token,
+ session?.sessionToken,
+ session?.session_token
+ );
+ const accessPayload = parseJwtPayload(accessToken);
+ const idPayload = parseJwtPayload(inputIdToken);
+ const accessAuth = getOpenAiAuthSection(accessPayload);
+ const idAuth = getOpenAiAuthSection(idPayload);
+ const profile = getOpenAiProfileSection(accessPayload);
+ const expiresAt = firstNonEmpty(
+ timestampFromUnixSeconds(accessPayload?.exp),
+ normalizeTimestamp(session?.expires),
+ normalizeTimestamp(session?.expiresAt),
+ normalizeTimestamp(session?.expired),
+ normalizeTimestamp(session?.expires_at)
+ );
+ const accountIdentifierEmail = normalizeString(state?.accountIdentifierType).toLowerCase() === 'email'
+ ? normalizeEmailValue(state?.accountIdentifier)
+ : '';
+ const email = firstNonEmpty(
+ normalizeEmailValue(session?.user?.email),
+ normalizeEmailValue(session?.email),
+ normalizeEmailValue(state?.email),
+ accountIdentifierEmail,
+ normalizeEmailValue(profile?.email),
+ normalizeEmailValue(idPayload?.email),
+ normalizeEmailValue(accessPayload?.email)
+ );
+ const accountId = firstNonEmpty(
+ session?.account?.id,
+ session?.account_id,
+ accessAuth?.chatgpt_account_id,
+ idAuth?.chatgpt_account_id
+ );
+ const userId = firstNonEmpty(
+ session?.user?.id,
+ session?.user_id,
+ accessAuth?.chatgpt_user_id,
+ accessAuth?.user_id,
+ idAuth?.chatgpt_user_id,
+ idAuth?.user_id
+ );
+ const planType = firstNonEmpty(
+ session?.account?.planType,
+ session?.account?.plan_type,
+ session?.planType,
+ session?.plan_type,
+ accessAuth?.chatgpt_plan_type,
+ idAuth?.chatgpt_plan_type
+ );
+ const exportedAt = normalizeTimestamp(options.now || new Date()) || new Date().toISOString();
+ const syntheticIdToken = inputIdToken
+ ? ''
+ : buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt);
+ const idToken = inputIdToken || syntheticIdToken;
+ const authJson = Object.fromEntries(
+ Object.entries({
+ type: 'codex',
+ account_id: accountId,
+ chatgpt_account_id: accountId,
+ email,
+ name: firstNonEmpty(email, state?.email, 'ChatGPT Account'),
+ plan_type: planType,
+ chatgpt_plan_type: planType,
+ id_token: idToken,
+ id_token_synthetic: syntheticIdToken ? true : undefined,
+ access_token: accessToken,
+ refresh_token: refreshToken || '',
+ session_token: sessionToken,
+ last_refresh: exportedAt,
+ expired: expiresAt,
+ disabled: session?.disabled === true ? true : undefined,
+ }).filter(([, value]) => value !== undefined && value !== null && value !== '')
+ );
+
+ return {
+ authJson,
+ accountId,
+ email,
+ expiresAt,
+ fileName: buildCpaAuthFileName({ email, planType, accountId }),
+ hasRefreshToken: Boolean(refreshToken),
+ };
+ }
+
+ async function logWithOptions(message, level = 'info', options = {}) {
+ await addLog(message, level, options.logOptions || {});
+ }
+
+ async function requestOAuthUrl(state, options = {}) {
+ const managementKey = normalizeString(state?.vpsPassword);
+ if (!managementKey) {
+ throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
+ }
+ const origin = deriveCpaManagementOrigin(state?.vpsUrl);
+ const result = await fetchCpaManagementJson(origin, '/v0/management/codex-auth-url', {
+ method: 'GET',
+ managementKey,
+ timeoutMs: options.timeoutMs,
+ });
+ const oauthUrl = firstNonEmpty(
+ result?.url,
+ result?.auth_url,
+ result?.authUrl,
+ result?.data?.url,
+ result?.data?.auth_url,
+ result?.data?.authUrl
+ );
+ const oauthState = firstNonEmpty(
+ result?.state,
+ result?.auth_state,
+ result?.authState,
+ result?.data?.state,
+ result?.data?.auth_state,
+ result?.data?.authState,
+ extractStateFromAuthUrl(oauthUrl)
+ );
+
+ if (!oauthUrl || !oauthUrl.startsWith('http')) {
+ throw new Error('CPA 管理接口未返回有效的 auth_url。');
+ }
+
+ return {
+ oauthUrl,
+ cpaOAuthState: oauthState || null,
+ cpaManagementOrigin: origin,
+ };
+ }
+
+ async function submitOAuthCallback(state, callbackUrl, options = {}) {
+ const managementKey = normalizeString(state?.vpsPassword);
+ if (!managementKey) {
+ throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
+ }
+ const origin = normalizeString(state?.cpaManagementOrigin) || deriveCpaManagementOrigin(state?.vpsUrl);
+ const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', {
+ method: 'POST',
+ managementKey,
+ timeoutMs: options.timeoutMs,
+ body: {
+ provider: 'codex',
+ redirect_url: normalizeString(callbackUrl),
+ },
+ });
+ return {
+ localhostUrl: normalizeString(callbackUrl),
+ verifiedStatus: firstNonEmpty(result?.message, result?.status_message, 'CPA 已通过接口提交回调'),
+ };
+ }
+
+ async function importCurrentChatGptSession(state = {}, options = {}) {
+ const logLabel = normalizeString(options.logLabel) || 'CPA 会话导入';
+ const managementKey = normalizeString(state?.vpsPassword);
+ if (!managementKey) {
+ throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
+ }
+ const origin = deriveCpaManagementOrigin(state?.vpsUrl);
+ const sessionAuth = buildCpaSessionAuthJson(state, options);
+
+ await logWithOptions(`${logLabel}:正在通过 CPA 管理接口导入当前 ChatGPT 会话...`, 'info', options);
+ if (!sessionAuth.hasRefreshToken) {
+ await logWithOptions(`${logLabel}:未包含 refresh_token,access_token 过期后无法自动续期。`, 'warn', options);
+ }
+
+ await fetchCpaManagementJson(origin, `/v0/management/auth-files?name=${encodeURIComponent(sessionAuth.fileName)}`, {
+ method: 'POST',
+ managementKey,
+ timeoutMs: options.importTimeoutMs || options.timeoutMs,
+ body: sessionAuth.authJson,
+ });
+
+ const verifiedStatus = sessionAuth.email
+ ? `CPA 会话导入完成:${sessionAuth.email}`
+ : `CPA 会话导入完成:${sessionAuth.fileName}`;
+ await logWithOptions(verifiedStatus, 'ok', options);
+ return {
+ verifiedStatus,
+ cpaImportedFileName: sessionAuth.fileName,
+ cpaImportedEmail: sessionAuth.email || null,
+ };
+ }
+
+ return {
+ buildCpaSessionAuthJson,
+ deriveCpaManagementOrigin,
+ fetchCpaManagementJson,
+ importCurrentChatGptSession,
+ requestOAuthUrl,
+ submitOAuthCallback,
+ };
+ }
+
+ return {
+ createCpaApi,
+ };
+});
diff --git a/background/message-router.js b/background/message-router.js
index be5fe15..cebebce 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -593,9 +593,14 @@
}
function normalizePlusAccountAccessStrategyForDisplay(value = '') {
- return String(value || '').trim().toLowerCase() === 'sub2api_codex_session'
- ? 'sub2api_codex_session'
- : 'oauth';
+ const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === 'sub2api_codex_session') {
+ return 'sub2api_codex_session';
+ }
+ if (normalized === 'cpa_codex_session') {
+ return 'cpa_codex_session';
+ }
+ return 'oauth';
}
function getPlusAccountAccessStrategyLabel(value = '') {
@@ -604,6 +609,27 @@
: 'OAuth';
}
+ function getPlusAccountAccessStrategyLabel(value = '', targetId = '') {
+ const strategy = normalizePlusAccountAccessStrategyForDisplay(value);
+ const normalizedTargetId = String(targetId || '').trim().toLowerCase();
+ if (strategy === 'sub2api_codex_session') {
+ return '导入当前 ChatGPT 会话到 SUB2API';
+ }
+ if (strategy === 'cpa_codex_session') {
+ return '导入当前 ChatGPT 会话到 CPA';
+ }
+ if (normalizedTargetId === 'cpa') {
+ return '通过 OAuth 回调创建 CPA 账号';
+ }
+ if (normalizedTargetId === 'sub2api') {
+ return '通过 OAuth 回调创建 SUB2API 账号';
+ }
+ if (normalizedTargetId === 'codex2api') {
+ return '通过 OAuth 回调创建 Codex2API 账号';
+ }
+ return 'OAuth';
+ }
+
async function handlePlatformVerifyStepData(payload) {
if (payload.localhostUrl) {
await closeLocalhostCallbackTabs(payload.localhostUrl);
@@ -1561,7 +1587,12 @@
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
);
const selectedPlusAccountAccessStrategy = getPlusAccountAccessStrategyLabel(
- stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth'
+ stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth',
+ stateUpdates.panelMode
+ ?? currentState?.panelMode
+ ?? stateUpdates.openaiIntegrationTargetId
+ ?? currentState?.openaiIntegrationTargetId
+ ?? 'cpa'
);
await addLog(
Boolean(updates.plusModeEnabled)
@@ -1576,7 +1607,12 @@
await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info');
} else if (plusAccountAccessStrategyChanged && nextPlusModeEnabled) {
const selectedPlusAccountAccessStrategy = getPlusAccountAccessStrategyLabel(
- stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth'
+ stateUpdates.plusAccountAccessStrategy ?? currentState?.plusAccountAccessStrategy ?? 'oauth',
+ stateUpdates.panelMode
+ ?? currentState?.panelMode
+ ?? stateUpdates.openaiIntegrationTargetId
+ ?? currentState?.openaiIntegrationTargetId
+ ?? 'cpa'
);
await addLog(`Plus 账号接入策略已切换为 ${selectedPlusAccountAccessStrategy},已更新对应的 Plus 尾链。`, 'info');
}
diff --git a/background/steps/cpa-session-import.js b/background/steps/cpa-session-import.js
new file mode 100644
index 0000000..67891a9
--- /dev/null
+++ b/background/steps/cpa-session-import.js
@@ -0,0 +1,276 @@
+(function attachBackgroundCpaSessionImport(root, factory) {
+ root.MultiPageBackgroundCpaSessionImport = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaSessionImportModule() {
+ const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
+ const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
+
+ function createCpaSessionImportExecutor(deps = {}) {
+ const {
+ addLog: rawAddLog = async () => {},
+ chrome,
+ completeNodeFromBackground,
+ ensureContentScriptReadyOnTabUntilStopped,
+ getTabId,
+ isTabAlive,
+ registerTab,
+ sendTabMessageUntilStopped,
+ sleepWithStop = async () => {},
+ throwIfStopped = () => {},
+ waitForTabCompleteUntilStopped = async () => {},
+ } = deps;
+
+ let cpaApi = null;
+
+ function addStepLog(step, message, level = 'info') {
+ return rawAddLog(message, level, {
+ step,
+ stepKey: 'cpa-session-import',
+ });
+ }
+
+ function getCpaApi() {
+ if (cpaApi) {
+ return cpaApi;
+ }
+ const factory = deps.createCpaApi
+ || self.MultiPageBackgroundCpaApi?.createCpaApi;
+ if (typeof factory !== 'function') {
+ throw new Error('CPA 接口模块未加载,无法导入当前 ChatGPT 会话。');
+ }
+ cpaApi = factory({
+ addLog: rawAddLog,
+ });
+ return cpaApi;
+ }
+
+ 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;
+ }
+ }
+
+ function getSessionTabHostPriority(url = '') {
+ try {
+ const hostname = String(new URL(String(url || '')).hostname || '').trim().toLowerCase();
+ if (/(^|\.)chatgpt\.com$/.test(hostname)) {
+ return 0;
+ }
+ if (hostname === 'chat.openai.com') {
+ return 1;
+ }
+ if (/(^|\.)openai\.com$/.test(hostname)) {
+ return 2;
+ }
+ } catch {
+ return Number.POSITIVE_INFINITY;
+ }
+ return Number.POSITIVE_INFINITY;
+ }
+
+ function getSessionTabActivityPriority(tab = {}) {
+ if (tab?.active && tab?.currentWindow) {
+ return 0;
+ }
+ if (tab?.active) {
+ return 1;
+ }
+ return 2;
+ }
+
+ function pickPreferredSessionTab(tabs = []) {
+ const candidates = (Array.isArray(tabs) ? tabs : [])
+ .filter((tab) => Number.isInteger(tab?.id) && isSupportedChatGptSessionUrl(tab.url));
+ if (!candidates.length) {
+ return null;
+ }
+
+ return candidates.reduce((best, candidate) => {
+ if (!best) {
+ return candidate;
+ }
+
+ const candidateHostPriority = getSessionTabHostPriority(candidate.url);
+ const bestHostPriority = getSessionTabHostPriority(best.url);
+ if (candidateHostPriority !== bestHostPriority) {
+ return candidateHostPriority < bestHostPriority ? candidate : best;
+ }
+
+ const candidateActivityPriority = getSessionTabActivityPriority(candidate);
+ const bestActivityPriority = getSessionTabActivityPriority(best);
+ if (candidateActivityPriority !== bestActivityPriority) {
+ return candidateActivityPriority < bestActivityPriority ? candidate : best;
+ }
+
+ const candidateLastAccessed = Number(candidate?.lastAccessed) || 0;
+ const bestLastAccessed = Number(best?.lastAccessed) || 0;
+ if (candidateLastAccessed !== bestLastAccessed) {
+ return candidateLastAccessed > bestLastAccessed ? candidate : best;
+ }
+
+ return Number(candidate.id) < Number(best.id) ? candidate : best;
+ }, null);
+ }
+
+ async function readSupportedSessionTab(tabId) {
+ const numericTabId = Number(tabId) || 0;
+ if (!numericTabId || !chrome?.tabs?.get) {
+ return null;
+ }
+
+ const tab = await chrome.tabs.get(numericTabId).catch(() => null);
+ return tab?.id && isSupportedChatGptSessionUrl(tab.url)
+ ? tab
+ : null;
+ }
+
+ async function findFallbackSessionTab() {
+ if (!chrome?.tabs?.query) {
+ return null;
+ }
+
+ const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
+ const activeMatch = pickPreferredSessionTab(activeTabs);
+ const allTabs = await chrome.tabs.query({}).catch(() => []);
+ const globalMatch = pickPreferredSessionTab(allTabs);
+ return pickPreferredSessionTab([activeMatch, globalMatch]);
+ }
+
+ 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)) {
+ const registeredTab = await readSupportedSessionTab(registeredTabId);
+ if (registeredTab?.id) {
+ return registeredTab.id;
+ }
+ }
+
+ const storedTabId = Number(state?.plusCheckoutTabId) || 0;
+ const storedTab = await readSupportedSessionTab(storedTabId);
+ if (storedTab?.id) {
+ if (typeof registerTab === 'function') {
+ await registerTab(PLUS_CHECKOUT_SOURCE, storedTab.id);
+ }
+ return storedTab.id;
+ }
+
+ const fallbackTab = await findFallbackSessionTab();
+ if (fallbackTab?.id) {
+ if (typeof registerTab === 'function') {
+ await registerTab(PLUS_CHECKOUT_SOURCE, fallbackTab.id);
+ }
+ return fallbackTab.id;
+ }
+
+ throw new Error('未找到可读取 ChatGPT 会话的标签页,请先打开一个已登录的 ChatGPT / OpenAI 页面,或完成当前 Plus 支付链路。');
+ }
+
+ async function getResolvedSessionTab(tabId, visibleStep) {
+ const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
+ if (!tab?.id) {
+ throw new Error(`步骤 ${visibleStep}:ChatGPT 会话标签页不存在或已关闭,无法继续导入 CPA。`);
+ }
+ 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 executeCpaSessionImport(state = {}) {
+ throwIfStopped();
+ const visibleStep = resolveVisibleStep(state);
+ const api = getCpaApi();
+
+ await addStepLog(visibleStep, '正在定位当前 ChatGPT 会话页并准备导入 CPA...', '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: 'cpa-session-import' },
+ timeoutMs: 120000,
+ importTimeoutMs: 120000,
+ });
+
+ await completeNodeFromBackground(state?.nodeId || 'cpa-session-import', result);
+ }
+
+ return {
+ executeCpaSessionImport,
+ isSupportedChatGptSessionUrl,
+ };
+ }
+
+ return {
+ createCpaSessionImportExecutor,
+ };
+});
diff --git a/background/steps/gopay-manual-confirm.js b/background/steps/gopay-manual-confirm.js
index e9942cf..c7d6f7d 100644
--- a/background/steps/gopay-manual-confirm.js
+++ b/background/steps/gopay-manual-confirm.js
@@ -4,14 +4,28 @@
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const GOPAY_CONFIRM_NODE_ID = 'gopay-subscription-confirm';
const SUB2API_SESSION_IMPORT_NODE_ID = 'sub2api-session-import';
+ const CPA_SESSION_IMPORT_NODE_ID = 'cpa-session-import';
const DEFAULT_CONFIRM_TITLE = 'GoPay 订阅确认';
const OAUTH_CONTINUATION_LABEL = 'OAuth 登录';
const SUB2API_SESSION_CONTINUATION_LABEL = '导入当前 ChatGPT 会话到 SUB2API';
+ const CPA_SESSION_CONTINUATION_LABEL = '导入当前 ChatGPT 会话到 CPA';
+
function normalizeString(value = '') {
return String(value || '').trim();
}
+ function getContinuationActionLabelForNodeId(nodeId = '') {
+ const normalizedNodeId = normalizeString(nodeId);
+ if (normalizedNodeId === SUB2API_SESSION_IMPORT_NODE_ID) {
+ return SUB2API_SESSION_CONTINUATION_LABEL;
+ }
+ if (normalizedNodeId === CPA_SESSION_IMPORT_NODE_ID) {
+ return CPA_SESSION_CONTINUATION_LABEL;
+ }
+ return OAUTH_CONTINUATION_LABEL;
+ }
+
function getContinuationActionLabel(state = {}, options = {}) {
const { getNodeIdsForState = null } = options;
if (typeof getNodeIdsForState === 'function') {
@@ -24,11 +38,16 @@
? 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;
+ if (nextNodeId) {
+ return getContinuationActionLabelForNodeId(nextNodeId);
+ }
+ if (currentNodeIndex < 0) {
+ if (nodeIds.includes(SUB2API_SESSION_IMPORT_NODE_ID)) {
+ return SUB2API_SESSION_CONTINUATION_LABEL;
+ }
+ if (nodeIds.includes(CPA_SESSION_IMPORT_NODE_ID)) {
+ return CPA_SESSION_CONTINUATION_LABEL;
+ }
}
}
return OAUTH_CONTINUATION_LABEL;
diff --git a/data/step-definitions.js b/data/step-definitions.js
index 7a6048f..75c495c 100644
--- a/data/step-definitions.js
+++ b/data/step-definitions.js
@@ -7,6 +7,7 @@
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_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const PLUS_PAYMENT_STEP_KEY = 'paypal-approve';
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
@@ -110,21 +111,51 @@
];
}
- 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 createCpaSessionImportTail(startId, startOrder) {
+ const id = Number(startId) || 10;
+ const order = Number(startOrder) || id * 10;
+ return [
+ {
+ id,
+ order,
+ key: 'cpa-session-import',
+ title: '导入当前 ChatGPT 会话到 CPA',
+ sourceId: 'vps-panel',
+ driverId: 'background/cpa-session-import',
+ command: 'cpa-session-import',
+ },
+ ];
}
- function shouldUseSub2ApiSessionImportTail(options = {}, signupMethod = SIGNUP_METHOD_EMAIL) {
- return signupMethod === SIGNUP_METHOD_EMAIL
- && normalizePlusAccountAccessStrategy(options?.plusAccountAccessStrategy)
- === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION;
+ function normalizePlusAccountAccessStrategy(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION;
+ }
+ if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
+ }
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
+ }
+
+ function resolvePlusSessionImportTail(options = {}, signupMethod = SIGNUP_METHOD_EMAIL) {
+ if (signupMethod !== SIGNUP_METHOD_EMAIL) {
+ return null;
+ }
+ const strategy = normalizePlusAccountAccessStrategy(options?.plusAccountAccessStrategy);
+ if (strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return createSub2ApiSessionImportTail;
+ }
+ if (strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return createCpaSessionImportTail;
+ }
+ return null;
}
function createOpenAiSteps(prefixSteps, startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL, options = {}) {
- const tailSteps = shouldUseSub2ApiSessionImportTail(options, signupMethod)
- ? createSub2ApiSessionImportTail(startId, startOrder)
+ const sessionTailFactory = resolvePlusSessionImportTail(options, signupMethod);
+ const tailSteps = sessionTailFactory
+ ? sessionTailFactory(startId, startOrder)
: createOpenAiAuthTail(startId, startOrder, signupMethod, options);
return [
...prefixSteps,
@@ -143,6 +174,13 @@
SIGNUP_METHOD_EMAIL,
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
);
+ const PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
+ PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS,
+ 10,
+ 100,
+ SIGNUP_METHOD_EMAIL,
+ { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_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);
@@ -153,6 +191,13 @@
SIGNUP_METHOD_EMAIL,
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
);
+ const PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
+ PLUS_GOPAY_PREFIX_STEP_DEFINITIONS,
+ 10,
+ 100,
+ SIGNUP_METHOD_EMAIL,
+ { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_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);
@@ -163,6 +208,13 @@
SIGNUP_METHOD_EMAIL,
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
);
+ const PLUS_GPC_CPA_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
+ PLUS_GPC_PREFIX_STEP_DEFINITIONS,
+ 10,
+ 100,
+ SIGNUP_METHOD_EMAIL,
+ { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_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 = [
@@ -308,6 +360,9 @@
if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
return PLUS_GPC_SUB2API_SESSION_STEP_DEFINITIONS;
}
+ if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_GPC_CPA_SESSION_STEP_DEFINITIONS;
+ }
return PLUS_GPC_STEP_DEFINITIONS;
}
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY) {
@@ -319,6 +374,9 @@
if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
return PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS;
}
+ if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS;
+ }
return PLUS_GOPAY_STEP_DEFINITIONS;
}
if (signupMethod === SIGNUP_METHOD_PHONE) {
@@ -329,6 +387,9 @@
if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
return PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS;
}
+ if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS;
+ }
return PLUS_PAYPAL_STEP_DEFINITIONS;
}
@@ -364,14 +425,17 @@
...NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
...PLUS_PAYPAL_STEP_DEFINITIONS,
...PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_CPA_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_CPA_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_CPA_SESSION_STEP_DEFINITIONS,
...PLUS_GPC_PHONE_STEP_DEFINITIONS,
...PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
]) {
diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js
index 67eb5dc..a1fd7d1 100644
--- a/shared/flow-capabilities.js
+++ b/shared/flow-capabilities.js
@@ -10,6 +10,7 @@
const SIGNUP_METHOD_PHONE = 'phone';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+ const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const VALID_OPENAI_TARGET_IDS = Array.isArray(flowRegistryApi.OPENAI_TARGET_IDS)
? flowRegistryApi.OPENAI_TARGET_IDS.slice()
: ['cpa', 'sub2api', 'codex2api'];
@@ -71,7 +72,10 @@
cpa: Object.freeze({
supportsPhoneSignup: true,
requiresPhoneSignupWarning: true,
- supportedPlusAccountAccessStrategies: Object.freeze([PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]),
+ supportedPlusAccountAccessStrategies: Object.freeze([
+ PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH,
+ PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
+ ]),
}),
sub2api: Object.freeze({
supportsPhoneSignup: true,
@@ -127,9 +131,14 @@
}
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;
+ const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION;
+ }
+ if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
+ }
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
}
function normalizeOpenAiTargetList(values = []) {
@@ -618,6 +627,7 @@
OPENAI_TARGET_CAPABILITIES,
PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH,
PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
+ PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
SIGNUP_METHOD_EMAIL,
SIGNUP_METHOD_PHONE,
VALID_OPENAI_TARGET_IDS,
diff --git a/shared/settings-schema.js b/shared/settings-schema.js
index 6e6dadf..2f39824 100644
--- a/shared/settings-schema.js
+++ b/shared/settings-schema.js
@@ -47,11 +47,16 @@
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'
- );
+ const normalizePlusAccountAccessStrategy = (value = '') => {
+ const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === 'sub2api_codex_session') {
+ return 'sub2api_codex_session';
+ }
+ if (normalized === 'cpa_codex_session') {
+ return 'cpa_codex_session';
+ }
+ return 'oauth';
+ };
function buildDefaultSettingsState() {
return {
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index cc178fa..c4c87b3 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -306,6 +306,7 @@
当前来源仅支持 OAuth
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index b50665c..624d604 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -547,6 +547,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
@@ -2853,9 +2854,13 @@ function normalizePlusPaymentMethod(value = '') {
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;
+ if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION;
+ }
+ if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
+ }
+ return PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
}
function getSelectedPlusPaymentMethod(state = latestState) {
@@ -2886,6 +2891,58 @@ function getRequestedPlusAccountAccessStrategy(state = latestState) {
return fallbackStrategy;
}
+function normalizePlusStrategyTargetId(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === 'sub2api') {
+ return 'sub2api';
+ }
+ if (normalized === 'codex2api') {
+ return 'codex2api';
+ }
+ return 'cpa';
+}
+
+function getPlusAccountAccessStrategyContinuationLabel(strategy = '', targetId = '') {
+ const normalizedStrategy = normalizePlusAccountAccessStrategy(strategy);
+ if (normalizedStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return '导入当前 ChatGPT 会话到 SUB2API';
+ }
+ if (normalizedStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return '导入当前 ChatGPT 会话到 CPA';
+ }
+ return 'OAuth 登录';
+}
+
+function getPlusAccountAccessStrategyDescription(strategy = '', targetId = '') {
+ const normalizedStrategy = normalizePlusAccountAccessStrategy(strategy);
+ const normalizedTargetId = normalizePlusStrategyTargetId(targetId);
+ if (normalizedStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return '复用当前 Plus 已登录会话,直接导入到 SUB2API';
+ }
+ if (normalizedStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return '复用当前 Plus 已登录会话,直接导入到 CPA';
+ }
+ if (normalizedTargetId === 'sub2api') {
+ return '通过 OAuth 回调创建 SUB2API 账号';
+ }
+ if (normalizedTargetId === 'codex2api') {
+ return '通过 OAuth 回调创建 Codex2API 账号';
+ }
+ return '通过 OAuth 回调创建 CPA 账号';
+}
+
+function resolvePlusManualContinuationActionLabelFromState(state = latestState) {
+ const activeFlowId = String(state?.activeFlowId || state?.flowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase();
+ const signupMethod = normalizeSignupMethod(state?.resolvedSignupMethod || state?.signupMethod || DEFAULT_SIGNUP_METHOD);
+ const plusModeEnabled = state?.plusModeEnabled === undefined ? true : Boolean(state?.plusModeEnabled);
+ const targetId = normalizePlusStrategyTargetId(state?.panelMode || state?.openaiIntegrationTargetId || 'cpa');
+ const strategy = normalizePlusAccountAccessStrategy(state?.plusAccountAccessStrategy || DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY);
+ const effectiveStrategy = plusModeEnabled && activeFlowId === DEFAULT_ACTIVE_FLOW_ID && signupMethod === SIGNUP_METHOD_EMAIL
+ ? strategy
+ : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
+ return getPlusAccountAccessStrategyContinuationLabel(effectiveStrategy, targetId);
+}
+
function normalizeGpcHelperPhoneModeValue(value = '') {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
@@ -9000,7 +9057,41 @@ function updatePlusModeUI() {
const sub2apiSessionStrategyValue = typeof PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION !== 'undefined'
? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
: 'sub2api_codex_session';
+ const cpaSessionStrategyValue = typeof PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION !== 'undefined'
+ ? PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION
+ : 'cpa_codex_session';
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : paypalValue;
+ const resolveStrategyTargetId = typeof normalizePlusStrategyTargetId === 'function'
+ ? normalizePlusStrategyTargetId
+ : ((value = '') => {
+ const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === 'sub2api') {
+ return 'sub2api';
+ }
+ if (normalized === 'codex2api') {
+ return 'codex2api';
+ }
+ return 'cpa';
+ });
+ const describePlusAccountAccessStrategy = typeof getPlusAccountAccessStrategyDescription === 'function'
+ ? getPlusAccountAccessStrategyDescription
+ : ((strategy = '', targetId = '') => {
+ const normalizedStrategy = normalizePlusAccountAccessStrategy(strategy);
+ const normalizedTargetId = resolveStrategyTargetId(targetId);
+ if (normalizedStrategy === sub2apiSessionStrategyValue) {
+ return '复用当前 Plus 已登录会话,直接导入到 SUB2API';
+ }
+ if (normalizedStrategy === cpaSessionStrategyValue) {
+ return '复用当前 Plus 已登录会话,直接导入到 CPA';
+ }
+ if (normalizedTargetId === 'sub2api') {
+ return '通过 OAuth 回调创建 SUB2API 账号';
+ }
+ if (normalizedTargetId === 'codex2api') {
+ return '通过 OAuth 回调创建 Codex2API 账号';
+ }
+ return '通过 OAuth 回调创建 CPA 账号';
+ });
const requestedPlusAccountAccessStrategy = typeof getRequestedPlusAccountAccessStrategy === 'function'
? getRequestedPlusAccountAccessStrategy(latestState)
: normalizePlusAccountAccessStrategy(latestState?.plusAccountAccessStrategy || DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY);
@@ -9038,9 +9129,20 @@ function updatePlusModeUI() {
: true;
const enabled = supportsPlusMode && rawEnabled;
const canEditPlusAccountAccessStrategy = Boolean(capabilityState?.canEditPlusAccountAccessStrategy);
+ const availablePlusAccountAccessStrategies = Array.isArray(capabilityState?.availablePlusAccountAccessStrategies)
+ && capabilityState.availablePlusAccountAccessStrategies.length > 0
+ ? capabilityState.availablePlusAccountAccessStrategies
+ : [oauthStrategyValue];
const effectivePlusAccountAccessStrategy = capabilityState?.effectivePlusAccountAccessStrategy
|| requestedPlusAccountAccessStrategy
|| oauthStrategyValue;
+ const effectiveTargetId = resolveStrategyTargetId(
+ capabilityState?.effectivePanelMode
+ || capabilityState?.effectiveTargetId
+ || capabilityState?.panelMode
+ || (typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : latestState?.panelMode)
+ || 'cpa'
+ );
const method = enabled ? getSelectedPlusPaymentMethod() : defaultMethod;
const gpcPhoneMode = normalizeGpcHelperPhoneModeValue(
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
@@ -9103,6 +9205,13 @@ function updatePlusModeUI() {
row.style.display = enabled ? '' : 'none';
});
if (typeof selectPlusAccountAccessStrategy !== 'undefined' && selectPlusAccountAccessStrategy) {
+ const availableStrategySet = new Set(availablePlusAccountAccessStrategies);
+ Array.from(selectPlusAccountAccessStrategy.options || []).forEach((option) => {
+ const optionValue = normalizePlusAccountAccessStrategy(option?.value || '');
+ const optionSupported = availableStrategySet.has(optionValue);
+ option.hidden = enabled ? !optionSupported : false;
+ option.disabled = enabled ? !optionSupported : false;
+ });
selectPlusAccountAccessStrategy.dataset.requestedValue = requestedPlusAccountAccessStrategy;
selectPlusAccountAccessStrategy.value = effectivePlusAccountAccessStrategy;
selectPlusAccountAccessStrategy.disabled = !enabled || !canEditPlusAccountAccessStrategy;
@@ -9121,6 +9230,24 @@ function updatePlusModeUI() {
plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
}
}
+ if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
+ if (!enabled || !canEditPlusAccountAccessStrategy) {
+ plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
+ } else {
+ plusAccountAccessStrategyCaption.textContent = describePlusAccountAccessStrategy(
+ effectivePlusAccountAccessStrategy,
+ effectiveTargetId
+ );
+ }
+ }
+ if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
+ plusAccountAccessStrategyCaption.textContent = !enabled || !canEditPlusAccountAccessStrategy
+ ? '当前来源仅支持 OAuth'
+ : describePlusAccountAccessStrategy(
+ effectivePlusAccountAccessStrategy,
+ effectiveTargetId
+ );
+ }
[
typeof rowPayPalAccount !== 'undefined' ? rowPayPalAccount : null,
].forEach((row) => {
@@ -9649,6 +9776,60 @@ async function syncPlusManualConfirmationDialog() {
}
}
}
+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 continuationActionLabel = resolvePlusManualContinuationActionLabelFromState(latestState);
+ if (method === 'gopay-otp') {
+ if (!sharedFormDialog?.open) {
+ return null;
+ }
+ const result = await sharedFormDialog.open({
+ title: String(options.title || '').trim() || 'GPC OTP 验证',
+ message: String(options.message || '').trim() || '请在WhatsApp里面获取验证码(耐心等待三十秒左右)',
+ fields: [
+ {
+ key: 'otp',
+ label: 'OTP',
+ type: 'text',
+ placeholder: '请输入 OTP 验证码',
+ inputMode: 'numeric',
+ autocomplete: 'one-time-code',
+ required: true,
+ requiredMessage: '请输入 OTP 验证码。',
+ normalize: (value) => String(value || '').trim().replace(/[^\d]/g, ''),
+ validate: (value) => {
+ const normalized = String(value || '').trim().replace(/[^\d]/g, '');
+ if (!normalized) return '请输入 OTP 验证码。';
+ if (!/^\d{6}$/.test(normalized)) return 'OTP 必须是 6 位数字,请检查。';
+ return '';
+ },
+ },
+ ],
+ confirmLabel: '提交 OTP',
+ });
+ return result
+ ? { action: 'confirm', otp: String(result.otp || '').trim().replace(/[^\d]/g, '') }
+ : { action: 'cancel' };
+ }
+
+ const title = String(options.title || '').trim() || (method === gopayValue ? 'GoPay subscription confirmation' : 'Manual confirmation');
+ const message = String(options.message || '').trim()
+ || (method === gopayValue
+ ? 'Complete the GoPay subscription on the current page, then continue.'
+ : 'Finish the current manual action on the page, then continue.');
+ return openActionModal({
+ title,
+ message,
+ actions: [
+ { id: 'cancel', label: 'Cancel', variant: 'btn-ghost' },
+ { id: 'confirm', label: 'Continue', variant: 'btn-primary' },
+ ],
+ alert: method === gopayValue
+ ? { text: `After confirmation, the Plus flow will continue with ${continuationActionLabel}.`, tone: 'info' }
+ : null,
+ });
+}
async function clearRegistrationEmail(options = {}) {
const { silent = false } = options;
diff --git a/tests/background-account-history-settings.test.js b/tests/background-account-history-settings.test.js
index dcb40ab..ddc5d2d 100644
--- a/tests/background-account-history-settings.test.js
+++ b/tests/background-account-history-settings.test.js
@@ -55,6 +55,7 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneMode'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizePhoneSmsProviderOrder'),
@@ -125,6 +126,10 @@ const DEFAULT_ACTIVE_FLOW_ID = 'openai';
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_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const FIVE_SIM_COUNTRY_ID = 'vietnam';
@@ -228,6 +233,7 @@ return {
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', 'cpa_codex_session'), 'cpa_codex_session');
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'unknown'), 'oauth');
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/checkout/start '),
diff --git a/tests/background-auth-chain-guard.test.js b/tests/background-auth-chain-guard.test.js
index 0aecc9e..a013aa0 100644
--- a/tests/background-auth-chain-guard.test.js
+++ b/tests/background-auth-chain-guard.test.js
@@ -48,13 +48,14 @@ function extractFunction(name) {
return source.slice(start, end);
}
-test('background auth chain set does not include SUB2API session import node', () => {
+test('background auth chain set does not include Plus session import nodes', () => {
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/);
+ assert.doesNotMatch(authChainBlock, /cpa-session-import/);
});
const NODE_EXECUTE_COMPAT_HELPERS = `
diff --git a/tests/background-cpa-api.test.js b/tests/background-cpa-api.test.js
new file mode 100644
index 0000000..df4865e
--- /dev/null
+++ b/tests/background-cpa-api.test.js
@@ -0,0 +1,184 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+function createJsonResponse(payload, status = 200) {
+ return {
+ ok: status >= 200 && status < 300,
+ status,
+ json: async () => payload,
+ };
+}
+
+function loadCpaApiModule() {
+ const source = fs.readFileSync('background/cpa-api.js', 'utf8');
+ return new Function('self', `${source}; return self.MultiPageBackgroundCpaApi;`)({});
+}
+
+function encodeBase64UrlJson(value) {
+ return Buffer.from(JSON.stringify(value)).toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/g, '');
+}
+
+function createJwtToken(payload = {}) {
+ return [
+ encodeBase64UrlJson({ alg: 'HS256', typ: 'JWT' }),
+ encodeBase64UrlJson(payload),
+ 'signature',
+ ].join('.');
+}
+
+test('cpa api imports current ChatGPT session through management auth-files endpoint', async () => {
+ const apiModule = loadCpaApiModule();
+ const logs = [];
+ const fetchCalls = [];
+ const expiresAt = '2026-05-20T12:34:56.000Z';
+ const accessToken = createJwtToken({
+ exp: Math.floor(Date.parse(expiresAt) / 1000),
+ email: 'jwt@example.com',
+ 'https://api.openai.com/auth': {
+ chatgpt_account_id: 'acct_123',
+ chatgpt_plan_type: 'plus',
+ chatgpt_user_id: 'user_123',
+ },
+ 'https://api.openai.com/profile': {
+ email: 'profile@example.com',
+ },
+ });
+
+ const api = apiModule.createCpaApi({
+ addLog: async (message, level = 'info', options = {}) => {
+ logs.push({ message, level, step: options.step, stepKey: options.stepKey });
+ },
+ 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 || 'POST',
+ headers: options.headers || {},
+ body,
+ });
+
+ if (parsed.pathname === '/v0/management/auth-files') {
+ return createJsonResponse({});
+ }
+
+ return createJsonResponse({ message: `unexpected path ${parsed.pathname}` }, 404);
+ },
+ });
+
+ const result = await api.importCurrentChatGptSession({
+ vpsUrl: 'https://cpa.example.com/management.html#/oauth',
+ vpsPassword: 'management-key',
+ session: {
+ accessToken,
+ expires: expiresAt,
+ user: {
+ email: 'flow@example.com',
+ },
+ account: {
+ id: 'acct_123',
+ planType: 'plus',
+ },
+ },
+ accessToken,
+ email: 'registration@example.com',
+ accountIdentifierType: 'email',
+ accountIdentifier: 'identifier@example.com',
+ }, {
+ logLabel: '步骤 10',
+ logOptions: { step: 10, stepKey: 'cpa-session-import' },
+ });
+
+ const importCall = fetchCalls.find((call) => call.path === '/v0/management/auth-files');
+ assert.ok(importCall, 'expected CPA auth-files import call');
+ assert.equal(importCall.method, 'POST');
+ assert.equal(importCall.headers.Authorization, 'Bearer management-key');
+ assert.equal(importCall.headers['X-Management-Key'], 'management-key');
+ assert.equal(
+ decodeURIComponent(new URLSearchParams(importCall.search).get('name')),
+ 'codex-flow@example.com-plus.json'
+ );
+ assert.equal(importCall.body.type, 'codex');
+ assert.equal(importCall.body.account_id, 'acct_123');
+ assert.equal(importCall.body.chatgpt_account_id, 'acct_123');
+ assert.equal(importCall.body.email, 'flow@example.com');
+ assert.equal(importCall.body.plan_type, 'plus');
+ assert.equal(importCall.body.chatgpt_plan_type, 'plus');
+ assert.equal(importCall.body.access_token, accessToken);
+ assert.equal(importCall.body.id_token_synthetic, true);
+ assert.match(String(importCall.body.id_token || ''), /\.synthetic$/);
+ assert.equal(importCall.body.expired, expiresAt);
+ assert.equal(Object.prototype.hasOwnProperty.call(importCall.body, 'refresh_token'), false);
+ assert.equal(result.verifiedStatus, 'CPA 会话导入完成:flow@example.com');
+ assert.equal(
+ logs.some((entry) => entry.level === 'warn' && /refresh_token/.test(entry.message)),
+ true
+ );
+});
+
+test('cpa api falls back to registration email when session has no readable email', () => {
+ const apiModule = loadCpaApiModule();
+ const accessToken = createJwtToken({
+ exp: Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000),
+ 'https://api.openai.com/auth': {
+ chatgpt_account_id: 'acct_456',
+ chatgpt_plan_type: 'plus',
+ chatgpt_user_id: 'user_456',
+ },
+ });
+
+ const api = apiModule.createCpaApi();
+ const result = api.buildCpaSessionAuthJson({
+ session: {
+ accessToken,
+ user: {
+ name: 'Flow User',
+ },
+ },
+ accessToken,
+ email: 'registration@example.com',
+ accountIdentifierType: 'email',
+ accountIdentifier: 'identifier@example.com',
+ });
+
+ assert.equal(result.email, 'registration@example.com');
+ assert.equal(result.authJson.email, 'registration@example.com');
+ assert.equal(result.fileName, 'codex-registration@example.com-plus.json');
+});
+
+test('cpa api preserves provided id_token and refresh_token when available', () => {
+ const apiModule = loadCpaApiModule();
+ const accessToken = createJwtToken({
+ exp: Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000),
+ 'https://api.openai.com/auth': {
+ chatgpt_account_id: 'acct_789',
+ chatgpt_plan_type: 'plus',
+ chatgpt_user_id: 'user_789',
+ },
+ });
+
+ const api = apiModule.createCpaApi();
+ const result = api.buildCpaSessionAuthJson({
+ session: {
+ accessToken,
+ user: {
+ email: 'session@example.com',
+ },
+ },
+ accessToken,
+ id_token: 'provided-id-token',
+ refresh_token: 'refresh-token-1',
+ session_token: 'session-token-1',
+ });
+
+ assert.equal(result.authJson.id_token, 'provided-id-token');
+ assert.equal(Object.prototype.hasOwnProperty.call(result.authJson, 'id_token_synthetic'), false);
+ assert.equal(result.authJson.refresh_token, 'refresh-token-1');
+ assert.equal(result.authJson.session_token, 'session-token-1');
+ assert.equal(result.hasRefreshToken, true);
+});
diff --git a/tests/background-cpa-session-import.test.js b/tests/background-cpa-session-import.test.js
new file mode 100644
index 0000000..b73c658
--- /dev/null
+++ b/tests/background-cpa-session-import.test.js
@@ -0,0 +1,247 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const test = require('node:test');
+
+function loadCpaSessionImportModule() {
+ const source = fs.readFileSync('background/steps/cpa-session-import.js', 'utf8');
+ return new Function('self', `${source}; return self.MultiPageBackgroundCpaSessionImport;`)({});
+}
+
+test('CPA session import step reads current ChatGPT session and completes node', async () => {
+ const moduleApi = loadCpaSessionImportModule();
+ const completed = [];
+ const logs = [];
+ const ensureCalls = [];
+ const sentMessages = [];
+ const importedPayloads = [];
+
+ const executor = moduleApi.createCpaSessionImportExecutor({
+ 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 });
+ },
+ createCpaApi: () => ({
+ importCurrentChatGptSession: async (state, options) => {
+ importedPayloads.push({ state, options });
+ return {
+ verifiedStatus: 'CPA 会话导入完成:flow@example.com',
+ cpaImportedFileName: 'codex-flow@example.com-plus.json',
+ cpaImportedEmail: 'flow@example.com',
+ };
+ },
+ }),
+ ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options = {}) => {
+ ensureCalls.push({ source, tabId, options });
+ },
+ getTabId: async () => null,
+ isTabAlive: async () => false,
+ 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 () => {},
+ });
+
+ await executor.executeCpaSessionImport({
+ nodeId: 'cpa-session-import',
+ visibleStep: 10,
+ plusCheckoutTabId: 91,
+ vpsUrl: 'https://cpa.example.com/management.html#/oauth',
+ vpsPassword: 'management-key',
+ });
+
+ 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: 'cpa-session-import',
+ payload: {
+ verifiedStatus: 'CPA 会话导入完成:flow@example.com',
+ cpaImportedFileName: 'codex-flow@example.com-plus.json',
+ cpaImportedEmail: 'flow@example.com',
+ },
+ });
+ assert.equal(
+ logs.some((entry) => entry.stepKey === 'cpa-session-import' && /ChatGPT/.test(entry.message)),
+ true
+ );
+});
+
+test('CPA session import step falls back to an active ChatGPT tab when no checkout tab is tracked', async () => {
+ const moduleApi = loadCpaSessionImportModule();
+ const completed = [];
+ const importedPayloads = [];
+ const queryCalls = [];
+ const registerCalls = [];
+
+ const sessionTab = {
+ id: 77,
+ url: 'https://chatgpt.com/?model=gpt-4o',
+ active: true,
+ currentWindow: true,
+ lastAccessed: 1234,
+ };
+
+ const executor = moduleApi.createCpaSessionImportExecutor({
+ addLog: async () => {},
+ chrome: {
+ tabs: {
+ get: async (tabId) => ({
+ ...sessionTab,
+ id: tabId,
+ }),
+ query: async (queryInfo = {}) => {
+ queryCalls.push(queryInfo);
+ if (queryInfo.active && queryInfo.currentWindow) {
+ return [sessionTab];
+ }
+ return [sessionTab];
+ },
+ update: async () => {},
+ },
+ },
+ completeNodeFromBackground: async (nodeId, payload) => {
+ completed.push({ nodeId, payload });
+ },
+ createCpaApi: () => ({
+ importCurrentChatGptSession: async (state) => {
+ importedPayloads.push(state);
+ return {
+ verifiedStatus: 'CPA 会话导入完成:fallback@example.com',
+ };
+ },
+ }),
+ ensureContentScriptReadyOnTabUntilStopped: async () => {},
+ getTabId: async () => null,
+ isTabAlive: async () => false,
+ registerTab: async (source, tabId) => {
+ registerCalls.push({ source, tabId });
+ },
+ sendTabMessageUntilStopped: async () => ({
+ session: {
+ accessToken: 'session-access-token',
+ user: {
+ email: 'fallback@example.com',
+ },
+ },
+ accessToken: 'session-access-token',
+ }),
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ waitForTabCompleteUntilStopped: async () => {},
+ });
+
+ await executor.executeCpaSessionImport({
+ nodeId: 'cpa-session-import',
+ visibleStep: 10,
+ vpsUrl: 'https://cpa.example.com/management.html#/oauth',
+ vpsPassword: 'management-key',
+ });
+
+ assert.deepStrictEqual(queryCalls, [
+ { active: true, currentWindow: true },
+ {},
+ ]);
+ assert.deepStrictEqual(registerCalls, [{
+ source: 'plus-checkout',
+ tabId: 77,
+ }]);
+ assert.equal(importedPayloads.length, 1);
+ assert.equal(importedPayloads[0].session.user.email, 'fallback@example.com');
+ assert.equal(completed.length, 1);
+});
+
+test('CPA session import step reports missing readable session tab when tracked tabs are unusable', async () => {
+ const moduleApi = loadCpaSessionImportModule();
+ let sendCalled = false;
+
+ const executor = moduleApi.createCpaSessionImportExecutor({
+ addLog: async () => {},
+ chrome: {
+ tabs: {
+ get: async (tabId) => ({
+ id: tabId,
+ url: 'https://example.com/not-chatgpt',
+ }),
+ update: async () => {},
+ },
+ },
+ completeNodeFromBackground: async () => {},
+ createCpaApi: () => ({
+ importCurrentChatGptSession: async () => ({}),
+ }),
+ ensureContentScriptReadyOnTabUntilStopped: async () => {},
+ getTabId: async () => 91,
+ isTabAlive: async () => true,
+ sendTabMessageUntilStopped: async () => {
+ sendCalled = true;
+ return {};
+ },
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ waitForTabCompleteUntilStopped: async () => {},
+ });
+
+ await assert.rejects(
+ () => executor.executeCpaSessionImport({
+ nodeId: 'cpa-session-import',
+ visibleStep: 10,
+ vpsUrl: 'https://cpa.example.com/management.html#/oauth',
+ vpsPassword: 'management-key',
+ }),
+ /未找到可读取 ChatGPT 会话的标签页/
+ );
+
+ assert.equal(sendCalled, false);
+});
+
+test('background wires CPA session import executor into the workflow runtime', () => {
+ const source = fs.readFileSync('background.js', 'utf8');
+ assert.match(source, /background\/cpa-api\.js/);
+ assert.match(source, /background\/steps\/cpa-session-import\.js/);
+ assert.match(source, /'cpa-session-import': \(state\) => cpaSessionImportExecutor\.executeCpaSessionImport\(state\)/);
+ assert.match(source, /'cpa-session-import',[\s\S]*'oauth-login'/);
+});
diff --git a/tests/background-effective-plus-account-access-strategy.test.js b/tests/background-effective-plus-account-access-strategy.test.js
index b841976..aae5b48 100644
--- a/tests/background-effective-plus-account-access-strategy.test.js
+++ b/tests/background-effective-plus-account-access-strategy.test.js
@@ -59,14 +59,25 @@ const workflowEngine = null;
const self = {
MultiPageStepDefinitions: {
getSteps(options = {}) {
- return String(options.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
- ? [
+ const strategy = String(options.plusAccountAccessStrategy || '').trim();
+ if (strategy === 'sub2api_codex_session') {
+ return [
{ 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' },
+ ];
+ }
+ return strategy === 'cpa_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: 'cpa-session-import' },
]
: [
{ id: 1, key: 'open-chatgpt' },
@@ -96,17 +107,26 @@ function normalizePlusPaymentMethod(value = '') {
return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
}
function normalizePlusAccountAccessStrategy(value = '') {
- return String(value || '').trim().toLowerCase() === 'sub2api_codex_session'
- ? 'sub2api_codex_session'
- : 'oauth';
+ const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === 'sub2api_codex_session') {
+ return 'sub2api_codex_session';
+ }
+ if (normalized === 'cpa_codex_session') {
+ return 'cpa_codex_session';
+ }
+ return 'oauth';
}
function isPlusModeState(state = {}) {
return Boolean(state?.plusModeEnabled);
}
function resolveCurrentFlowCapabilities(state = {}, options = {}) {
- const effectiveStrategy = String(options.panelMode || '').trim().toLowerCase() === 'sub2api'
- ? normalizePlusAccountAccessStrategy(state.plusAccountAccessStrategy)
- : 'oauth';
+ const normalizedPanelMode = String(options.panelMode || '').trim().toLowerCase();
+ const requestedStrategy = normalizePlusAccountAccessStrategy(state.plusAccountAccessStrategy);
+ const effectiveStrategy = normalizedPanelMode === 'sub2api'
+ ? (requestedStrategy === 'sub2api_codex_session' ? 'sub2api_codex_session' : 'oauth')
+ : (normalizedPanelMode === 'cpa'
+ ? (requestedStrategy === 'cpa_codex_session' ? 'cpa_codex_session' : 'oauth')
+ : 'oauth');
return {
effectivePanelMode: options.panelMode,
effectivePlusAccountAccessStrategy: effectiveStrategy,
@@ -159,6 +179,32 @@ test('background step resolution keeps SUB2API session tail only when the effect
]);
});
+test('background step resolution keeps CPA session tail when the effective Plus target supports it', () => {
+ const api = createHarness();
+ const state = {
+ activeFlowId: 'openai',
+ flowId: 'openai',
+ panelMode: 'cpa',
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'cpa_codex_session',
+ signupMethod: 'email',
+ };
+
+ const resolvedState = api.buildResolvedStepDefinitionState(state);
+ const nodeIds = api.getNodeIdsForState(state);
+
+ assert.equal(resolvedState.plusAccountAccessStrategy, 'cpa_codex_session');
+ assert.deepStrictEqual(nodeIds, [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'paypal-approve',
+ 'plus-checkout-return',
+ 'cpa-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 = {
diff --git a/tests/background-message-router-plus-final-step.test.js b/tests/background-message-router-plus-final-step.test.js
index 4587aa6..f0d0afb 100644
--- a/tests/background-message-router-plus-final-step.test.js
+++ b/tests/background-message-router-plus-final-step.test.js
@@ -173,3 +173,33 @@ test('message router appends success record when SUB2API session import is the f
assert.equal(appendCalls.length, 1);
assert.equal(appendCalls[0][0], 'success');
});
+
+test('message router appends success record when CPA session import is the final Plus node', async () => {
+ const { appendCalls, router } = createRouterWithFinalNode({
+ finalNodeId: 'cpa-session-import',
+ nodeIds: [
+ 'open-chatgpt',
+ 'plus-checkout-create',
+ 'plus-checkout-billing',
+ 'paypal-approve',
+ 'plus-checkout-return',
+ 'cpa-session-import',
+ ],
+ nodeStepMap: {
+ 'plus-checkout-create': 6,
+ 'plus-checkout-billing': 7,
+ 'paypal-approve': 8,
+ 'plus-checkout-return': 9,
+ 'cpa-session-import': 10,
+ },
+ });
+
+ await router.handleMessage({
+ type: 'NODE_COMPLETE',
+ nodeId: 'cpa-session-import',
+ payload: { nodeId: 'cpa-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 4a4bee5..d7bd25c 100644
--- a/tests/background-settings-schema-persistence.test.js
+++ b/tests/background-settings-schema-persistence.test.js
@@ -111,6 +111,10 @@ const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
const PERSISTED_SETTINGS_SCHEMA_KEYS = ['settingsSchemaVersion', 'settingsState'];
const LEGACY_AUTO_STEP_DELAY_KEYS = [];
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = [];
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
function isPlainObjectValue(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
@@ -125,6 +129,7 @@ function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
}
+${extractFunction('normalizePlusAccountAccessStrategy')}
function normalizeSub2ApiGroupNames(value) {
return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
}
diff --git a/tests/background-sub2api-session-import.test.js b/tests/background-sub2api-session-import.test.js
index f5a8783..8c486b1 100644
--- a/tests/background-sub2api-session-import.test.js
+++ b/tests/background-sub2api-session-import.test.js
@@ -601,5 +601,5 @@ 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'/);
+ assert.match(source, /'sub2api-session-import',[\s\S]*'oauth-login'/);
});
diff --git a/tests/flow-capabilities-module.test.js b/tests/flow-capabilities-module.test.js
index c85c6f3..db05a61 100644
--- a/tests/flow-capabilities-module.test.js
+++ b/tests/flow-capabilities-module.test.js
@@ -216,7 +216,7 @@ test('flow capability registry exposes editable Plus account access strategies f
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'sub2api_codex_session');
});
-test('flow capability registry preserves requested Plus account strategy while forcing OAuth on unsupported targets', () => {
+test('flow capability registry exposes editable Plus account access strategies for CPA', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry();
@@ -226,12 +226,36 @@ test('flow capability registry preserves requested Plus account strategy while f
openaiIntegrationTargetId: 'cpa',
signupMethod: 'email',
plusModeEnabled: true,
- plusAccountAccessStrategy: 'sub2api_codex_session',
+ plusAccountAccessStrategy: 'cpa_codex_session',
+ },
+ });
+
+ assert.deepEqual(
+ capabilityState.availablePlusAccountAccessStrategies,
+ ['oauth', 'cpa_codex_session']
+ );
+ assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
+ assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'cpa_codex_session');
+ assert.equal(capabilityState.canEditPlusAccountAccessStrategy, true);
+ assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'cpa_codex_session');
+});
+
+test('flow capability registry forces OAuth when the current target only supports OAuth', () => {
+ const api = loadApi();
+ const registry = api.createFlowCapabilityRegistry();
+
+ const capabilityState = registry.resolveSidepanelCapabilities({
+ state: {
+ activeFlowId: 'openai',
+ openaiIntegrationTargetId: 'codex2api',
+ signupMethod: 'email',
+ plusModeEnabled: true,
+ plusAccountAccessStrategy: 'cpa_codex_session',
},
});
assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['oauth']);
- assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_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 95fe10f..f91be78 100644
--- a/tests/flow-registry-settings-schema.test.js
+++ b/tests/flow-registry-settings-schema.test.js
@@ -99,3 +99,15 @@ test('settings schema can project canonical state into a read view without legac
assert.equal(view.settingsSchemaVersion, 4);
assert.equal(view.settingsState.activeFlowId, 'kiro');
});
+
+test('settings schema preserves CPA session strategy in canonical state and read view', () => {
+ const { settingsSchema } = loadApis();
+ const schema = settingsSchema.createSettingsSchema();
+ const normalized = schema.normalizeSettingsState({
+ plusAccountAccessStrategy: 'cpa_codex_session',
+ });
+ const view = schema.buildSettingsView(normalized);
+
+ assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'cpa_codex_session');
+ assert.equal(view.plusAccountAccessStrategy, 'cpa_codex_session');
+});
diff --git a/tests/gopay-manual-confirm.test.js b/tests/gopay-manual-confirm.test.js
index 11384ac..b116083 100644
--- a/tests/gopay-manual-confirm.test.js
+++ b/tests/gopay-manual-confirm.test.js
@@ -50,7 +50,7 @@ test('GoPay manual confirm executor publishes pending manual confirmation state
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.match(stateUpdates[0].plusManualConfirmationMessage, /OAuth/);
assert.equal(broadcasts.length, 1);
assert.equal(broadcasts[0].plusManualConfirmationPending, true);
assert.deepStrictEqual(
@@ -59,7 +59,7 @@ test('GoPay manual confirm executor publishes pending manual confirmation state
);
assert.match(
events.find((event) => event.type === 'log')?.message || '',
- /确认后继续OAuth 登录/
+ /OAuth/
);
});
@@ -100,9 +100,53 @@ test('GoPay manual confirm executor switches continuation copy to SUB2API sessio
assert.equal(stateUpdates.length, 1);
assert.equal(stateUpdates[0].plusManualConfirmationStep, 9);
- assert.match(stateUpdates[0].plusManualConfirmationMessage, /导入当前 ChatGPT 会话到 SUB2API/);
+ assert.match(stateUpdates[0].plusManualConfirmationMessage, /SUB2API/);
assert.match(
events.find((event) => event.type === 'log')?.message || '',
- /确认后继续导入当前 ChatGPT 会话到 SUB2API/
+ /SUB2API/
+ );
+});
+
+test('GoPay manual confirm executor switches continuation copy to CPA session import when the effective tail is CPA 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',
+ 'cpa-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, /CPA/);
+ assert.match(
+ events.find((event) => event.type === 'log')?.message || '',
+ /CPA/
);
});
diff --git a/tests/plus-account-access-strategy.test.js b/tests/plus-account-access-strategy.test.js
index 84fe8d0..3998edc 100644
--- a/tests/plus-account-access-strategy.test.js
+++ b/tests/plus-account-access-strategy.test.js
@@ -57,6 +57,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
@@ -165,6 +166,33 @@ test('sidepanel enables SUB2API session strategy selection when the current Plus
assert.match(api.plusAccountAccessStrategyCaption.textContent, /SUB2API/);
});
+test('sidepanel enables CPA session strategy selection when the current Plus target supports it', () => {
+ const api = buildHarness(
+ `{
+ canShowPlusSettings: true,
+ runtimeLocks: { plusModeEnabled: true },
+ canEditPlusAccountAccessStrategy: true,
+ availablePlusAccountAccessStrategies: ['oauth', 'cpa_codex_session'],
+ effectivePanelMode: 'cpa',
+ effectivePlusAccountAccessStrategy: 'cpa_codex_session',
+ }`,
+ `{
+ activeFlowId: 'openai',
+ panelMode: 'cpa',
+ plusPaymentMethod: 'paypal',
+ plusAccountAccessStrategy: 'cpa_codex_session',
+ }`
+ );
+
+ api.updatePlusModeUI();
+
+ assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
+ assert.equal(api.selectPlusAccountAccessStrategy.disabled, false);
+ assert.equal(api.selectPlusAccountAccessStrategy.value, 'cpa_codex_session');
+ assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'cpa_codex_session');
+ assert.match(api.plusAccountAccessStrategyCaption.textContent, /CPA/);
+});
+
test('sidepanel rebuilds step definitions and workflow nodes with the effective Plus account strategy', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
@@ -195,6 +223,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
let currentPlusAccountAccessStrategy = 'oauth';
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
@@ -272,12 +301,20 @@ return {
test('background declares Plus account access strategy constants before precomputing session-tail step definitions', () => {
const strategyConstantIndex = backgroundSource.indexOf("const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';");
+ const cpaStrategyConstantIndex = backgroundSource.indexOf("const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';");
const precomputedSessionStepsIndex = backgroundSource.indexOf('const PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS =');
+ const precomputedCpaSessionStepsIndex = backgroundSource.indexOf('const PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS =');
assert.ok(strategyConstantIndex >= 0, 'expected Plus account access strategy constant declaration');
+ assert.ok(cpaStrategyConstantIndex >= 0, 'expected CPA Plus account access strategy constant declaration');
assert.ok(precomputedSessionStepsIndex >= 0, 'expected precomputed SUB2API session step definitions');
+ assert.ok(precomputedCpaSessionStepsIndex >= 0, 'expected precomputed CPA session step definitions');
assert.ok(
strategyConstantIndex < precomputedSessionStepsIndex,
'strategy constant must be declared before background precomputes session-tail step definitions'
);
+ assert.ok(
+ cpaStrategyConstantIndex < precomputedCpaSessionStepsIndex,
+ 'CPA strategy constant must be declared before background precomputes CPA session-tail step definitions'
+ );
});
diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js
index 0088740..b6560ee 100644
--- a/tests/sidepanel-plus-payment-method.test.js
+++ b/tests/sidepanel-plus-payment-method.test.js
@@ -85,6 +85,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email';
@@ -170,6 +171,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const rowPayPalAccount = { style: { display: '' } };
${bundle}
@@ -229,6 +231,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
${bundle}
return {
@@ -272,6 +275,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email';
@@ -330,6 +334,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
@@ -432,6 +437,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
@@ -495,6 +501,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
@@ -577,6 +584,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
@@ -745,6 +753,7 @@ test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED sta
const api = new Function(`
const events = [];
let latestState = {
+ activeFlowId: 'openai',
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'gopay-request-1',
plusManualConfirmationStep: 7,
@@ -799,6 +808,11 @@ return { events, syncPlusManualConfirmationDialog };
test('sidepanel resolves pending GPC OTP with typed code', async () => {
const bundle = [
+ extractFunction('normalizeSignupMethod'),
+ extractFunction('normalizePlusAccountAccessStrategy'),
+ extractFunction('normalizePlusStrategyTargetId'),
+ extractFunction('getPlusAccountAccessStrategyContinuationLabel'),
+ extractFunction('resolvePlusManualContinuationActionLabelFromState'),
extractLastFunction('openPlusManualConfirmationDialog'),
extractLastFunction('syncPlusManualConfirmationDialog'),
].join('\n');
@@ -815,6 +829,14 @@ let latestState = {
};
let activePlusManualConfirmationRequestId = '';
let plusManualConfirmationDialogInFlight = false;
+const DEFAULT_ACTIVE_FLOW_ID = 'openai';
+const SIGNUP_METHOD_EMAIL = 'email';
+const SIGNUP_METHOD_PHONE = 'phone';
+const DEFAULT_SIGNUP_METHOD = 'email';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
const sharedFormDialog = {
async open(options) {
events.push({ type: 'form', options });
diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js
index f6e6099..a93131a 100644
--- a/tests/step-definitions-module.test.js
+++ b/tests/step-definitions-module.test.js
@@ -335,6 +335,70 @@ test('Plus phone signup never switches to SUB2API session tail even if the reque
assert.equal(stepKeys.includes('platform-verify'), true);
});
+test('Plus session strategy swaps the OAuth tail for a single CPA 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: 'cpa_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: 'cpa_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: 'cpa_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 === 'cpa-session-import');
+
+ assert.equal(stepKeys.at(-1), 'cpa-session-import', `${label} should end with CPA session import`);
+ assert.equal(nodeIds.at(-1), 'cpa-session-import', `${label} node order should end with CPA session import`);
+ forbiddenTailKeys.forEach((key) => {
+ assert.equal(stepKeys.includes(key), false, `${label} should not keep ${key} in CPA session mode`);
+ assert.equal(nodeIds.includes(key), false, `${label} nodes should not keep ${key} in CPA session mode`);
+ });
+ assert.deepStrictEqual(api.getStepIds(options), expectedStepIds, `${label} step ids should follow the CPA tail`);
+ assert.equal(api.getLastStepId(options), expectedStepIds.at(-1), `${label} last step id should match CPA session import`);
+ assert.deepStrictEqual(previousNode?.next, ['cpa-session-import'], `${label} previous node should link to CPA session import`);
+ assert.deepStrictEqual(sessionImportNode?.next, [], `${label} CPA session import should be terminal`);
+ });
+});
+
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const definitionsIndex = html.indexOf('');