From 8d42770ce3f62b5826b58aecb87e0a744e469733 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=88=9A=E9=91=AB=E7=A3=8A?=
<11811705+qixinlei_24@user.noreply.gitee.com>
Date: Wed, 22 Jul 2026 14:22:16 +0800
Subject: [PATCH] feat(sub2api): add agent identity import mode
---
background.js | 12 +-
background/sub2api-api.js | 157 +++++++++++++
.../steps/sub2api-session-import.js | 80 ++++++-
flows/openai/index.js | 2 +
flows/openai/workflow.js | 37 +++
sidepanel/sidepanel.html | 7 +
sidepanel/sidepanel.js | 86 +++++++
.../background-sub2api-session-import.test.js | 212 ++++++++++++++++++
tests/sidepanel-flow-source-registry.test.js | 68 ++++++
tests/step-definitions-module.test.js | 21 ++
10 files changed, 677 insertions(+), 5 deletions(-)
diff --git a/background.js b/background.js
index 374243a6..c630f750 100644
--- a/background.js
+++ b/background.js
@@ -928,6 +928,9 @@ function getStepDefinitionsForState(state = {}) {
plusModeEnabled: Boolean(resolvedState?.plusModeEnabled),
plusPaymentMethod: normalizePlusPaymentMethod(resolvedState?.plusPaymentMethod),
plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(resolvedState?.plusAccountAccessStrategy),
+ ...(String(resolvedState?.sub2apiImportMode || '').trim().toLowerCase() === 'agent_identity'
+ ? { sub2apiImportMode: 'agent_identity' }
+ : {}),
signupMethod: getSignupMethodForStepDefinitions(resolvedState),
phoneVerificationEnabled: Boolean(resolvedState?.phoneVerificationEnabled),
phoneSignupReloginAfterBindEmailEnabled: Boolean(resolvedState?.phoneSignupReloginAfterBindEmailEnabled),
@@ -1233,6 +1236,7 @@ const PERSISTED_SETTING_DEFAULTS = {
sub2apiUrl: DEFAULT_SUB2API_URL,
sub2apiEmail: '',
sub2apiPassword: '',
+ sub2apiImportMode: 'oauth',
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME,
sub2apiGroupNames: DEFAULT_SUB2API_GROUP_NAMES,
sub2apiAccountPriority: DEFAULT_SUB2API_ACCOUNT_PRIORITY,
@@ -1399,6 +1403,7 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'sub2apiUrl',
'sub2apiEmail',
'sub2apiPassword',
+ 'sub2apiImportMode',
'sub2apiGroupName',
'sub2apiGroupNames',
'sub2apiAccountPriority',
@@ -1442,7 +1447,7 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
const SETTINGS_SCHEMA_VIEW_KEY_SET = new Set(SETTINGS_SCHEMA_VIEW_KEYS);
const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings';
-const STEP6_REGISTRATION_SUCCESS_WAIT_MS = 20000;
+const STEP6_REGISTRATION_SUCCESS_WAIT_MS = 1000;
const DEFAULT_STATE = {
flowId: DEFAULT_ACTIVE_FLOW_ID,
@@ -3218,6 +3223,8 @@ function normalizePersistentSettingValue(key, value) {
return String(value || '').trim();
case 'sub2apiPassword':
return String(value || '');
+ case 'sub2apiImportMode':
+ return String(value || '').trim().toLowerCase() === 'agent_identity' ? 'agent_identity' : 'oauth';
case 'sub2apiGroupName':
return String(value || '').trim();
case 'sub2apiGroupNames':
@@ -12268,7 +12275,7 @@ const AUTO_RUN_NODE_DELAYS = Object.freeze({
'fill-password': 3000,
'fetch-signup-code': 2000,
'fill-profile': 0,
- 'wait-registration-success': 3000,
+ 'wait-registration-success': 0,
'plus-checkout-create': 3000,
'paypal-hosted-openai-checkout': 2000,
'paypal-hosted-email': 2000,
@@ -14211,6 +14218,7 @@ const stepExecutorsByKey = {
'paypal-approve': (state) => payPalApproveExecutor.executePayPalApprove(state),
'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state),
'sub2api-session-import': (state) => sub2ApiSessionImportExecutor.executeSub2ApiSessionImport(state),
+ 'sub2api-agent-identity-import': (state) => sub2ApiSessionImportExecutor.executeSub2ApiAgentIdentityImport(state),
'cpa-session-import': (state) => cpaSessionImportExecutor.executeCpaSessionImport(state),
'openai-upload-session-to-webchat': (state) => openAiWebchatPublisher.executeOpenAiUploadSessionToWebchat(state),
'openai-upload-session-to-chatgpt2api': (state) => openAiChatgpt2ApiPublisher.executeOpenAiUploadSessionToChatgpt2Api(state),
diff --git a/background/sub2api-api.js b/background/sub2api-api.js
index 9b975c0e..63260203 100644
--- a/background/sub2api-api.js
+++ b/background/sub2api-api.js
@@ -9,6 +9,7 @@
fetchImpl = (...args) => fetch(...args),
setTimeoutImpl = (...args) => setTimeout(...args),
clearTimeoutImpl = (...args) => clearTimeout(...args),
+ cryptoImpl = globalThis.crypto,
} = deps;
const DEFAULT_REDIRECT_URI = 'http://localhost:1455/auth/callback';
@@ -558,6 +559,99 @@
}
}
+ function encodeBase64(bytes) {
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
+ if (typeof btoa === 'function') {
+ let binary = '';
+ for (let index = 0; index < view.length; index += 0x8000) {
+ binary += String.fromCharCode(...view.subarray(index, index + 0x8000));
+ }
+ return btoa(binary);
+ }
+ if (typeof Buffer !== 'undefined') {
+ return Buffer.from(view).toString('base64');
+ }
+ throw new Error('当前环境不支持 Base64 编码。');
+ }
+
+ function encodeSshEd25519PublicKey(rawPublicKey) {
+ const algorithm = new TextEncoder().encode('ssh-ed25519');
+ const publicKey = new Uint8Array(rawPublicKey);
+ const bytes = new Uint8Array(4 + algorithm.length + 4 + publicKey.length);
+ const view = new DataView(bytes.buffer);
+ view.setUint32(0, algorithm.length);
+ bytes.set(algorithm, 4);
+ view.setUint32(4 + algorithm.length, publicKey.length);
+ bytes.set(publicKey, 8 + algorithm.length);
+ return `ssh-ed25519 ${encodeBase64(bytes)}`;
+ }
+
+ function buildAgentIdentityContext(state = {}, session = null, accessToken = '') {
+ const token = normalizeString(accessToken || session?.accessToken);
+ const claims = parseCodexAccessTokenClaims(token);
+ const auth = claims?.['https://api.openai.com/auth'] || {};
+ const profile = claims?.['https://api.openai.com/profile'] || {};
+ const accountId = normalizeString(auth.chatgpt_account_id);
+ const userId = normalizeString(auth.chatgpt_user_id || auth.user_id || claims?.sub);
+ if (!accountId || !userId) {
+ throw new Error('accessToken 缺少 OpenAI account_id 或 user_id,无法生成 Agent Identity。');
+ }
+ return {
+ accessToken: token,
+ accountId,
+ userId,
+ email: normalizeEmailValue(profile.email)
+ || resolveCodexSessionImportAccountName(state, session, token),
+ planType: normalizeString(auth.chatgpt_plan_type) || 'free',
+ };
+ }
+
+ async function createOpenAiAgentIdentity(state = {}, session = null, accessToken = '', options = {}) {
+ if (!cryptoImpl?.subtle) {
+ throw new Error('当前浏览器不支持 Web Crypto,无法生成 Agent Identity。');
+ }
+ const context = buildAgentIdentityContext(state, session, accessToken);
+ let keyPair;
+ try {
+ keyPair = await cryptoImpl.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']);
+ } catch {
+ throw new Error('当前浏览器不支持 Ed25519,无法生成 Agent Identity。');
+ }
+ const [privateKey, publicKey] = await Promise.all([
+ cryptoImpl.subtle.exportKey('pkcs8', keyPair.privateKey),
+ cryptoImpl.subtle.exportKey('raw', keyPair.publicKey),
+ ]);
+ const registered = await requestJson('https://auth.openai.com', '/api/accounts/v1/agent/register', {
+ method: 'POST',
+ token: context.accessToken,
+ timeoutMs: options.agentTimeoutMs || options.timeoutMs,
+ body: {
+ abom: {
+ agent_version: '0.138.0-alpha.6',
+ agent_harness_id: 'codex-cli',
+ running_location: 'local',
+ },
+ agent_public_key: encodeSshEd25519PublicKey(publicKey),
+ },
+ });
+ const runtimeId = normalizeString(registered?.agent_runtime_id);
+ if (!runtimeId) {
+ throw new Error('OpenAI Agent 注册未返回 agent_runtime_id。');
+ }
+ return {
+ auth_mode: 'agent_identity',
+ agent_identity: {
+ agent_runtime_id: runtimeId,
+ agent_private_key: encodeBase64(privateKey),
+ account_id: context.accountId,
+ chatgpt_user_id: context.userId,
+ email: context.email,
+ plan_type: context.planType,
+ chatgpt_account_is_fedramp: false,
+ },
+ };
+ }
+
function resolveCodexSessionImportAccountName(state = {}, session = null, accessToken = '') {
const sessionObject = normalizeCodexSessionObject(session);
const claims = parseCodexAccessTokenClaims(accessToken || sessionObject?.accessToken);
@@ -960,9 +1054,71 @@
};
}
+ async function importCurrentChatGptSessionAsAgentIdentity(state = {}, options = {}) {
+ const logLabel = normalizeString(options.logLabel) || 'SUB2API Agent Identity 导入';
+ const session = normalizeCodexSessionObject(state?.session);
+ const accessToken = normalizeString(state?.accessToken || session?.accessToken);
+ if (!accessToken) {
+ throw new Error('未读取到 ChatGPT accessToken,无法生成 Agent Identity。');
+ }
+
+ await logWithOptions(`${logLabel}:正在使用当前 ChatGPT accessToken 注册 Agent...`, 'info', options);
+ const authJson = await createOpenAiAgentIdentity(state, session, accessToken, options);
+ const accountName = normalizeString(authJson?.agent_identity?.email)
+ || resolveCodexSessionImportAccountName(state, session, accessToken);
+
+ await logWithOptions(`${logLabel}:正在登录 SUB2API 并解析分组、代理...`, 'info', options);
+ const { origin, token } = await loginSub2Api(state, options);
+ const groupNames = state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME;
+ const groups = await getGroupsByNames(origin, token, groupNames, options);
+ const proxyPreference = resolveSub2ApiProxyPreference(state);
+ const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference, options) : null;
+ const proxyId = normalizeProxyId(proxy?.id);
+ const accountPriority = resolveSub2ApiAccountPriority(state);
+ const groupIds = groups
+ .map((group) => Number(group?.id))
+ .filter((id) => Number.isFinite(id) && id > 0);
+ if (!groupIds.length) {
+ throw new Error('SUB2API 返回的目标分组 ID 无效。');
+ }
+
+ const importPayload = {
+ content: JSON.stringify(authJson),
+ group_ids: groupIds,
+ ...(accountName ? { name: accountName } : {}),
+ priority: accountPriority,
+ update_existing: true,
+ };
+ if (proxyId) {
+ importPayload.proxy_id = proxyId;
+ }
+
+ await logWithOptions(`${logLabel}:正在导入 Agent Identity 到 SUB2API...`, 'info', options);
+ const importResult = normalizeCodexSessionImportResult(await requestJson(origin, '/api/v1/admin/accounts/import/codex-session', {
+ method: 'POST',
+ token,
+ timeoutMs: options.importTimeoutMs || options.timeoutMs,
+ body: importPayload,
+ }));
+ if (importResult.failed > 0 || (importResult.created <= 0 && importResult.updated <= 0)) {
+ throw new Error(getCodexSessionImportFailureMessage(importResult));
+ }
+ const verifiedStatus = `SUB2API Agent Identity 导入完成:新建 ${importResult.created},更新 ${importResult.updated},跳过 ${importResult.skipped},失败 ${importResult.failed}`;
+ await logWithOptions(verifiedStatus, 'ok', options);
+ return {
+ verifiedStatus,
+ sub2apiImportTotal: importResult.total,
+ sub2apiImportCreated: importResult.created,
+ sub2apiImportUpdated: importResult.updated,
+ sub2apiImportSkipped: importResult.skipped,
+ sub2apiImportFailed: importResult.failed,
+ };
+ }
+
return {
buildDraftAccountName,
buildCodexSessionImportContent,
+ buildAgentIdentityContext,
buildOpenAiCredentials,
buildOpenAiExtra,
buildProxyDisplayName,
@@ -972,6 +1128,7 @@
prepareGrokOAuth,
createGrokAccountFromOAuth,
importCurrentChatGptSession,
+ importCurrentChatGptSessionAsAgentIdentity,
loginSub2Api,
normalizeProxyId,
normalizeRedirectUri,
diff --git a/flows/openai/background/steps/sub2api-session-import.js b/flows/openai/background/steps/sub2api-session-import.js
index a163022f..143122d0 100644
--- a/flows/openai/background/steps/sub2api-session-import.js
+++ b/flows/openai/background/steps/sub2api-session-import.js
@@ -3,6 +3,9 @@
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundSub2ApiSessionImportModule() {
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'flows/openai/content/plus-checkout.js'];
+ const SESSION_READ_INITIAL_DELAY_MS = 1000;
+ const SESSION_READ_RETRY_DELAY_MS = 2000;
+ const SESSION_READ_MAX_ATTEMPTS = 11;
function createSub2ApiSessionImportExecutor(deps = {}) {
const {
@@ -15,6 +18,9 @@
normalizeSub2ApiUrl = (value) => value,
registerTab,
sendTabMessageUntilStopped,
+ sessionReadInitialDelayMs = SESSION_READ_INITIAL_DELAY_MS,
+ sessionReadRetryDelayMs = SESSION_READ_RETRY_DELAY_MS,
+ sessionReadMaxAttempts = SESSION_READ_MAX_ATTEMPTS,
sleepWithStop = async () => {},
throwIfStopped = () => {},
waitForTabCompleteUntilStopped = async () => {},
@@ -199,9 +205,7 @@
return tab;
}
- async function readCurrentChatGptSession(tabId, visibleStep) {
- await waitForTabCompleteUntilStopped(tabId);
- await sleepWithStop(1000);
+ async function requestCurrentChatGptSession(tabId, visibleStep, options = {}) {
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
inject: PLUS_CHECKOUT_INJECT_FILES,
injectSource: PLUS_CHECKOUT_SOURCE,
@@ -227,6 +231,9 @@
sessionResult?.accessToken
|| session?.accessToken
);
+ if (options.requireAccessToken && !accessToken) {
+ throw new Error(`步骤 ${visibleStep}:未读取到 ChatGPT accessToken,请确认当前标签页仍处于已登录状态。`);
+ }
if (!session && !accessToken) {
throw new Error(`步骤 ${visibleStep}:未读取到有效的 ChatGPT 会话或 accessToken,请确认当前标签页仍处于已登录状态。`);
}
@@ -237,6 +244,39 @@
};
}
+ async function readCurrentChatGptSession(tabId, visibleStep, options = {}) {
+ await waitForTabCompleteUntilStopped(tabId);
+
+ const firstDelayMs = Math.max(0, Math.floor(Number(sessionReadInitialDelayMs) || 0));
+ const retryDelayMs = Math.max(0, Math.floor(Number(sessionReadRetryDelayMs) || 0));
+ const maxAttempts = Math.max(1, Math.floor(Number(sessionReadMaxAttempts) || SESSION_READ_MAX_ATTEMPTS));
+ let lastError = null;
+
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
+ throwIfStopped();
+ const delayMs = attempt === 1 ? firstDelayMs : retryDelayMs;
+ if (delayMs > 0) {
+ await sleepWithStop(delayMs);
+ }
+
+ try {
+ return await requestCurrentChatGptSession(tabId, visibleStep, options);
+ } catch (error) {
+ lastError = error;
+ if (attempt >= maxAttempts) {
+ break;
+ }
+ await addStepLog(
+ visibleStep,
+ `未读取到 ChatGPT session/accessToken,${Math.round(retryDelayMs / 1000)} 秒后重试...`,
+ 'warn'
+ );
+ }
+ }
+
+ throw lastError || new Error(`步骤 ${visibleStep}:未读取到有效的 ChatGPT 会话或 accessToken,请确认当前标签页仍处于已登录状态。`);
+ }
+
async function executeSub2ApiSessionImport(state = {}) {
throwIfStopped();
const visibleStep = resolveVisibleStep(state);
@@ -268,8 +308,42 @@
await completeNodeFromBackground(state?.nodeId || 'sub2api-session-import', result);
}
+ async function executeSub2ApiAgentIdentityImport(state = {}) {
+ throwIfStopped();
+ const visibleStep = resolveVisibleStep(state);
+ const api = getSub2ApiApi();
+ const importAgentIdentity = api.importCurrentChatGptSessionAsAgentIdentity
+ || api.importCurrentChatGptSession;
+
+ await addStepLog(visibleStep, '正在定位当前 ChatGPT 会话页并读取 accessToken...', '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(() => {});
+ }
+ const sessionState = await readCurrentChatGptSession(tab.id, visibleStep, {
+ requireAccessToken: true,
+ });
+ throwIfStopped();
+
+ const result = await importAgentIdentity({
+ ...state,
+ session: sessionState.session,
+ accessToken: sessionState.accessToken,
+ }, {
+ visibleStep,
+ logLabel: `步骤 ${visibleStep}`,
+ logOptions: { step: visibleStep, stepKey: 'sub2api-agent-identity-import' },
+ timeoutMs: 120000,
+ agentTimeoutMs: 60000,
+ importTimeoutMs: 120000,
+ });
+ await completeNodeFromBackground(state?.nodeId || 'sub2api-agent-identity-import', result);
+ }
+
return {
executeSub2ApiSessionImport,
+ executeSub2ApiAgentIdentityImport,
isSupportedChatGptSessionUrl,
};
}
diff --git a/flows/openai/index.js b/flows/openai/index.js
index 8d9e5456..608c1473 100644
--- a/flows/openai/index.js
+++ b/flows/openai/index.js
@@ -71,6 +71,7 @@
"sub2apiUrl": "",
"sub2apiEmail": "",
"sub2apiPassword": "",
+ "sub2apiImportMode": "oauth",
"sub2apiGroupName": "codex",
"sub2apiGroupNames": [
"codex",
@@ -431,6 +432,7 @@
"row-sub2api-url",
"row-sub2api-email",
"row-sub2api-password",
+ "row-sub2api-import-mode",
"row-sub2api-group",
"row-sub2api-account-priority",
"row-sub2api-default-proxy"
diff --git a/flows/openai/workflow.js b/flows/openai/workflow.js
index 3888b8aa..1dc796b5 100644
--- a/flows/openai/workflow.js
+++ b/flows/openai/workflow.js
@@ -16,6 +16,7 @@
const OPENAI_WEBCHAT_UPLOAD_STEP_KEY = 'openai-upload-session-to-webchat';
const OPENAI_CHATGPT2API_TARGET_ID = 'chatgpt2api';
const OPENAI_CHATGPT2API_UPLOAD_STEP_KEY = 'openai-upload-session-to-chatgpt2api';
+ const OPENAI_SUB2API_AGENT_IDENTITY_IMPORT_STEP_KEY = 'sub2api-agent-identity-import';
function freezeDeep(entry) {
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
@@ -1909,6 +1910,37 @@
};
}
+ function getSub2ApiAgentIdentityImportStep() {
+ return {
+ key: OPENAI_SUB2API_AGENT_IDENTITY_IMPORT_STEP_KEY,
+ title: '生成并导入 Agent Identity',
+ sourceId: 'sub2api-panel',
+ driverId: 'flows/openai/background/steps/sub2api-session-import',
+ command: OPENAI_SUB2API_AGENT_IDENTITY_IMPORT_STEP_KEY,
+ flowId: 'openai',
+ };
+ }
+
+ function isSub2ApiAgentIdentityMode(options = {}) {
+ return String(options?.targetId || '').trim().toLowerCase() === 'sub2api'
+ && String(options?.sub2apiImportMode || '').trim().toLowerCase() === 'agent_identity';
+ }
+
+ function replaceOpenAiOAuthTailWithAgentIdentity(steps = []) {
+ const registrationWaitIndex = steps.findIndex((step) => step.key === PLUS_REGISTRATION_WAIT_STEP_KEY);
+ if (registrationWaitIndex < 0) return steps;
+ return [
+ ...steps.slice(0, registrationWaitIndex + 1),
+ getSub2ApiAgentIdentityImportStep(),
+ ];
+ }
+
+ function replaceSessionImportWithAgentIdentity(steps = []) {
+ return steps.map((step) => step.key === 'sub2api-session-import'
+ ? getSub2ApiAgentIdentityImportStep()
+ : step);
+ }
+
function getPlusRegistrationWaitStep() {
const sourceStep = STEP_VARIANTS.normal.find((step) => step.key === PLUS_REGISTRATION_WAIT_STEP_KEY);
return {
@@ -2079,6 +2111,11 @@
if (!isPhoneVerificationEnabled(options)) {
steps = omitPostLoginPhoneVerificationSteps(steps);
}
+ if (isSub2ApiAgentIdentityMode(options)) {
+ steps = steps.some((step) => step.key === 'sub2api-session-import')
+ ? replaceSessionImportWithAgentIdentity(steps)
+ : replaceOpenAiOAuthTailWithAgentIdentity(steps);
+ }
if (isOpenAiRemotePublicationTarget(options)) {
steps = omitOpenAiWebchatPublicationSteps(steps);
}
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 342b6587..6ed9791a 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -201,6 +201,13 @@
data-hide-label="隐藏 SUB2API 密码" aria-label="显示 SUB2API 密码" title="显示 SUB2API 密码">
+
+ 导入模式
+
+
双发布
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 6a6df3e8..7ea83744 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -103,6 +103,8 @@ const rowSub2ApiEmail = document.getElementById('row-sub2api-email');
const inputSub2ApiEmail = document.getElementById('input-sub2api-email');
const rowSub2ApiPassword = document.getElementById('row-sub2api-password');
const inputSub2ApiPassword = document.getElementById('input-sub2api-password');
+const rowSub2ApiImportMode = document.getElementById('row-sub2api-import-mode');
+const inputSub2ApiImportMode = document.getElementById('input-sub2api-import-mode');
const rowSub2ApiGroup = document.getElementById('row-sub2api-group');
const inputSub2ApiGroup = document.getElementById('input-sub2api-group');
const sub2ApiGroupPickerRoot = document.getElementById('sub2api-group-picker');
@@ -638,6 +640,7 @@ let currentStepDefinitionFlowId = DEFAULT_ACTIVE_FLOW_ID;
let currentStepDefinitionTargetId = '';
let currentStepDefinitionOpenAiWebchatUploadEnabled = false;
let currentStepDefinitionGrokSub2apiGrok2ApiUploadEnabled = false;
+let currentStepDefinitionSub2ApiImportMode = 'oauth';
let phoneSignupReuseUiWasLocked = false;
let kiroRsConnectionTestStatusText = '未测试';
let lastPhoneSmsProviderBeforeChange = null;
@@ -662,6 +665,7 @@ const nexSmsCountrySearchTextById = new Map();
let stepDefinitions = getStepDefinitionsForMode(false, {
plusPaymentMethod: currentPlusPaymentMethod,
plusAccountAccessStrategy: currentPlusAccountAccessStrategy,
+ sub2apiImportMode: currentStepDefinitionSub2ApiImportMode,
signupMethod: currentSignupMethod,
phoneVerificationEnabled: currentPhoneVerificationEnabled,
phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled,
@@ -669,6 +673,7 @@ let stepDefinitions = getStepDefinitionsForMode(false, {
let workflowNodes = getWorkflowNodesForMode(false, {
plusPaymentMethod: currentPlusPaymentMethod,
plusAccountAccessStrategy: currentPlusAccountAccessStrategy,
+ sub2apiImportMode: currentStepDefinitionSub2ApiImportMode,
signupMethod: currentSignupMethod,
phoneVerificationEnabled: currentPhoneVerificationEnabled,
phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled,
@@ -1121,6 +1126,12 @@ function initPhoneVerificationSectionExpandedState() {
}
}
+function normalizeSub2ApiImportModeForStepDefinitions(value = '') {
+ return String(value || '').trim().toLowerCase() === 'agent_identity'
+ ? 'agent_identity'
+ : 'oauth';
+}
+
function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
@@ -1131,6 +1142,17 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
const rawPlusAccountAccessStrategy = typeof options === 'string'
? currentPlusAccountAccessStrategy
: (options.plusAccountAccessStrategy || currentPlusAccountAccessStrategy || defaultStrategy);
+ const normalizeSub2ApiImportModeSafe = typeof normalizeSub2ApiImportModeForStepDefinitions === 'function'
+ ? normalizeSub2ApiImportModeForStepDefinitions
+ : ((value = '') => (String(value || '').trim().toLowerCase() === 'agent_identity' ? 'agent_identity' : 'oauth'));
+ const sub2apiImportMode = normalizeSub2ApiImportModeSafe(
+ typeof options === 'string'
+ ? currentStepDefinitionSub2ApiImportMode
+ : (options.sub2apiImportMode
+ ?? (typeof inputSub2ApiImportMode !== 'undefined' && inputSub2ApiImportMode
+ ? inputSub2ApiImportMode.value
+ : (typeof latestState !== 'undefined' ? latestState?.sub2apiImportMode : currentStepDefinitionSub2ApiImportMode)))
+ );
const rawSignupMethod = typeof options === 'string'
? currentSignupMethod
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
@@ -1174,6 +1196,7 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
plusModeEnabled,
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(rawPlusAccountAccessStrategy),
+ ...(sub2apiImportMode === 'agent_identity' ? { sub2apiImportMode } : {}),
openaiWebchatUploadEnabled,
grokSub2apiGrok2ApiUploadEnabled,
settingsState,
@@ -1200,6 +1223,17 @@ function getWorkflowNodesForMode(plusModeEnabled = false, options = {}) {
const rawPlusAccountAccessStrategy = typeof options === 'string'
? currentPlusAccountAccessStrategy
: (options.plusAccountAccessStrategy || currentPlusAccountAccessStrategy || defaultStrategy);
+ const normalizeSub2ApiImportModeSafe = typeof normalizeSub2ApiImportModeForStepDefinitions === 'function'
+ ? normalizeSub2ApiImportModeForStepDefinitions
+ : ((value = '') => (String(value || '').trim().toLowerCase() === 'agent_identity' ? 'agent_identity' : 'oauth'));
+ const sub2apiImportMode = normalizeSub2ApiImportModeSafe(
+ typeof options === 'string'
+ ? currentStepDefinitionSub2ApiImportMode
+ : (options.sub2apiImportMode
+ ?? (typeof inputSub2ApiImportMode !== 'undefined' && inputSub2ApiImportMode
+ ? inputSub2ApiImportMode.value
+ : (typeof latestState !== 'undefined' ? latestState?.sub2apiImportMode : currentStepDefinitionSub2ApiImportMode)))
+ );
const rawSignupMethod = typeof options === 'string'
? currentSignupMethod
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
@@ -1243,6 +1277,7 @@ function getWorkflowNodesForMode(plusModeEnabled = false, options = {}) {
plusModeEnabled,
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(rawPlusAccountAccessStrategy),
+ ...(sub2apiImportMode === 'agent_identity' ? { sub2apiImportMode } : {}),
openaiWebchatUploadEnabled,
grokSub2apiGrok2ApiUploadEnabled,
settingsState,
@@ -1311,6 +1346,17 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
const rawPlusAccountAccessStrategy = typeof options === 'string'
? currentPlusAccountAccessStrategy
: (options.plusAccountAccessStrategy || currentPlusAccountAccessStrategy || defaultStrategy);
+ const normalizeSub2ApiImportModeSafe = typeof normalizeSub2ApiImportModeForStepDefinitions === 'function'
+ ? normalizeSub2ApiImportModeForStepDefinitions
+ : ((value = '') => (String(value || '').trim().toLowerCase() === 'agent_identity' ? 'agent_identity' : 'oauth'));
+ const sub2apiImportMode = normalizeSub2ApiImportModeSafe(
+ typeof options === 'string'
+ ? currentStepDefinitionSub2ApiImportMode
+ : (options.sub2apiImportMode
+ ?? (typeof inputSub2ApiImportMode !== 'undefined' && inputSub2ApiImportMode
+ ? inputSub2ApiImportMode.value
+ : (typeof latestState !== 'undefined' ? latestState?.sub2apiImportMode : currentStepDefinitionSub2ApiImportMode)))
+ );
const rawSignupMethod = typeof options === 'string'
? currentSignupMethod
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
@@ -1343,6 +1389,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
const settingsState = options.settingsState || (typeof latestState !== 'undefined' ? latestState?.settingsState : undefined);
currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
currentPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(rawPlusAccountAccessStrategy);
+ currentStepDefinitionSub2ApiImportMode = sub2apiImportMode;
currentSignupMethod = normalizeSignupMethod(rawSignupMethod);
currentPhoneVerificationEnabled = Boolean(phoneVerificationEnabled);
currentPhoneSignupReloginAfterBindEmailEnabled = phoneSignupReloginAfterBindEmailEnabled;
@@ -1368,6 +1415,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
targetId,
plusPaymentMethod: currentPlusPaymentMethod,
plusAccountAccessStrategy: currentPlusAccountAccessStrategy,
+ sub2apiImportMode: currentStepDefinitionSub2ApiImportMode,
openaiWebchatUploadEnabled,
grokSub2apiGrok2ApiUploadEnabled,
settingsState,
@@ -1382,6 +1430,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
targetId,
plusPaymentMethod: currentPlusPaymentMethod,
plusAccountAccessStrategy: currentPlusAccountAccessStrategy,
+ sub2apiImportMode: currentStepDefinitionSub2ApiImportMode,
openaiWebchatUploadEnabled,
grokSub2apiGrok2ApiUploadEnabled,
settingsState,
@@ -5460,6 +5509,10 @@ function collectSettingsPayload() {
sub2apiUrl: inputSub2ApiUrl.value.trim(),
sub2apiEmail: inputSub2ApiEmail.value.trim(),
sub2apiPassword: inputSub2ApiPassword.value,
+ sub2apiImportMode: (
+ typeof inputSub2ApiImportMode !== 'undefined'
+ && inputSub2ApiImportMode?.value === 'agent_identity'
+ ) ? 'agent_identity' : 'oauth',
sub2apiGroupName: selectedSub2ApiGroupName,
sub2apiGroupNames,
sub2apiAccountPriority: sub2apiAccountPriorityNormalizer(
@@ -11884,6 +11937,15 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
|| currentPlusAccountAccessStrategy
|| defaultStrategy
);
+ const normalizeSub2ApiImportModeSafe = typeof normalizeSub2ApiImportModeForStepDefinitions === 'function'
+ ? normalizeSub2ApiImportModeForStepDefinitions
+ : ((value = '') => (String(value || '').trim().toLowerCase() === 'agent_identity' ? 'agent_identity' : 'oauth'));
+ const nextSub2ApiImportMode = normalizeSub2ApiImportModeSafe(
+ options.sub2apiImportMode
+ ?? (typeof inputSub2ApiImportMode !== 'undefined' && inputSub2ApiImportMode
+ ? inputSub2ApiImportMode.value
+ : (typeof latestState !== 'undefined' ? latestState?.sub2apiImportMode : currentStepDefinitionSub2ApiImportMode))
+ );
const nextSignupMethod = normalizeSignupMethod(options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
const nextPhoneVerificationEnabled = Boolean(options.phoneVerificationEnabled ?? (typeof inputPhoneVerificationEnabled !== 'undefined' && inputPhoneVerificationEnabled
? inputPhoneVerificationEnabled.checked
@@ -11930,6 +11992,9 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
const currentGrokSub2apiGrok2ApiUploadEnabled = typeof currentStepDefinitionGrokSub2apiGrok2ApiUploadEnabled !== 'undefined'
? Boolean(currentStepDefinitionGrokSub2apiGrok2ApiUploadEnabled)
: Boolean(typeof latestState !== 'undefined' ? latestState?.grokSub2apiGrok2ApiUploadEnabled : false);
+ const currentSub2ApiImportMode = typeof currentStepDefinitionSub2ApiImportMode !== 'undefined'
+ ? normalizeSub2ApiImportModeSafe(currentStepDefinitionSub2ApiImportMode)
+ : 'oauth';
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const currentPaymentStep = stepDefinitions.find((step) => step.key === 'paypal-approve');
const nextPaymentTitle = rootScope.MultiPageStepDefinitions?.getPlusPaymentStepTitle?.({
@@ -11938,6 +12003,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
plusModeEnabled: nextPlusModeEnabled,
plusPaymentMethod: nextPaymentMethod,
plusAccountAccessStrategy: nextPlusAccountAccessStrategy,
+ sub2apiImportMode: nextSub2ApiImportMode,
openaiWebchatUploadEnabled: nextOpenaiWebchatUploadEnabled,
grokSub2apiGrok2ApiUploadEnabled: nextGrokSub2apiGrok2ApiUploadEnabled,
settingsState: nextSettingsState,
@@ -11950,6 +12016,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
|| nextPlusModeEnabled !== currentPlusModeEnabled
|| nextPaymentMethod !== currentPlusPaymentMethod
|| nextPlusAccountAccessStrategy !== currentPlusAccountAccessStrategy
+ || nextSub2ApiImportMode !== currentSub2ApiImportMode
|| nextSignupMethod !== currentSignupMethod
|| nextPhoneVerificationEnabled !== currentPhoneVerificationEnabled
|| nextPhoneSignupReloginAfterBindEmailEnabled !== currentPhoneSignupReloginAfterBindEmailEnabled
@@ -11968,6 +12035,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
targetId: nextTargetId,
plusPaymentMethod: nextPaymentMethod,
plusAccountAccessStrategy: nextPlusAccountAccessStrategy,
+ sub2apiImportMode: nextSub2ApiImportMode,
openaiWebchatUploadEnabled: nextOpenaiWebchatUploadEnabled,
grokSub2apiGrok2ApiUploadEnabled: nextGrokSub2apiGrok2ApiUploadEnabled,
settingsState: nextSettingsState,
@@ -12000,6 +12068,7 @@ function syncStepDefinitionsFromUiState(stateOverrides = {}) {
targetId: nextState?.targetId,
plusPaymentMethod: getSelectedPlusPaymentMethod(nextState),
plusAccountAccessStrategy: stepDefinitionState.plusAccountAccessStrategy,
+ sub2apiImportMode: nextState?.sub2apiImportMode,
openaiWebchatUploadEnabled: stepDefinitionState.openaiWebchatUploadEnabled,
grokSub2apiGrok2ApiUploadEnabled: stepDefinitionState.grokSub2apiGrok2ApiUploadEnabled,
settingsState: nextState?.settingsState,
@@ -12031,6 +12100,7 @@ function applySettingsState(state) {
plusPaymentMethod: state?.plusPaymentMethod,
signupMethod: stepDefinitionState.signupMethod,
plusAccountAccessStrategy: stepDefinitionState.plusAccountAccessStrategy,
+ sub2apiImportMode: state?.sub2apiImportMode,
openaiWebchatUploadEnabled: stepDefinitionState.openaiWebchatUploadEnabled,
grokSub2apiGrok2ApiUploadEnabled: stepDefinitionState.grokSub2apiGrok2ApiUploadEnabled,
settingsState: state?.settingsState,
@@ -12138,6 +12208,9 @@ function applySettingsState(state) {
inputSub2ApiUrl.value = state?.sub2apiUrl || '';
inputSub2ApiEmail.value = state?.sub2apiEmail || '';
inputSub2ApiPassword.value = state?.sub2apiPassword || '';
+ if (typeof inputSub2ApiImportMode !== 'undefined' && inputSub2ApiImportMode) {
+ inputSub2ApiImportMode.value = state?.sub2apiImportMode === 'agent_identity' ? 'agent_identity' : 'oauth';
+ }
renderSub2ApiGroupOptions(state, state?.sub2apiGroupName || '');
if (typeof inputSub2ApiAccountPriority !== 'undefined' && inputSub2ApiAccountPriority) {
inputSub2ApiAccountPriority.value = String(normalizeSub2ApiAccountPriorityValue(state?.sub2apiAccountPriority));
@@ -17382,6 +17455,19 @@ inputSub2ApiPassword.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
+if (typeof inputSub2ApiImportMode !== 'undefined' && inputSub2ApiImportMode) {
+ inputSub2ApiImportMode.addEventListener('change', () => {
+ const sub2apiImportMode = normalizeSub2ApiImportModeForStepDefinitions(inputSub2ApiImportMode.value);
+ inputSub2ApiImportMode.value = sub2apiImportMode;
+ syncLatestState({ sub2apiImportMode });
+ if (typeof syncStepDefinitionsFromUiState === 'function') {
+ syncStepDefinitionsFromUiState({ sub2apiImportMode });
+ }
+ markSettingsDirty(true);
+ saveSettings({ silent: true }).catch(() => {});
+ });
+}
+
inputSub2ApiGroup.addEventListener('change', () => {
syncLatestState({
sub2apiGroupName: getSelectedSub2ApiGroupName(),
diff --git a/tests/background-sub2api-session-import.test.js b/tests/background-sub2api-session-import.test.js
index 8d352643..07c7df17 100644
--- a/tests/background-sub2api-session-import.test.js
+++ b/tests/background-sub2api-session-import.test.js
@@ -254,6 +254,81 @@ test('sub2api session import falls back to registration email when session has n
assert.equal(importBody.name, 'registration@example.com');
});
+test('sub2api agent identity import registers an agent and never submits the access token as import content', async () => {
+ const apiModule = loadSub2ApiApiModule();
+ const token = createJwtToken({
+ sub: 'user-123',
+ email: 'owner@example.com',
+ 'https://api.openai.com/auth': {
+ chatgpt_account_id: 'account-123',
+ chatgpt_user_id: 'user-123',
+ chatgpt_plan_type: 'plus',
+ },
+ 'https://api.openai.com/profile': { email: 'owner@example.com' },
+ });
+ let importBody = null;
+ const api = apiModule.createSub2ApiApi({
+ addLog: async () => {},
+ normalizeSub2ApiUrl: (value) => value,
+ DEFAULT_SUB2API_GROUP_NAME: 'codex',
+ cryptoImpl: {
+ subtle: {
+ async generateKey() { return { privateKey: {}, publicKey: {} }; },
+ async exportKey(format) {
+ return format === 'pkcs8'
+ ? Uint8Array.from([1, 2, 3]).buffer
+ : Uint8Array.from(new Array(32).fill(7)).buffer;
+ },
+ },
+ },
+ fetchImpl: async (url, options = {}) => {
+ const parsed = new URL(url);
+ const body = options.body ? JSON.parse(options.body) : null;
+ if (parsed.origin === 'https://auth.openai.com') {
+ assert.equal(parsed.pathname, '/api/accounts/v1/agent/register');
+ assert.equal(options.headers.Authorization, `Bearer ${token}`);
+ assert.match(body.agent_public_key, /^ssh-ed25519 /);
+ return createJsonResponse({ agent_runtime_id: 'agent-123' });
+ }
+ if (parsed.pathname === '/api/v1/auth/login') {
+ return createJsonResponse({ code: 0, data: { access_token: 'admin-token' } });
+ }
+ if (parsed.pathname === '/api/v1/admin/groups/all') {
+ return createJsonResponse({ code: 0, data: [{ id: 5, name: 'codex', platform: 'openai' }] });
+ }
+ if (parsed.pathname === '/api/v1/admin/proxies/all') {
+ return createJsonResponse({ code: 0, data: [{ id: 7, name: '新加坡', status: 'active' }] });
+ }
+ if (parsed.pathname === '/api/v1/admin/accounts/import/codex-session') {
+ importBody = body;
+ return createJsonResponse({ code: 0, data: { total: 1, created: 1, updated: 0, skipped: 0, failed: 0 } });
+ }
+ return createJsonResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404);
+ },
+ });
+
+ const result = await api.importCurrentChatGptSessionAsAgentIdentity({
+ sub2apiUrl: 'https://sub.example/admin/accounts',
+ sub2apiEmail: 'admin@example.com',
+ sub2apiPassword: 'secret',
+ sub2apiGroupName: 'codex',
+ sub2apiDefaultProxyName: '新加坡',
+ sub2apiAccountPriority: 2,
+ accessToken: token,
+ });
+
+ const authJson = JSON.parse(importBody.content);
+ assert.equal(authJson.auth_mode, 'agent_identity');
+ assert.equal(authJson.agent_identity.agent_runtime_id, 'agent-123');
+ assert.equal(authJson.agent_identity.email, 'owner@example.com');
+ assert.equal(importBody.name, 'owner@example.com');
+ assert.equal(importBody.proxy_id, 7);
+ assert.deepEqual(importBody.group_ids, [5]);
+ assert.equal(importBody.priority, 2);
+ assert.equal(importBody.content.includes(token), false);
+ assert.match(result.verifiedStatus, /Agent Identity/);
+});
+
test('session import step reads current ChatGPT session and completes node', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
const completed = [];
@@ -365,6 +440,143 @@ test('session import step reads current ChatGPT session and completes node', asy
);
});
+test('session import retries session token reads after a short initial delay', async () => {
+ const moduleApi = loadSub2ApiSessionImportModule();
+ const sleeps = [];
+ const sentMessages = [];
+ const logs = [];
+ const importedPayloads = [];
+
+ const executor = moduleApi.createSub2ApiSessionImportExecutor({
+ addLog: async (message, level = 'info', options = {}) => {
+ logs.push({ message, level, step: options.step, stepKey: options.stepKey });
+ },
+ chrome: {
+ tabs: {
+ get: async (tabId) => ({
+ id: tabId,
+ url: 'https://chatgpt.com/',
+ }),
+ update: async () => {},
+ },
+ },
+ completeNodeFromBackground: async () => {},
+ createSub2ApiApi: () => ({
+ importCurrentChatGptSession: async (state) => {
+ importedPayloads.push(state);
+ return { verifiedStatus: 'ok' };
+ },
+ }),
+ ensureContentScriptReadyOnTabUntilStopped: async () => {},
+ getTabId: async () => null,
+ isTabAlive: async () => false,
+ normalizeSub2ApiUrl: (value) => value,
+ registerTab: async () => {},
+ sendTabMessageUntilStopped: async (tabId, source, message) => {
+ sentMessages.push({ tabId, source, message });
+ if (sentMessages.length === 1) {
+ return {};
+ }
+ return {
+ session: {
+ accessToken: 'retry-access-token',
+ user: { email: 'retry@example.com' },
+ },
+ accessToken: 'retry-access-token',
+ };
+ },
+ sessionReadMaxAttempts: 2,
+ sleepWithStop: async (ms) => {
+ sleeps.push(ms);
+ },
+ throwIfStopped: () => {},
+ waitForTabCompleteUntilStopped: async () => {},
+ });
+
+ await executor.executeSub2ApiSessionImport({
+ nodeId: 'sub2api-session-import',
+ visibleStep: 10,
+ plusCheckoutTabId: 91,
+ });
+
+ assert.deepStrictEqual(sleeps, [1000, 2000]);
+ assert.equal(sentMessages.length, 2);
+ assert.equal(importedPayloads.length, 1);
+ assert.equal(importedPayloads[0].accessToken, 'retry-access-token');
+ assert.equal(
+ logs.some((entry) => entry.level === 'warn' && /2 秒后重试/.test(entry.message)),
+ true
+ );
+});
+
+test('agent identity import retries when session exists but access token is not ready', async () => {
+ const moduleApi = loadSub2ApiSessionImportModule();
+ const sleeps = [];
+ const sentMessages = [];
+ const importedPayloads = [];
+
+ const executor = moduleApi.createSub2ApiSessionImportExecutor({
+ addLog: async () => {},
+ chrome: {
+ tabs: {
+ get: async (tabId) => ({
+ id: tabId,
+ url: 'https://chatgpt.com/',
+ }),
+ update: async () => {},
+ },
+ },
+ completeNodeFromBackground: async () => {},
+ createSub2ApiApi: () => ({
+ importCurrentChatGptSessionAsAgentIdentity: async (state) => {
+ importedPayloads.push(state);
+ return { verifiedStatus: 'ok' };
+ },
+ }),
+ ensureContentScriptReadyOnTabUntilStopped: async () => {},
+ getTabId: async () => null,
+ isTabAlive: async () => false,
+ normalizeSub2ApiUrl: (value) => value,
+ registerTab: async () => {},
+ sendTabMessageUntilStopped: async () => {
+ sentMessages.push(true);
+ if (sentMessages.length === 1) {
+ return {
+ session: {
+ user: { email: 'pending@example.com' },
+ },
+ accessToken: '',
+ };
+ }
+ return {
+ session: {
+ accessToken: 'agent-access-token',
+ user: { email: 'ready@example.com' },
+ },
+ accessToken: 'agent-access-token',
+ };
+ },
+ sessionReadMaxAttempts: 2,
+ sleepWithStop: async (ms) => {
+ sleeps.push(ms);
+ },
+ throwIfStopped: () => {},
+ waitForTabCompleteUntilStopped: async () => {},
+ });
+
+ await executor.executeSub2ApiAgentIdentityImport({
+ nodeId: 'sub2api-agent-identity-import',
+ visibleStep: 7,
+ plusCheckoutTabId: 91,
+ });
+
+ assert.deepStrictEqual(sleeps, [1000, 2000]);
+ assert.equal(sentMessages.length, 2);
+ assert.equal(importedPayloads.length, 1);
+ assert.equal(importedPayloads[0].accessToken, 'agent-access-token');
+ assert.equal(importedPayloads[0].session.user.email, 'ready@example.com');
+});
+
test('session import step falls back to an active ChatGPT tab when no checkout tab is tracked', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
const completed = [];
diff --git a/tests/sidepanel-flow-source-registry.test.js b/tests/sidepanel-flow-source-registry.test.js
index 5748696f..f8512cc2 100644
--- a/tests/sidepanel-flow-source-registry.test.js
+++ b/tests/sidepanel-flow-source-registry.test.js
@@ -412,6 +412,74 @@ return {
assert.equal(api.calls.at(-2).options.openaiWebchatUploadEnabled, true);
});
+test('sidepanel step definitions rerender when SUB2API import mode changes', () => {
+ const bundle = [
+ extractFunction(sidepanelSource, 'normalizeSignupMethod'),
+ extractFunction(sidepanelSource, 'normalizePlusPaymentMethod'),
+ extractFunction(sidepanelSource, 'getStepDefinitionsForMode'),
+ extractFunction(sidepanelSource, 'rebuildStepDefinitionState'),
+ extractFunction(sidepanelSource, 'syncStepDefinitionsForMode'),
+ ].join('\n');
+
+ const api = new Function(`
+const calls = [];
+const window = {
+ MultiPageStepDefinitions: {
+ getSteps(options) {
+ calls.push({ type: 'getSteps', options });
+ return options.sub2apiImportMode === 'agent_identity'
+ ? [{ id: 7, order: 7, key: 'sub2api-agent-identity-import' }]
+ : [{ id: 7, order: 7, key: 'oauth-login' }, { id: 10, order: 10, key: 'platform-verify' }];
+ },
+ },
+};
+let latestState = { activeFlowId: 'openai', targetId: 'sub2api', sub2apiImportMode: 'oauth' };
+let currentPlusModeEnabled = false;
+let currentPlusPaymentMethod = 'paypal';
+let currentPlusAccountAccessStrategy = 'oauth';
+let currentStepDefinitionSub2ApiImportMode = 'oauth';
+let currentSignupMethod = 'email';
+let currentPhoneVerificationEnabled = false;
+let currentPhoneSignupReloginAfterBindEmailEnabled = false;
+let currentStepDefinitionFlowId = 'openai';
+let currentStepDefinitionTargetId = 'sub2api';
+let currentStepDefinitionOpenAiWebchatUploadEnabled = false;
+let currentStepDefinitionGrokSub2apiGrok2ApiUploadEnabled = false;
+const DEFAULT_ACTIVE_FLOW_ID = 'openai';
+const DEFAULT_SIGNUP_METHOD = 'email';
+const DEFAULT_PLUS_PAYMENT_METHOD = 'paypal';
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
+let stepDefinitions = [{ id: 7, key: 'oauth-login' }, { id: 10, key: 'platform-verify' }];
+let STEP_IDS = [7, 10];
+let STEP_DEFAULT_STATUSES = { 7: 'pending', 10: 'pending' };
+let SKIPPABLE_STEPS = new Set([7, 10]);
+function renderStepsList() {
+ calls.push({ type: 'render', stepIds: [...STEP_IDS] });
+}
+function normalizePlusAccountAccessStrategy(value = '') {
+ return String(value || DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY).trim().toLowerCase() || DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
+}
+${bundle}
+return {
+ calls,
+ syncStepDefinitionsForMode,
+ getStepIds: () => [...STEP_IDS],
+};
+`)();
+
+ api.syncStepDefinitionsForMode(false, {
+ activeFlowId: 'openai',
+ targetId: 'sub2api',
+ sub2apiImportMode: 'agent_identity',
+ plusPaymentMethod: 'paypal',
+ signupMethod: 'email',
+ });
+
+ assert.deepEqual(api.getStepIds(), [7]);
+ assert.equal(api.calls.at(-2).options.sub2apiImportMode, 'agent_identity');
+ assert.equal(api.calls.at(-1).type, 'render');
+});
+
test('sidepanel step definitions rerender when Grok SUB2API dual publishing changes', () => {
const bundle = [
extractFunction(sidepanelSource, 'normalizeSignupMethod'),
diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js
index a595e2e3..4020bd66 100644
--- a/tests/step-definitions-module.test.js
+++ b/tests/step-definitions-module.test.js
@@ -601,6 +601,27 @@ test('OpenAI remote upload is appended only for remote publication targets', ()
assert.equal(schemaSyncSteps.some((step) => step.key === 'openai-upload-session-to-chatgpt2api'), false);
});
+test('SUB2API Agent Identity mode replaces the OAuth callback tail while OAuth mode remains unchanged', () => {
+ const globalScope = {};
+ const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope);
+ const oauthKeys = api.getSteps({ targetId: 'sub2api', sub2apiImportMode: 'oauth' }).map((step) => step.key);
+ assert.equal(oauthKeys.includes('oauth-login'), true);
+ assert.equal(oauthKeys.includes('platform-verify'), true);
+
+ const agentSteps = api.getSteps({ targetId: 'sub2api', sub2apiImportMode: 'agent_identity' });
+ assert.deepEqual(agentSteps.map((step) => step.key), [
+ 'open-chatgpt',
+ 'submit-signup-email',
+ 'fill-password',
+ 'fetch-signup-code',
+ 'fill-profile',
+ 'wait-registration-success',
+ 'sub2api-agent-identity-import',
+ ]);
+ assert.equal(agentSteps.at(-1)?.title, '生成并导入 Agent Identity');
+ assert.equal(agentSteps.at(-1)?.driverId, 'flows/openai/background/steps/sub2api-session-import');
+});
+
test('Plus session strategy swaps the OAuth tail for a single SUB2API import node', () => {
const globalScope = {};
const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope);