From e40e792264d5c946ab443282b35ce3b5abf6e51e Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sat, 23 May 2026 11:14:24 +0800 Subject: [PATCH 1/4] fix(grok): restart and inject sso remotely --- background/logging-status.js | 2 +- core/flow-kernel/logging-status.js | 2 +- flows/grok/background/register-runner.js | 109 ++++++++++++- flows/grok/background/state.js | 22 +++ flows/grok/index.js | 8 +- flows/grok/workflow.js | 10 ++ tests/background-grok-state-module.test.js | 48 ++++++ tests/grok-runner.test.js | 180 ++++++++++++++++++++- 8 files changed, 374 insertions(+), 7 deletions(-) diff --git a/background/logging-status.js b/background/logging-status.js index 64bd29fb..d98ffd95 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -134,7 +134,7 @@ function isRestartCurrentAttemptError(error) { const message = String(typeof error === 'string' ? error : error?.message || ''); - return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(message); + return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::|GROK_RESTART_CURRENT_ATTEMPT::/i.test(message); } function isSignupUserAlreadyExistsFailure(error) { diff --git a/core/flow-kernel/logging-status.js b/core/flow-kernel/logging-status.js index 64bd29fb..d98ffd95 100644 --- a/core/flow-kernel/logging-status.js +++ b/core/flow-kernel/logging-status.js @@ -134,7 +134,7 @@ function isRestartCurrentAttemptError(error) { const message = String(typeof error === 'string' ? error : error?.message || ''); - return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(message); + return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::|GROK_RESTART_CURRENT_ATTEMPT::/i.test(message); } function isSignupUserAlreadyExistsFailure(error) { diff --git a/flows/grok/background/register-runner.js b/flows/grok/background/register-runner.js index f4bb501f..469a281a 100644 --- a/flows/grok/background/register-runner.js +++ b/flows/grok/background/register-runner.js @@ -1,6 +1,6 @@ (function attachBackgroundGrokRegisterRunner(root, factory) { root.MultiPageBackgroundGrokRegisterRunner = factory(root); -})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokRegisterRunnerModule() { +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokRegisterRunnerModule(root) { const GROK_SIGNUP_URL = 'https://accounts.x.ai/sign-up?redirect=grok-com'; const GROK_REGISTER_PAGE_SOURCE_ID = 'grok-register-page'; const DEFAULT_GROK_PAGE_TIMEOUT_MS = 90 * 1000; @@ -36,6 +36,7 @@ chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null), completeNodeFromBackground, ensureContentScriptReadyOnTab = null, + fetchImpl = (...args) => fetch(...args), generatePassword = null, generateRandomName = null, getState = async () => ({}), @@ -57,6 +58,8 @@ markCurrentRegistrationAccountUsed = null, } = deps; + let remoteAccountInjectApi = null; + if (typeof completeNodeFromBackground !== 'function') { throw new Error('Grok register runner requires completeNodeFromBackground.'); } @@ -94,6 +97,42 @@ }; } + function getRemoteAccountInjectApi() { + if (remoteAccountInjectApi) { + return remoteAccountInjectApi; + } + const factory = deps.createRemoteAccountInjectApi + || root.MultiPageBackgroundRemoteAccountInjectApi?.createRemoteAccountInjectApi; + if (typeof factory !== 'function') { + throw new Error('远程账号注入接口模块未加载。'); + } + remoteAccountInjectApi = factory({ fetchImpl }); + return remoteAccountInjectApi; + } + + function createRestartCurrentAttemptError(message) { + const prefix = root.MultiPageBackgroundGrokState?.GROK_RESTART_CURRENT_ATTEMPT_ERROR_PREFIX + || 'GROK_RESTART_CURRENT_ATTEMPT::'; + return new Error(`${prefix}${message}`); + } + + function isRestartCurrentAttemptError(error) { + const prefix = root.MultiPageBackgroundGrokState?.GROK_RESTART_CURRENT_ATTEMPT_ERROR_PREFIX + || 'GROK_RESTART_CURRENT_ATTEMPT::'; + return getErrorMessage(error).startsWith(prefix); + } + + async function markRestartCurrentAttempt(message) { + const latestState = await getState(); + const patch = typeof root.MultiPageBackgroundGrokState?.buildRestartCurrentAttemptPatch === 'function' + ? root.MultiPageBackgroundGrokState.buildRestartCurrentAttemptPatch(latestState) + : {}; + if (Object.keys(patch).length) { + await persistState(patch); + } + return createRestartCurrentAttemptError(message); + } + async function completeNode(nodeId, patch = {}) { await persistState(patch); await completeNodeFromBackground(nodeId, patch); @@ -610,7 +649,7 @@ ssoCookie = cleanString(result?.ssoCookie); } if (!ssoCookie) { - throw new Error('未找到 x.ai/grok sso Cookie。'); + throw await markRestartCurrentAttempt('Grok 注册未产出 sso Cookie,需要重新开始当前轮。'); } const latestState = await getState(); @@ -657,6 +696,10 @@ await completeNode(nodeId, completionPatch); } catch (error) { const message = getErrorMessage(error); + if (isRestartCurrentAttemptError(error)) { + await log(`步骤 5:${message}`, 'warn', nodeId); + throw error; + } await persistState(buildGrokRuntimePatch({ session: { lastError: message, @@ -670,9 +713,71 @@ } } + async function executeGrokRemoteSsoInject(state = {}) { + const nodeId = cleanString(state?.nodeId) || 'grok-remote-sso-inject'; + const currentState = await getExecutionState(state); + const url = cleanString(currentState.grokRemoteAccountInjectUrl); + const adminKey = cleanString(currentState.grokRemoteAccountInjectAdminKey); + if (!url || !adminKey) { + await log('步骤 6:Grok 远程 SSO 注入未配置 URL 或管理员密钥,已跳过。', 'info', nodeId); + await completeNode(nodeId, { + grokRemoteSsoInjectSkipped: true, + grokRemoteSsoInjectReason: !url ? 'missing_url' : 'missing_admin_key', + }); + return; + } + + const ssoCookie = cleanString( + currentState.grokSsoCookie + || currentState.runtimeState?.flowState?.grok?.sso?.currentCookie + || currentState.flowState?.grok?.sso?.currentCookie + ); + if (!ssoCookie) { + throw new Error('缺少 Grok SSO Cookie,请先执行步骤 5。'); + } + + try { + await log('步骤 6:正在将 Grok SSO 注入远程账号池...', 'info', nodeId); + await getRemoteAccountInjectApi().injectRemoteAccounts({ + url, + adminKey, + timeoutMs: 30000, + body: { + accounts: [{ + token: ssoCookie, + provider: 'grok', + type: 'basic', + }], + strategy: 'merge', + source_id: 'flowpilot-grok-sso', + source_name: 'FlowPilot Grok SSO', + provider: 'grok', + }, + }); + await log('步骤 6:Grok SSO 远程注入完成:已提交 1 个 SSO。', 'ok', nodeId); + await completeNode(nodeId, { + grokRemoteSsoInjectSkipped: false, + grokRemoteSsoInjectSubmitted: 1, + }); + } catch (error) { + const message = getErrorMessage(error); + await persistState(buildGrokRuntimePatch({ + session: { + lastError: message, + }, + register: { + status: 'error', + }, + })); + await log(`步骤 6:${message}`, 'error', nodeId); + throw error; + } + } + return { executeGrokExtractSsoCookie, executeGrokOpenSignupPage, + executeGrokRemoteSsoInject, executeGrokSubmitEmail, executeGrokSubmitProfile, executeGrokSubmitVerificationCode, diff --git a/flows/grok/background/state.js b/flows/grok/background/state.js index 8eb5507a..772e613d 100644 --- a/flows/grok/background/state.js +++ b/flows/grok/background/state.js @@ -1,6 +1,16 @@ (function attachBackgroundGrokState(root, factory) { root.MultiPageBackgroundGrokState = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokStateModule() { + const GROK_RESTART_CURRENT_ATTEMPT_ERROR_PREFIX = 'GROK_RESTART_CURRENT_ATTEMPT::'; + const GROK_REGISTER_NODE_IDS = Object.freeze([ + 'grok-open-signup-page', + 'grok-submit-email', + 'grok-submit-verification-code', + 'grok-submit-profile', + 'grok-extract-sso-cookie', + 'grok-remote-sso-inject', + ]); + function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } @@ -277,6 +287,7 @@ return buildRegisterOnlyResetPatch(currentState, {}); case 'grok-submit-profile': case 'grok-extract-sso-cookie': + case 'grok-remote-sso-inject': return buildSsoResetPatch(currentState); default: return {}; @@ -359,11 +370,22 @@ return buildRuntimeStatePatch(currentState, nextRuntimeState); } + function buildRestartCurrentAttemptPatch(currentState = {}) { + return { + currentNodeId: '', + nodeStatuses: Object.fromEntries(GROK_REGISTER_NODE_IDS.map((nodeId) => [nodeId, 'pending'])), + ...buildFreshKeepState(currentState), + }; + } + return { + GROK_REGISTER_NODE_IDS, + GROK_RESTART_CURRENT_ATTEMPT_ERROR_PREFIX, applyNodeCompletionPayload, buildDefaultRuntimeState, buildDownstreamResetPatch, buildFreshKeepState, + buildRestartCurrentAttemptPatch, buildRuntimeStatePatch, buildSessionStatePatch, buildStateView, diff --git a/flows/grok/index.js b/flows/grok/index.js index a06eac82..26a7bb1c 100644 --- a/flows/grok/index.js +++ b/flows/grok/index.js @@ -94,6 +94,7 @@ 'grok-submit-verification-code', 'grok-submit-profile', 'grok-extract-sso-cookie', + 'grok-remote-sso-inject', ], }, 'flows/grok/background/register-runner': { @@ -104,16 +105,19 @@ 'grok-submit-verification-code', 'grok-submit-profile', 'grok-extract-sso-cookie', + 'grok-remote-sso-inject', ], }, }, defaultTargetId: 'webchat2api', settingsDefaults: { + grokRemoteAccountInjectUrl: '', + grokRemoteAccountInjectAdminKey: '', autoRun: { stepExecutionRange: { enabled: false, fromStep: 1, - toStep: 5, + toStep: 6, }, }, }, @@ -123,6 +127,8 @@ label: 'webchat2api', rowIds: [ 'row-grok-sso-settings', + 'row-grok-remote-account-inject-url', + 'row-grok-remote-account-inject-admin-key', ], }, 'grok-runtime-status': { diff --git a/flows/grok/workflow.js b/flows/grok/workflow.js index f872a75f..49fb78f3 100644 --- a/flows/grok/workflow.js +++ b/flows/grok/workflow.js @@ -64,6 +64,16 @@ command: 'grok-extract-sso-cookie', flowId: 'grok', }, + { + id: 6, + order: 60, + key: 'grok-remote-sso-inject', + title: '远程注入 SSO', + sourceId: 'grok-register-page', + driverId: 'flows/grok/background/register-runner', + command: 'grok-remote-sso-inject', + flowId: 'grok', + }, ], }); diff --git a/tests/background-grok-state-module.test.js b/tests/background-grok-state-module.test.js index 2eb3674d..29ebb97e 100644 --- a/tests/background-grok-state-module.test.js +++ b/tests/background-grok-state-module.test.js @@ -156,3 +156,51 @@ test('grok downstream reset clears only the state owned by the restarted tail', assert.equal(emailPatch.grokRegisterStatus, ''); assert.equal(emailPatch.grokRegisterTabId, 7); }); + +test('grok restart-current-attempt patch resets lifecycle progress to step 1', () => { + const api = loadGrokStateApi(); + const patch = api.buildRestartCurrentAttemptPatch({ + currentNodeId: 'grok-extract-sso-cookie', + nodeStatuses: Object.fromEntries(api.GROK_REGISTER_NODE_IDS.map((nodeId) => [nodeId, 'completed'])), + runtimeState: { + flowState: { + grok: { + session: { + registerTabId: 42, + pageState: 'profile_submitted', + pageUrl: 'https://accounts.x.ai/sign-up', + lastError: 'old-error', + }, + register: { + email: 'grok@example.com', + firstName: 'Ada', + lastName: 'Lovelace', + password: 'Secret123!', + verificationRequestedAt: 123, + verificationCode: 'ABC123', + status: 'profile_submitted', + completedAt: 456, + }, + sso: { + currentCookie: 'existing-cookie', + cookies: ['existing-cookie'], + extractedAt: 789, + }, + }, + }, + }, + }); + + assert.equal(patch.currentNodeId, ''); + assert.deepEqual( + patch.nodeStatuses, + Object.fromEntries(api.GROK_REGISTER_NODE_IDS.map((nodeId) => [nodeId, 'pending'])) + ); + assert.equal(patch.grokRegisterTabId, null); + assert.equal(patch.grokPageState, ''); + assert.equal(patch.grokEmail, ''); + assert.equal(patch.grokRegisterStatus, ''); + assert.equal(patch.grokSsoCookie, 'existing-cookie'); + assert.deepEqual(patch.grokSsoCookies, ['existing-cookie']); + assert.equal(patch.runtimeState.flowState.grok.register.email, ''); +}); diff --git a/tests/grok-runner.test.js b/tests/grok-runner.test.js index 1b5d652d..486b8b2f 100644 --- a/tests/grok-runner.test.js +++ b/tests/grok-runner.test.js @@ -2,9 +2,8 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); -function loadGrokRunnerApi() { +function loadGrokRunnerApi(globalScope = {}) { const source = fs.readFileSync('flows/grok/background/register-runner.js', 'utf8'); - const globalScope = {}; return new Function('self', `${source}; return self.MultiPageBackgroundGrokRegisterRunner;`)(globalScope); } @@ -176,6 +175,183 @@ test('grok SSO extraction accumulates unique cookies without logging the secret assert.equal(logs.some(({ message }) => message.includes('new-cookie')), false); }); +test('grok sso extraction failure marks retry as restart-current-attempt and resets node progress', async () => { + const grokStateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8'); + const globalScope = {}; + globalScope.MultiPageBackgroundGrokState = new Function('self', `${grokStateSource}; return self.MultiPageBackgroundGrokState;`)(globalScope); + const api = loadGrokRunnerApi(globalScope); + const logs = []; + let completedPayload = null; + let currentState = { + activeFlowId: 'grok', + flowId: 'grok', + currentNodeId: 'grok-extract-sso-cookie', + nodeStatuses: Object.fromEntries(globalScope.MultiPageBackgroundGrokState.GROK_REGISTER_NODE_IDS.map((nodeId) => [nodeId, 'completed'])), + grokRegisterTabId: 303, + grokEmail: 'grok-user@example.com', + grokSsoCookies: ['old-cookie'], + runtimeState: { + flowState: { + grok: { + session: { + registerTabId: 303, + pageState: 'profile_submitted', + }, + register: { + email: 'grok-user@example.com', + status: 'profile_submitted', + }, + sso: { + cookies: ['old-cookie'], + }, + }, + }, + }, + }; + const runner = api.createGrokRegisterRunner({ + addLog: async (message, level) => { + logs.push({ message, level }); + }, + chrome: { + cookies: { + get: async () => null, + }, + tabs: { + get: async (tabId) => ({ id: tabId }), + update: async () => {}, + }, + }, + completeNodeFromBackground: async (_nodeId, payload) => { + completedPayload = payload; + }, + ensureContentScriptReadyOnTab: async () => {}, + getState: async () => currentState, + getTabId: async () => 303, + isTabAlive: async () => true, + registerTab: async () => {}, + sendToContentScriptResilient: async () => ({ state: 'profile_submitted', ssoCookie: '' }), + setState: async (patch) => { + currentState = { + ...currentState, + ...patch, + nodeStatuses: patch.nodeStatuses ? { ...patch.nodeStatuses } : currentState.nodeStatuses, + runtimeState: patch.runtimeState ? JSON.parse(JSON.stringify(patch.runtimeState)) : currentState.runtimeState, + }; + }, + sleepWithStop: async () => {}, + waitForTabStableComplete: async () => {}, + }); + + await assert.rejects( + () => runner.executeGrokExtractSsoCookie({ nodeId: 'grok-extract-sso-cookie', ...currentState }), + /GROK_RESTART_CURRENT_ATTEMPT::Grok 注册未产出 sso Cookie/ + ); + + assert.equal(completedPayload, null); + assert.equal(currentState.currentNodeId, ''); + assert.deepEqual( + currentState.nodeStatuses, + Object.fromEntries(globalScope.MultiPageBackgroundGrokState.GROK_REGISTER_NODE_IDS.map((nodeId) => [nodeId, 'pending'])) + ); + assert.equal(currentState.grokRegisterTabId, null); + assert.equal(currentState.grokEmail, ''); + assert.equal(currentState.grokRegisterStatus, ''); + assert.equal(currentState.grokSsoCookie, ''); + assert.deepEqual(currentState.grokSsoCookies, ['old-cookie']); + assert.ok(logs.some(({ message, level }) => level === 'warn' && /GROK_RESTART_CURRENT_ATTEMPT::/.test(message))); +}); + +test('grok remote SSO inject skips cleanly when config is missing', async () => { + const api = loadGrokRunnerApi(); + const completed = []; + const logs = []; + let injectCalled = false; + + const runner = api.createGrokRegisterRunner({ + addLog: async (message, level = 'info', options = {}) => { + logs.push({ message, level, nodeId: options.nodeId }); + }, + completeNodeFromBackground: async (nodeId, payload) => { + completed.push({ nodeId, payload }); + }, + createRemoteAccountInjectApi: () => ({ + injectRemoteAccounts: async () => { + injectCalled = true; + return { skipped: false }; + }, + }), + getState: async () => ({}), + setState: async () => {}, + }); + + await runner.executeGrokRemoteSsoInject({ + nodeId: 'grok-remote-sso-inject', + grokSsoCookie: 'sso-cookie', + grokRemoteAccountInjectUrl: '', + grokRemoteAccountInjectAdminKey: 'admin-secret', + }); + + assert.equal(injectCalled, false); + assert.deepEqual(completed, [{ + nodeId: 'grok-remote-sso-inject', + payload: { + grokRemoteSsoInjectSkipped: true, + grokRemoteSsoInjectReason: 'missing_url', + }, + }]); + assert.equal(logs.some((entry) => /已跳过/.test(entry.message)), true); +}); + +test('grok remote SSO inject posts Grok account payload', async () => { + const api = loadGrokRunnerApi(); + const completed = []; + const injected = []; + + const runner = api.createGrokRegisterRunner({ + addLog: async () => {}, + completeNodeFromBackground: async (nodeId, payload) => { + completed.push({ nodeId, payload }); + }, + createRemoteAccountInjectApi: () => ({ + injectRemoteAccounts: async (options) => { + injected.push(options); + return { skipped: false }; + }, + }), + getState: async () => ({}), + setState: async () => {}, + }); + + await runner.executeGrokRemoteSsoInject({ + nodeId: 'grok-remote-sso-inject', + grokSsoCookie: 'sso-cookie', + grokRemoteAccountInjectUrl: 'https://remote.example.com/admin', + grokRemoteAccountInjectAdminKey: 'admin-secret', + }); + + assert.equal(injected.length, 1); + assert.equal(injected[0].url, 'https://remote.example.com/admin'); + assert.equal(injected[0].adminKey, 'admin-secret'); + assert.deepEqual(injected[0].body, { + accounts: [{ + token: 'sso-cookie', + provider: 'grok', + type: 'basic', + }], + strategy: 'merge', + source_id: 'flowpilot-grok-sso', + source_name: 'FlowPilot Grok SSO', + provider: 'grok', + }); + assert.deepEqual(completed, [{ + nodeId: 'grok-remote-sso-inject', + payload: { + grokRemoteSsoInjectSkipped: false, + grokRemoteSsoInjectSubmitted: 1, + }, + }]); +}); + test('grok register runner requires background node completion dependency', () => { const api = loadGrokRunnerApi(); assert.throws( From 82ed15df6738dcc39444237ecc16465bd05e2334 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sat, 23 May 2026 11:14:55 +0800 Subject: [PATCH 2/4] feat(openai): add remote account injection --- background.js | 44 ++- background/remote-account-inject-api.js | 116 +++++++ core/flow-kernel/settings-schema.js | 52 ++++ .../background/steps/remote-account-inject.js | 292 ++++++++++++++++++ flows/openai/index.js | 17 +- flows/openai/workflow.js | 33 +- tests/background-step-registry.test.js | 7 +- tests/flow-registry-settings-schema.test.js | 21 +- tests/openai-remote-account-inject.test.js | 128 ++++++++ tests/remote-account-inject-api.test.js | 102 ++++++ tests/source-registry-module.test.js | 2 + tests/step-definitions-module.test.js | 61 ++-- 12 files changed, 842 insertions(+), 33 deletions(-) create mode 100644 background/remote-account-inject-api.js create mode 100644 flows/openai/background/steps/remote-account-inject.js create mode 100644 tests/openai-remote-account-inject.test.js create mode 100644 tests/remote-account-inject-api.test.js diff --git a/background.js b/background.js index fa8c68eb..d02a821f 100644 --- a/background.js +++ b/background.js @@ -31,6 +31,7 @@ importScripts( 'background/ip-proxy-core.js', 'background/sub2api-api.js', 'background/cpa-api.js', + 'background/remote-account-inject-api.js', 'background/panel-bridge.js', 'background/registration-email-state.js', 'core/flow-kernel/workflow-engine.js', @@ -72,6 +73,7 @@ importScripts( 'flows/openai/background/steps/paypal-approve.js', 'flows/openai/background/steps/gopay-approve.js', 'flows/openai/background/steps/plus-return-confirm.js', + 'flows/openai/background/steps/remote-account-inject.js', 'flows/openai/background/steps/sub2api-session-import.js', 'flows/openai/background/steps/cpa-session-import.js', 'flows/openai/background/steps/oauth-login.js', @@ -1293,6 +1295,10 @@ const PERSISTED_SETTING_DEFAULTS = { ipProxyRegion: '', codex2apiUrl: DEFAULT_CODEX2API_URL, codex2apiAdminKey: '', + remoteAccountInjectUrl: '', + remoteAccountInjectAdminKey: '', + grokRemoteAccountInjectUrl: '', + grokRemoteAccountInjectAdminKey: '', customPassword: '', plusModeEnabled: false, plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD, @@ -1465,6 +1471,10 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([ 'sub2apiDefaultProxyName', 'codex2apiUrl', 'codex2apiAdminKey', + 'remoteAccountInjectUrl', + 'remoteAccountInjectAdminKey', + 'grokRemoteAccountInjectUrl', + 'grokRemoteAccountInjectAdminKey', 'customPassword', 'signupMethod', 'phoneVerificationEnabled', @@ -3249,6 +3259,14 @@ function normalizePersistentSettingValue(key, value) { return normalizeCodex2ApiUrl(value); case 'codex2apiAdminKey': return String(value || '').trim(); + case 'remoteAccountInjectUrl': + return String(value || '').trim(); + case 'remoteAccountInjectAdminKey': + return String(value || '').trim(); + case 'grokRemoteAccountInjectUrl': + return String(value || '').trim(); + case 'grokRemoteAccountInjectAdminKey': + return String(value || '').trim(); case 'customPassword': return String(value || ''); case 'signupMethod': @@ -3822,6 +3840,10 @@ function buildSettingsStatePatchFromFlatUpdates(updates = {}) { assignIfUpdated('sub2apiDefaultProxyName', ['flows', 'openai', 'targets', 'sub2api', 'sub2apiDefaultProxyName']); assignIfUpdated('codex2apiUrl', ['flows', 'openai', 'targets', 'codex2api', 'codex2apiUrl']); assignIfUpdated('codex2apiAdminKey', ['flows', 'openai', 'targets', 'codex2api', 'codex2apiAdminKey']); + assignIfUpdated('remoteAccountInjectUrl', ['flows', 'openai', 'remoteAccountInjectUrl']); + assignIfUpdated('remoteAccountInjectAdminKey', ['flows', 'openai', 'remoteAccountInjectAdminKey']); + assignIfUpdated('grokRemoteAccountInjectUrl', ['flows', 'grok', 'grokRemoteAccountInjectUrl']); + assignIfUpdated('grokRemoteAccountInjectAdminKey', ['flows', 'grok', 'grokRemoteAccountInjectAdminKey']); assignIfUpdated('customPassword', ['services', 'account', 'customPassword']); assignIfUpdated('signupMethod', ['flows', 'openai', 'signup', 'signupMethod']); assignIfUpdated('phoneVerificationEnabled', ['flows', 'openai', 'signup', 'phoneVerificationEnabled']); @@ -9379,7 +9401,7 @@ function isRestartCurrentAttemptError(error) { return loggingStatus.isRestartCurrentAttemptError(error); } const message = String(typeof error === 'string' ? error : error?.message || ''); - return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(message); + return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::|GROK_RESTART_CURRENT_ATTEMPT::/i.test(message); } function isSignupPhonePasswordMismatchFailure(error) { @@ -10913,6 +10935,7 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'plus-checkout-return', 'sub2api-session-import', 'cpa-session-import', + 'remote-account-inject', 'oauth-login', 'fetch-login-code', 'post-login-phone-verification', @@ -10936,6 +10959,7 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'grok-submit-verification-code', 'grok-submit-profile', 'grok-extract-sso-cookie', + 'grok-remote-sso-inject', ]); const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([ 'fill-password', @@ -12034,6 +12058,7 @@ const AUTO_RUN_NODE_DELAYS = Object.freeze({ 'gopay-subscription-confirm': 2000, 'paypal-approve': 2000, 'plus-checkout-return': 1000, + 'remote-account-inject': 0, 'sub2api-session-import': 0, 'cpa-session-import': 0, 'oauth-login': 2000, @@ -13914,6 +13939,20 @@ const cpaSessionImportExecutor = self.MultiPageBackgroundCpaSessionImport?.creat throwIfStopped, waitForTabCompleteUntilStopped, }); +const remoteAccountInjectExecutor = self.MultiPageBackgroundRemoteAccountInject?.createRemoteAccountInjectExecutor({ + addLog, + chrome, + completeNodeFromBackground, + ensureContentScriptReadyOnTabUntilStopped, + fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getTabId, + isTabAlive, + registerTab, + sendTabMessageUntilStopped, + sleepWithStop, + throwIfStopped, + waitForTabCompleteUntilStopped, +}); const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKiroRegisterRunner({ addLog, chrome, @@ -13943,6 +13982,7 @@ const grokRegisterRunner = self.MultiPageBackgroundGrokRegisterRunner?.createGro chrome, ensureContentScriptReadyOnTab, completeNodeFromBackground, + fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null, generatePassword, generateRandomName, getTabId, @@ -14089,6 +14129,7 @@ const stepExecutorsByKey = { ? goPayApproveExecutor.executeGoPayApprove(state) : payPalApproveExecutor.executePayPalApprove(state), 'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state), + 'remote-account-inject': (state) => remoteAccountInjectExecutor.executeRemoteAccountInject(state), 'sub2api-session-import': (state) => sub2ApiSessionImportExecutor.executeSub2ApiSessionImport(state), 'cpa-session-import': (state) => cpaSessionImportExecutor.executeCpaSessionImport(state), 'oauth-login': (state) => step7Executor.executeStep7(state), @@ -14115,6 +14156,7 @@ const stepExecutorsByKey = { 'grok-submit-verification-code': (state) => grokRegisterRunner.executeGrokSubmitVerificationCode(state), 'grok-submit-profile': (state) => grokRegisterRunner.executeGrokSubmitProfile(state), 'grok-extract-sso-cookie': (state) => grokRegisterRunner.executeGrokExtractSsoCookie(state), + 'grok-remote-sso-inject': (state) => grokRegisterRunner.executeGrokRemoteSsoInject(state), }; const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({ addLog, diff --git a/background/remote-account-inject-api.js b/background/remote-account-inject-api.js new file mode 100644 index 00000000..7697bd4d --- /dev/null +++ b/background/remote-account-inject-api.js @@ -0,0 +1,116 @@ +(function attachBackgroundRemoteAccountInjectApi(root, factory) { + root.MultiPageBackgroundRemoteAccountInjectApi = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundRemoteAccountInjectApiModule() { + function normalizeString(value = '') { + return String(value ?? '').trim(); + } + + function normalizeRemoteAccountInjectUrl(rawUrl = '') { + const value = normalizeString(rawUrl); + if (!value) { + return ''; + } + const withProtocol = /^https?:\/\//i.test(value) ? value : `http://${value}`; + let parsed; + try { + parsed = new URL(withProtocol); + } catch (error) { + throw new Error('远程账号注入地址格式无效,请检查配置。'); + } + if (!/^https?:$/i.test(parsed.protocol)) { + throw new Error('远程账号注入地址仅支持 HTTP/HTTPS。'); + } + return `${parsed.origin}/api/remote-account/inject`; + } + + function getRemoteAccountInjectErrorMessage(payload, responseStatus = 500) { + const candidates = [ + payload?.message, + payload?.detail, + payload?.error, + payload?.reason, + ]; + const message = candidates.map(normalizeString).find(Boolean); + return message || `远程账号注入请求失败(HTTP ${responseStatus})。`; + } + + function createRemoteAccountInjectApi(deps = {}) { + const { + fetchImpl = (...args) => fetch(...args), + } = deps; + + async function injectRemoteAccounts(options = {}) { + const endpoint = normalizeRemoteAccountInjectUrl(options.url); + const adminKey = normalizeString(options.adminKey); + if (!endpoint) { + return { skipped: true, reason: 'missing_url' }; + } + if (!adminKey) { + return { skipped: true, reason: 'missing_admin_key' }; + } + + const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000)); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetchImpl(endpoint, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${adminKey}`, + }, + body: JSON.stringify(options.body || {}), + signal: controller.signal, + }); + + const text = await response.text(); + let payload = null; + try { + payload = text ? JSON.parse(text) : null; + } catch (error) { + payload = null; + } + + if (payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'code')) { + if (Number(payload.code) === 0) { + return { + skipped: false, + endpoint, + payload: payload.data, + }; + } + throw new Error(getRemoteAccountInjectErrorMessage(payload, response.status)); + } + + if (!response.ok) { + throw new Error(getRemoteAccountInjectErrorMessage(payload, response.status)); + } + + return { + skipped: false, + endpoint, + payload, + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error('远程账号注入请求超时,请稍后重试。'); + } + throw error; + } finally { + clearTimeout(timer); + } + } + + return { + injectRemoteAccounts, + normalizeRemoteAccountInjectUrl, + }; + } + + return { + createRemoteAccountInjectApi, + normalizeRemoteAccountInjectUrl, + }; +}); diff --git a/core/flow-kernel/settings-schema.js b/core/flow-kernel/settings-schema.js index 8571706d..651811a5 100644 --- a/core/flow-kernel/settings-schema.js +++ b/core/flow-kernel/settings-schema.js @@ -195,6 +195,10 @@ ); const base = { selectedTargetId: defaultTargetId, + ...(flowId === 'grok' ? { + grokRemoteAccountInjectUrl: '', + grokRemoteAccountInjectAdminKey: '', + } : {}), targets: buildDefaultTargets(flowId), autoRun: { stepExecutionRange: getDefaultStepExecutionRange(flowId), @@ -202,6 +206,8 @@ }; if (flowId === 'openai') { return mergePlainObjects(base, { + remoteAccountInjectUrl: '', + remoteAccountInjectAdminKey: '', signup: { signupMethod: 'email', phoneVerificationEnabled: false, @@ -295,6 +301,20 @@ apiKey: String(targetState.apiKey ?? ''), }; } + if (flowId === 'openai') { + return { + ...targetState, + remoteAccountInjectUrl: String(targetState.remoteAccountInjectUrl ?? '').trim(), + remoteAccountInjectAdminKey: String(targetState.remoteAccountInjectAdminKey ?? '').trim(), + }; + } + if (flowId === 'grok') { + return { + ...targetState, + grokRemoteAccountInjectUrl: String(targetState.grokRemoteAccountInjectUrl ?? '').trim(), + grokRemoteAccountInjectAdminKey: String(targetState.grokRemoteAccountInjectAdminKey ?? '').trim(), + }; + } return targetState; } @@ -398,6 +418,16 @@ }; return { ...currentFlow, + remoteAccountInjectUrl: String( + input?.remoteAccountInjectUrl + ?? currentFlow.remoteAccountInjectUrl + ?? defaults.flows.openai.remoteAccountInjectUrl + ).trim(), + remoteAccountInjectAdminKey: String( + input?.remoteAccountInjectAdminKey + ?? currentFlow.remoteAccountInjectAdminKey + ?? defaults.flows.openai.remoteAccountInjectAdminKey + ).trim(), targets: { ...currentFlow.targets, cpa: normalizeFlowTargetState('openai', 'cpa', cpaSource, defaults.flows.openai.targets.cpa), @@ -537,6 +567,23 @@ if (normalized.flows.kiro) { normalized.flows.kiro = normalizeKiroSettings(input, defaults, normalized.flows.kiro); } + if (normalized.flows.grok) { + normalized.flows.grok = { + ...normalized.flows.grok, + grokRemoteAccountInjectUrl: String( + input?.grokRemoteAccountInjectUrl + ?? normalized.flows.grok.grokRemoteAccountInjectUrl + ?? defaults.flows.grok?.grokRemoteAccountInjectUrl + ?? '' + ).trim(), + grokRemoteAccountInjectAdminKey: String( + input?.grokRemoteAccountInjectAdminKey + ?? normalized.flows.grok.grokRemoteAccountInjectAdminKey + ?? defaults.flows.grok?.grokRemoteAccountInjectAdminKey + ?? '' + ).trim(), + }; + } return normalized; } @@ -601,6 +648,7 @@ }; const openaiState = normalizedState.flows.openai || buildDefaultFlowSettings('openai'); const kiroState = normalizedState.flows.kiro || buildDefaultFlowSettings('kiro'); + const grokState = normalizedState.flows.grok || buildDefaultFlowSettings('grok'); next.activeFlowId = normalizedState.activeFlowId; next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId); next.vpsUrl = openaiState.targets.cpa?.vpsUrl || ''; @@ -615,6 +663,10 @@ next.sub2apiDefaultProxyName = openaiState.targets.sub2api?.sub2apiDefaultProxyName || ''; next.codex2apiUrl = openaiState.targets.codex2api?.codex2apiUrl || ''; next.codex2apiAdminKey = openaiState.targets.codex2api?.codex2apiAdminKey || ''; + next.remoteAccountInjectUrl = openaiState.remoteAccountInjectUrl || ''; + next.remoteAccountInjectAdminKey = openaiState.remoteAccountInjectAdminKey || ''; + next.grokRemoteAccountInjectUrl = grokState.grokRemoteAccountInjectUrl || ''; + next.grokRemoteAccountInjectAdminKey = grokState.grokRemoteAccountInjectAdminKey || ''; next.customPassword = normalizedState.services.account.customPassword; next.signupMethod = openaiState.signup?.signupMethod || 'email'; next.phoneVerificationEnabled = Boolean(openaiState.signup?.phoneVerificationEnabled); diff --git a/flows/openai/background/steps/remote-account-inject.js b/flows/openai/background/steps/remote-account-inject.js new file mode 100644 index 00000000..8ffc221d --- /dev/null +++ b/flows/openai/background/steps/remote-account-inject.js @@ -0,0 +1,292 @@ +(function attachBackgroundRemoteAccountInject(root, factory) { + root.MultiPageBackgroundRemoteAccountInject = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundRemoteAccountInjectModule() { + const PLUS_CHECKOUT_SOURCE = 'plus-checkout'; + const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'flows/openai/content/plus-checkout.js']; + + function createRemoteAccountInjectExecutor(deps = {}) { + const { + addLog: rawAddLog = async () => {}, + chrome, + completeNodeFromBackground, + ensureContentScriptReadyOnTabUntilStopped, + getTabId, + isTabAlive, + registerTab, + sendTabMessageUntilStopped, + sleepWithStop = async () => {}, + throwIfStopped = () => {}, + waitForTabCompleteUntilStopped = async () => {}, + } = deps; + + let remoteAccountInjectApi = null; + + function normalizeString(value = '') { + return String(value ?? '').trim(); + } + + function addStepLog(step, message, level = 'info') { + return rawAddLog(message, level, { + step, + stepKey: 'remote-account-inject', + }); + } + + function getRemoteAccountInjectApi() { + if (remoteAccountInjectApi) { + return remoteAccountInjectApi; + } + const factory = deps.createRemoteAccountInjectApi + || self.MultiPageBackgroundRemoteAccountInjectApi?.createRemoteAccountInjectApi; + if (typeof factory !== 'function') { + throw new Error('远程账号注入接口模块未加载。'); + } + remoteAccountInjectApi = factory({ + fetchImpl: deps.fetchImpl, + }); + return remoteAccountInjectApi; + } + + function resolveVisibleStep(state = {}) { + const visibleStep = Math.floor(Number(state?.visibleStep) || 0); + return visibleStep > 0 ? visibleStep : 7; + } + + 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 (error) { + 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 (error) { + 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 页面。'); + } + + async function getResolvedSessionTab(tabId, visibleStep) { + const tab = await chrome?.tabs?.get?.(tabId).catch(() => null); + if (!tab?.id) { + throw new Error(`步骤 ${visibleStep}:ChatGPT 会话标签页不存在或已关闭,无法远程注入 accessToken。`); + } + if (!isSupportedChatGptSessionUrl(tab.url)) { + throw new Error(`步骤 ${visibleStep}:当前标签页不在 ChatGPT / OpenAI 页面,无法读取当前登录会话。`); + } + return tab; + } + + async function readCurrentChatGptAccessToken(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 会话页完成加载,再继续读取 accessToken...`, + }); + + 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 (!accessToken) { + throw new Error(`步骤 ${visibleStep}:未读取到 ChatGPT accessToken,请确认当前标签页仍处于已登录状态。`); + } + return accessToken; + } + + async function executeRemoteAccountInject(state = {}) { + throwIfStopped(); + const visibleStep = resolveVisibleStep(state); + const nodeId = normalizeString(state?.nodeId) || 'remote-account-inject'; + const url = normalizeString(state?.remoteAccountInjectUrl); + const adminKey = normalizeString(state?.remoteAccountInjectAdminKey); + if (!url || !adminKey) { + await addStepLog(visibleStep, '远程账号注入未配置 URL 或管理员密钥,已跳过。', 'info'); + await completeNodeFromBackground(nodeId, { + remoteAccountInjectSkipped: true, + remoteAccountInjectReason: !url ? 'missing_url' : 'missing_admin_key', + }); + return; + } + + 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 accessToken = await readCurrentChatGptAccessToken(tab.id, visibleStep); + throwIfStopped(); + + const api = getRemoteAccountInjectApi(); + const result = await api.injectRemoteAccounts({ + url, + adminKey, + timeoutMs: 30000, + body: { + tokens: [accessToken], + strategy: 'merge', + source_id: 'flowpilot-codex-at', + source_name: 'FlowPilot Codex AT', + provider: 'gpt', + }, + }); + + if (result?.skipped) { + await addStepLog(visibleStep, '远程账号注入配置不完整,已跳过。', 'info'); + await completeNodeFromBackground(nodeId, { + remoteAccountInjectSkipped: true, + remoteAccountInjectReason: result.reason || 'skipped', + }); + return; + } + + await addStepLog(visibleStep, '远程账号注入完成:已提交 1 个 accessToken。', 'ok'); + await completeNodeFromBackground(nodeId, { + remoteAccountInjectSkipped: false, + remoteAccountInjectSubmitted: 1, + }); + } + + return { + executeRemoteAccountInject, + isSupportedChatGptSessionUrl, + }; + } + + return { + createRemoteAccountInjectExecutor, + }; +}); diff --git a/flows/openai/index.js b/flows/openai/index.js index d98c68e4..111af3f1 100644 --- a/flows/openai/index.js +++ b/flows/openai/index.js @@ -47,7 +47,8 @@ "openai-plus", "openai-phone", "openai-oauth", - "openai-step6" + "openai-step6", + "openai-remote-account-inject" ], "targets": { "cpa": { @@ -266,6 +267,12 @@ } }, "driverDefinitions": { + "flows/openai/background/steps/remote-account-inject": { + "sourceId": "plus-checkout", + "commands": [ + "remote-account-inject" + ] + }, "flows/openai/content/openai-auth": { "sourceId": "openai-auth", "commands": [ @@ -392,6 +399,14 @@ "rowIds": [ "row-step6-cookie-settings" ] + }, + "openai-remote-account-inject": { + "id": "openai-remote-account-inject", + "label": "远程账号注入", + "rowIds": [ + "row-remote-account-inject-url", + "row-remote-account-inject-admin-key" + ] } }, "targetCapabilities": { diff --git a/flows/openai/workflow.js b/flows/openai/workflow.js index ddd27e89..67b2a330 100644 --- a/flows/openai/workflow.js +++ b/flows/openai/workflow.js @@ -11,6 +11,7 @@ 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 REMOTE_ACCOUNT_INJECT_STEP_KEY = 'remote-account-inject'; const PLUS_PAYMENT_STEP_KEY = 'paypal-approve'; const PLUS_REGISTRATION_WAIT_STEP_KEY = 'wait-registration-success'; @@ -3032,6 +3033,29 @@ return steps.filter((step) => !PLUS_PAYMENT_CHAIN_STEP_KEYS.includes(String(step?.key || '').trim())); } + function insertRemoteAccountInjectStep(steps = []) { + if (!Array.isArray(steps) || steps.some((step) => step.key === REMOTE_ACCOUNT_INJECT_STEP_KEY)) { + return steps; + } + const registrationWaitIndex = steps.findIndex((step) => step.key === PLUS_REGISTRATION_WAIT_STEP_KEY); + const oauthLoginIndex = steps.findIndex((step) => step.key === 'oauth-login'); + if (registrationWaitIndex < 0 || oauthLoginIndex < 0 || registrationWaitIndex >= oauthLoginIndex) { + return steps; + } + const sourceStep = steps[registrationWaitIndex] || {}; + const remoteStep = { + id: Number(sourceStep.id) + 1, + order: Number(sourceStep.order) + 5, + key: REMOTE_ACCOUNT_INJECT_STEP_KEY, + title: '远程注入 Access Token', + sourceId: 'plus-checkout', + driverId: 'flows/openai/background/steps/remote-account-inject', + command: REMOTE_ACCOUNT_INJECT_STEP_KEY, + flowId: 'openai', + }; + return steps.flatMap((step, index) => (index === registrationWaitIndex ? [step, remoteStep] : [step])); + } + function reindexModeStepDefinitions(steps = []) { return (Array.isArray(steps) ? steps : []).map((step, index) => ({ ...step, @@ -3200,6 +3224,7 @@ if (isPlusMode) { steps = insertPlusRegistrationWaitStep(steps); } + steps = insertRemoteAccountInjectStep(steps); if ( isPlusMode && normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod) === PLUS_PAYMENT_METHOD_NONE @@ -3212,9 +3237,11 @@ function getAllSteps() { const keyed = new Map(); Object.entries(STEP_VARIANTS).forEach(([variantKey, steps]) => { - const variantSteps = String(variantKey || '').startsWith('plus') - ? insertPlusRegistrationWaitStep(steps) - : steps; + const variantSteps = insertRemoteAccountInjectStep( + String(variantKey || '').startsWith('plus') + ? insertPlusRegistrationWaitStep(steps) + : steps + ); reindexModeStepDefinitions(variantSteps).forEach((step) => { keyed.set(`${step.id}:${step.key}`, step); }); diff --git a/tests/background-step-registry.test.js b/tests/background-step-registry.test.js index a3cf0c98..964462e5 100644 --- a/tests/background-step-registry.test.js +++ b/tests/background-step-registry.test.js @@ -39,7 +39,12 @@ test('background imports node registry and wires the rebuilt Kiro executors', () assert.match(source, /'grok-submit-email': \(state\) => grokRegisterRunner\.executeGrokSubmitEmail\(state\)/); assert.match(source, /'grok-submit-verification-code': \(state\) => grokRegisterRunner\.executeGrokSubmitVerificationCode\(state\)/); assert.match(source, /'grok-submit-profile': \(state\) => grokRegisterRunner\.executeGrokSubmitProfile\(state\)/); + assert.match(source, /background\/remote-account-inject-api\.js/); + assert.match(source, /flows\/openai\/background\/steps\/remote-account-inject\.js/); + assert.match(source, /'remote-account-inject': \(state\) => remoteAccountInjectExecutor\.executeRemoteAccountInject\(state\)/); + assert.match(source, /'wait-registration-success',[\s\S]*'remote-account-inject',[\s\S]*'oauth-login'/); assert.match(source, /'grok-extract-sso-cookie': \(state\) => grokRegisterRunner\.executeGrokExtractSsoCookie\(state\)/); + assert.match(source, /'grok-remote-sso-inject': \(state\) => grokRegisterRunner\.executeGrokRemoteSsoInject\(state\)/); assert.match( source, @@ -47,7 +52,7 @@ test('background imports node registry and wires the rebuilt Kiro executors', () ); assert.match( source, - /'grok-open-signup-page',[\s\S]*'grok-submit-email',[\s\S]*'grok-submit-verification-code',[\s\S]*'grok-submit-profile',[\s\S]*'grok-extract-sso-cookie'/ + /'grok-open-signup-page',[\s\S]*'grok-submit-email',[\s\S]*'grok-submit-verification-code',[\s\S]*'grok-submit-profile',[\s\S]*'grok-extract-sso-cookie',[\s\S]*'grok-remote-sso-inject'/ ); }); diff --git a/tests/flow-registry-settings-schema.test.js b/tests/flow-registry-settings-schema.test.js index 0c9169c9..d2a235a6 100644 --- a/tests/flow-registry-settings-schema.test.js +++ b/tests/flow-registry-settings-schema.test.js @@ -26,7 +26,7 @@ test('flow registry exposes canonical flow and target metadata', () => { assert.equal(flowRegistry.normalizeTargetId('grok', 'anything-else'), 'webchat2api'); assert.deepEqual( flowRegistry.getVisibleGroupIds('openai', 'cpa'), - ['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy'] + ['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-remote-account-inject', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy'] ); assert.deepEqual( flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'), @@ -48,6 +48,10 @@ test('flow registry exposes canonical flow and target metadata', () => { flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds, ['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method'] ); + assert.deepEqual( + flowRegistry.getSettingsGroupDefinition('openai-remote-account-inject')?.rowIds, + ['row-remote-account-inject-url', 'row-remote-account-inject-admin-key'] + ); assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs'); assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true); assert.equal(flowRegistry.getFlowCapabilities('kiro').supportsAccountContribution, true); @@ -78,6 +82,10 @@ test('settings schema normalizes view input into canonical nested namespaces', ( plusAccountAccessStrategy: 'sub2api_codex_session', kiroRsUrl: 'https://kiro.example.com/admin', kiroRsKey: 'secret-key', + remoteAccountInjectUrl: 'https://remote.example.com', + remoteAccountInjectAdminKey: 'remote-admin', + grokRemoteAccountInjectUrl: 'https://grok-remote.example.com', + grokRemoteAccountInjectAdminKey: 'grok-admin', stepExecutionRangeByFlow: { openai: { enabled: true, fromStep: 2, toStep: 9 }, kiro: { enabled: true, fromStep: 1, toStep: 9 }, @@ -95,6 +103,15 @@ test('settings schema normalizes view input into canonical nested namespaces', ( assert.equal(normalized.flows.grok.selectedTargetId, 'webchat2api'); assert.equal(normalized.flows.kiro.targets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin'); assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key'); + assert.equal(normalized.flows.openai.remoteAccountInjectUrl, 'https://remote.example.com'); + assert.equal(normalized.flows.openai.remoteAccountInjectAdminKey, 'remote-admin'); + assert.equal(normalized.flows.grok.grokRemoteAccountInjectUrl, 'https://grok-remote.example.com'); + assert.equal(normalized.flows.grok.grokRemoteAccountInjectAdminKey, 'grok-admin'); + const view = schema.buildSettingsView(normalized); + assert.equal(view.remoteAccountInjectUrl, 'https://remote.example.com'); + assert.equal(view.remoteAccountInjectAdminKey, 'remote-admin'); + assert.equal(view.grokRemoteAccountInjectUrl, 'https://grok-remote.example.com'); + assert.equal(view.grokRemoteAccountInjectAdminKey, 'grok-admin'); assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, { enabled: true, fromStep: 1, @@ -153,7 +170,7 @@ test('settings schema can project canonical state into a read view without legac assert.deepEqual(view.stepExecutionRangeByFlow.grok, { enabled: false, fromStep: 1, - toStep: 5, + toStep: 6, }); }); diff --git a/tests/openai-remote-account-inject.test.js b/tests/openai-remote-account-inject.test.js new file mode 100644 index 00000000..7e26c370 --- /dev/null +++ b/tests/openai-remote-account-inject.test.js @@ -0,0 +1,128 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const test = require('node:test'); + +function loadRemoteAccountInjectStepModule() { + const source = fs.readFileSync('flows/openai/background/steps/remote-account-inject.js', 'utf8'); + return new Function('self', `${source}; return self.MultiPageBackgroundRemoteAccountInject;`)({}); +} + +test('OpenAI remote account inject step skips cleanly when config is missing', async () => { + const moduleApi = loadRemoteAccountInjectStepModule(); + const logs = []; + const completed = []; + let sentMessage = false; + + const executor = moduleApi.createRemoteAccountInjectExecutor({ + addLog: async (message, level = 'info', options = {}) => { + logs.push({ message, level, step: options.step, stepKey: options.stepKey }); + }, + chrome: { tabs: { get: async () => null } }, + completeNodeFromBackground: async (nodeId, payload) => { + completed.push({ nodeId, payload }); + }, + ensureContentScriptReadyOnTabUntilStopped: async () => {}, + getTabId: async () => null, + isTabAlive: async () => false, + registerTab: async () => {}, + sendTabMessageUntilStopped: async () => { + sentMessage = true; + return {}; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + waitForTabCompleteUntilStopped: async () => {}, + }); + + await executor.executeRemoteAccountInject({ + nodeId: 'remote-account-inject', + visibleStep: 7, + remoteAccountInjectUrl: '', + remoteAccountInjectAdminKey: 'admin-secret', + }); + + assert.equal(sentMessage, false); + assert.deepEqual(completed, [{ + nodeId: 'remote-account-inject', + payload: { + remoteAccountInjectSkipped: true, + remoteAccountInjectReason: 'missing_url', + }, + }]); + assert.equal(logs.some((entry) => entry.stepKey === 'remote-account-inject' && /已跳过/.test(entry.message)), true); +}); + +test('OpenAI remote account inject step reads access token and posts GPT payload', async () => { + const moduleApi = loadRemoteAccountInjectStepModule(); + const ensureCalls = []; + const sentMessages = []; + const injected = []; + const completed = []; + + const executor = moduleApi.createRemoteAccountInjectExecutor({ + addLog: async () => {}, + chrome: { + tabs: { + get: async (tabId) => ({ id: tabId, url: 'https://chatgpt.com/?model=gpt-4o' }), + update: async () => {}, + }, + }, + completeNodeFromBackground: async (nodeId, payload) => { + completed.push({ nodeId, payload }); + }, + createRemoteAccountInjectApi: () => ({ + injectRemoteAccounts: async (options) => { + injected.push(options); + return { skipped: false }; + }, + }), + 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 { accessToken: 'openai-access-token' }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + waitForTabCompleteUntilStopped: async () => {}, + }); + + await executor.executeRemoteAccountInject({ + nodeId: 'remote-account-inject', + visibleStep: 7, + plusCheckoutTabId: 91, + remoteAccountInjectUrl: 'https://remote.example.com/panel', + remoteAccountInjectAdminKey: 'admin-secret', + }); + + assert.equal(ensureCalls.length, 1); + assert.deepEqual(sentMessages[0].message, { + type: 'PLUS_CHECKOUT_GET_STATE', + source: 'background', + payload: { + includeSession: true, + includeAccessToken: true, + }, + }); + assert.equal(injected.length, 1); + assert.equal(injected[0].url, 'https://remote.example.com/panel'); + assert.equal(injected[0].adminKey, 'admin-secret'); + assert.deepEqual(injected[0].body, { + tokens: ['openai-access-token'], + strategy: 'merge', + source_id: 'flowpilot-codex-at', + source_name: 'FlowPilot Codex AT', + provider: 'gpt', + }); + assert.deepEqual(completed, [{ + nodeId: 'remote-account-inject', + payload: { + remoteAccountInjectSkipped: false, + remoteAccountInjectSubmitted: 1, + }, + }]); +}); diff --git a/tests/remote-account-inject-api.test.js b/tests/remote-account-inject-api.test.js new file mode 100644 index 00000000..eb36f2f8 --- /dev/null +++ b/tests/remote-account-inject-api.test.js @@ -0,0 +1,102 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const test = require('node:test'); + +function createJsonResponse(payload, status = 200) { + return { + ok: status >= 200 && status < 300, + status, + text: async () => JSON.stringify(payload), + }; +} + +function loadRemoteAccountInjectApiModule() { + const source = fs.readFileSync('background/remote-account-inject-api.js', 'utf8'); + return new Function('self', `${source}; return self.MultiPageBackgroundRemoteAccountInjectApi;`)({}); +} + +test('remote account inject helper posts to normalized origin endpoint with bearer admin key', async () => { + const moduleApi = loadRemoteAccountInjectApiModule(); + const calls = []; + const api = moduleApi.createRemoteAccountInjectApi({ + fetchImpl: async (url, options = {}) => { + calls.push({ url, options }); + return createJsonResponse({ total: 1, added: 1 }); + }, + }); + + const result = await api.injectRemoteAccounts({ + url: 'https://remote.example.com/admin/deep/path', + adminKey: 'admin-secret', + body: { + tokens: ['access-token'], + strategy: 'merge', + provider: 'gpt', + }, + }); + + assert.equal(calls.length, 1); + assert.equal(calls[0].url, 'https://remote.example.com/api/remote-account/inject'); + assert.equal(calls[0].options.method, 'POST'); + assert.equal(calls[0].options.headers.Authorization, 'Bearer admin-secret'); + assert.equal(calls[0].options.headers['Content-Type'], 'application/json'); + assert.deepEqual(JSON.parse(calls[0].options.body), { + tokens: ['access-token'], + strategy: 'merge', + provider: 'gpt', + }); + assert.equal(result.skipped, false); +}); + +test('remote account inject helper skips missing URL or admin key without fetching', async () => { + const moduleApi = loadRemoteAccountInjectApiModule(); + let fetchCalled = false; + const api = moduleApi.createRemoteAccountInjectApi({ + fetchImpl: async () => { + fetchCalled = true; + return createJsonResponse({}); + }, + }); + + assert.deepEqual(await api.injectRemoteAccounts({ adminKey: 'admin-secret' }), { + skipped: true, + reason: 'missing_url', + }); + assert.deepEqual(await api.injectRemoteAccounts({ url: 'http://remote.example.com' }), { + skipped: true, + reason: 'missing_admin_key', + }); + assert.equal(fetchCalled, false); +}); + +test('remote account inject helper unwraps code-zero API envelopes', async () => { + const moduleApi = loadRemoteAccountInjectApiModule(); + const api = moduleApi.createRemoteAccountInjectApi({ + fetchImpl: async () => createJsonResponse({ code: 0, data: { total: 1, added: 1 } }), + }); + + const result = await api.injectRemoteAccounts({ + url: 'http://remote.example.com', + adminKey: 'admin-secret', + body: { tokens: ['access-token'] }, + }); + + assert.equal(result.skipped, false); + assert.deepEqual(result.payload, { total: 1, added: 1 }); +}); + +test('remote account inject helper reports API error messages without exposing secrets', async () => { + const moduleApi = loadRemoteAccountInjectApiModule(); + const api = moduleApi.createRemoteAccountInjectApi({ + fetchImpl: async () => createJsonResponse({ error: 'invalid admin key' }, 403), + }); + + await assert.rejects( + () => api.injectRemoteAccounts({ + url: 'remote.example.com/trailing/path', + adminKey: 'admin-secret', + body: { accounts: [] }, + }), + /invalid admin key/ + ); +}); diff --git a/tests/source-registry-module.test.js b/tests/source-registry-module.test.js index 74ea2445..55f3621d 100644 --- a/tests/source-registry-module.test.js +++ b/tests/source-registry-module.test.js @@ -197,4 +197,6 @@ test('shared source registry exposes canonical Kiro sources and drivers', () => assert.equal(registry.driverAcceptsCommand('flows/kiro/background/publisher-kiro-rs', 'kiro-upload-credential'), true); assert.equal(registry.driverAcceptsCommand('flows/grok/content/register-page', 'grok-submit-profile'), true); assert.equal(registry.driverAcceptsCommand('flows/grok/background/register-runner', 'grok-extract-sso-cookie'), true); + assert.equal(registry.driverAcceptsCommand('flows/grok/background/register-runner', 'grok-remote-sso-inject'), true); + assert.equal(registry.driverAcceptsCommand('flows/openai/background/steps/remote-account-inject', 'remote-account-inject'), true); }); diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js index ee5a4660..bd5f0af2 100644 --- a/tests/step-definitions-module.test.js +++ b/tests/step-definitions-module.test.js @@ -26,7 +26,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () const grokSteps = api.getSteps({ activeFlowId: 'grok' }); assert.equal(Array.isArray(steps), true); - assert.equal(steps.length, 11); + assert.equal(steps.length, 12); assert.equal(steps.every((step) => step.flowId === 'openai'), true); assert.deepStrictEqual( steps.map((step) => step.order), @@ -41,6 +41,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'oauth-login', 'fetch-login-code', 'post-login-phone-verification', @@ -61,6 +62,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'oauth-login', 'fetch-login-code', 'bind-email', @@ -78,6 +80,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'oauth-login', 'fetch-login-code', 'bind-email', @@ -101,6 +104,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'plus-checkout-create', 'plus-checkout-billing', 'paypal-approve', @@ -126,6 +130,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'plus-checkout-create', 'plus-checkout-billing', 'paypal-approve', @@ -153,14 +158,14 @@ test('step definitions module exposes ordered normal and Plus step metadata', () ] ); assert.equal(goPaySteps.some((step) => step.key === 'paypal-approve'), false); - assert.equal(api.getStepById(9, { plusModeEnabled: true, plusPaymentMethod: 'gopay' })?.key, 'oauth-login'); + assert.equal(api.getStepById(10, { plusModeEnabled: true, plusPaymentMethod: 'gopay' })?.key, 'oauth-login'); assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), ''); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); - assert.equal(api.getLastStepId({ plusModeEnabled: true }), 15); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); - assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone' }), 16); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]); - assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + assert.equal(api.getLastStepId({ plusModeEnabled: true }), 16); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone' }), 17); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 20); assert.equal(api.hasFlow('openai'), true); assert.equal(api.hasFlow('kiro'), true); assert.equal(api.hasFlow('grok'), true); @@ -220,6 +225,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'grok-submit-verification-code', 'grok-submit-profile', 'grok-extract-sso-cookie', + 'grok-remote-sso-inject', ] ); assert.equal(grokSteps.every((step) => step.flowId === 'grok'), true); @@ -228,10 +234,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', () assert.equal(grokSteps[2].mailRuleId, 'grok-submit-verification-code'); assert.deepStrictEqual( grokSteps.map((step) => step.title), - ['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie'] + ['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie', '远程注入 SSO'] ); - assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5]); - assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 5); + assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5, 6]); + assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 6); assert.deepStrictEqual( api.getNodes({ activeFlowId: 'grok' }).map((node) => node.next), [ @@ -239,11 +245,12 @@ test('step definitions module exposes ordered normal and Plus step metadata', () ['grok-submit-verification-code'], ['grok-submit-profile'], ['grok-extract-sso-cookie'], + ['grok-remote-sso-inject'], [], ] ); - assert.equal(plusSteps[6].title, '创建 Plus Checkout'); - assert.equal(plusSteps[8].title, 'PayPal 登录与授权'); + assert.equal(plusSteps[7].title, '创建 Plus Checkout'); + assert.equal(plusSteps[9].title, 'PayPal 登录与授权'); assert.deepStrictEqual( hostedSteps.map((step) => step.key), @@ -254,6 +261,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'plus-checkout-create', 'paypal-hosted-email', 'paypal-hosted-card', @@ -271,8 +279,8 @@ test('step definitions module exposes ordered normal and Plus step metadata', () assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-openai-checkout'), false); assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-verification'), false); assert.equal(hostedSteps.find((step) => step.key === 'paypal-hosted-card')?.title, '无卡直绑填写 PayPal 资料'); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); - assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 15); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 16); assert.deepStrictEqual( goPaySteps.map((step) => step.key), @@ -283,6 +291,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'plus-checkout-create', 'gopay-subscription-confirm', 'oauth-login', @@ -292,10 +301,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'platform-verify', ] ); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); - assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 13); - assert.equal(goPaySteps[6].title, '打开 GoPay 订阅页'); - assert.equal(goPaySteps[7].title, '等待 GoPay 订阅确认'); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 14); + assert.equal(goPaySteps[7].title, '打开 GoPay 订阅页'); + assert.equal(goPaySteps[8].title, '等待 GoPay 订阅确认'); assert.deepStrictEqual( gpcSteps.map((step) => step.key), @@ -306,6 +315,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'plus-checkout-create', 'plus-checkout-billing', 'oauth-login', @@ -315,10 +325,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'platform-verify', ] ); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); - assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13); - assert.equal(gpcSteps[6].title, '创建 GPC 订单'); - assert.equal(gpcSteps[7].title, '等待 GPC 任务完成'); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 14); + assert.equal(gpcSteps[7].title, '创建 GPC 订单'); + assert.equal(gpcSteps[8].title, '等待 GPC 任务完成'); }); test('Plus no-payment mode removes only payment chain nodes', () => { @@ -347,6 +357,7 @@ test('Plus no-payment mode removes only payment chain nodes', () => { 'fetch-signup-code', 'fill-profile', 'wait-registration-success', + 'remote-account-inject', 'oauth-login', 'fetch-login-code', 'post-login-phone-verification', @@ -357,7 +368,7 @@ test('Plus no-payment mode removes only payment chain nodes', () => { assert.equal(oauthStepKeys.includes(key), false, `no-payment OAuth should not keep ${key}`); assert.equal(oauthNodes.some((node) => node.nodeId === key), false, `no-payment OAuth nodes should not keep ${key}`); }); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'none' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'none' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'none' }), ''); assert.deepStrictEqual( oauthNodes.find((node) => node.nodeId === 'fill-profile')?.next, @@ -365,7 +376,7 @@ test('Plus no-payment mode removes only payment chain nodes', () => { ); assert.deepStrictEqual( oauthNodes.find((node) => node.nodeId === 'wait-registration-success')?.next, - ['oauth-login'] + ['remote-account-inject'] ); const sub2apiSteps = api.getSteps({ From ee6697ebc188c7f25e44bcb0e23286199c875b38 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sat, 23 May 2026 11:15:32 +0800 Subject: [PATCH 3/4] feat(sidepanel): expose remote injection settings --- sidepanel/sidepanel.html | 30 ++++++++++++++ sidepanel/sidepanel.js | 42 ++++++++++++++++++++ tests/sidepanel-flow-source-registry.test.js | 8 ++++ 3 files changed, 80 insertions(+) diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 6b0c79e5..114a8c2a 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -308,6 +308,36 @@ + + + +
账户密码
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 56a3b014..dacc87ed 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -197,6 +197,12 @@ const displayGrokRegisterStatus = document.getElementById('display-grok-register const rowGrokSsoStatus = document.getElementById('row-grok-sso-status'); const displayGrokSsoStatus = document.getElementById('display-grok-sso-status'); const rowGrokSsoSettings = document.getElementById('row-grok-sso-settings'); +const rowGrokRemoteAccountInject = document.getElementById('row-grok-remote-account-inject'); +const inputGrokRemoteAccountInjectUrl = document.getElementById('input-grok-remote-account-inject-url'); +const inputGrokRemoteAccountInjectAdminKey = document.getElementById('input-grok-remote-account-inject-admin-key'); +const rowRemoteAccountInject = document.getElementById('row-remote-account-inject'); +const inputRemoteAccountInjectUrl = document.getElementById('input-remote-account-inject-url'); +const inputRemoteAccountInjectAdminKey = document.getElementById('input-remote-account-inject-admin-key'); const displayGrokSsoCookie = document.getElementById('display-grok-sso-cookie'); const btnCopyGrokSso = document.getElementById('btn-copy-grok-sso'); const btnExportGrokSso = document.getElementById('btn-export-grok-sso'); @@ -4939,6 +4945,18 @@ function collectSettingsPayload() { sub2apiUrl: inputSub2ApiUrl.value.trim(), sub2apiEmail: inputSub2ApiEmail.value.trim(), sub2apiPassword: inputSub2ApiPassword.value, + remoteAccountInjectUrl: (typeof inputRemoteAccountInjectUrl !== 'undefined' && inputRemoteAccountInjectUrl) + ? inputRemoteAccountInjectUrl.value.trim() + : '', + remoteAccountInjectAdminKey: (typeof inputRemoteAccountInjectAdminKey !== 'undefined' && inputRemoteAccountInjectAdminKey) + ? inputRemoteAccountInjectAdminKey.value + : '', + grokRemoteAccountInjectUrl: (typeof inputGrokRemoteAccountInjectUrl !== 'undefined' && inputGrokRemoteAccountInjectUrl) + ? inputGrokRemoteAccountInjectUrl.value.trim() + : '', + grokRemoteAccountInjectAdminKey: (typeof inputGrokRemoteAccountInjectAdminKey !== 'undefined' && inputGrokRemoteAccountInjectAdminKey) + ? inputGrokRemoteAccountInjectAdminKey.value + : '', sub2apiGroupName: selectedSub2ApiGroupName, sub2apiGroupNames, sub2apiAccountPriority: sub2apiAccountPriorityNormalizer( @@ -11122,6 +11140,18 @@ function applySettingsState(state) { inputSub2ApiAccountPriority.value = String(normalizeSub2ApiAccountPriorityValue(state?.sub2apiAccountPriority)); } inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || ''; + if (typeof inputRemoteAccountInjectUrl !== 'undefined' && inputRemoteAccountInjectUrl) { + inputRemoteAccountInjectUrl.value = state?.remoteAccountInjectUrl || ''; + } + if (typeof inputRemoteAccountInjectAdminKey !== 'undefined' && inputRemoteAccountInjectAdminKey) { + inputRemoteAccountInjectAdminKey.value = state?.remoteAccountInjectAdminKey || ''; + } + if (typeof inputGrokRemoteAccountInjectUrl !== 'undefined' && inputGrokRemoteAccountInjectUrl) { + inputGrokRemoteAccountInjectUrl.value = state?.grokRemoteAccountInjectUrl || ''; + } + if (typeof inputGrokRemoteAccountInjectAdminKey !== 'undefined' && inputGrokRemoteAccountInjectAdminKey) { + inputGrokRemoteAccountInjectAdminKey.value = state?.grokRemoteAccountInjectAdminKey || ''; + } if (typeof inputKiroRsUrl !== 'undefined' && inputKiroRsUrl) { inputKiroRsUrl.value = String(state?.kiroRsUrl || '').trim(); } @@ -16139,6 +16169,18 @@ inputCodex2ApiUrl.addEventListener('blur', () => { saveSettings({ silent: true }).catch(() => { }); }); +[inputRemoteAccountInjectUrl, inputRemoteAccountInjectAdminKey, inputGrokRemoteAccountInjectUrl, inputGrokRemoteAccountInjectAdminKey] + .filter(Boolean) + .forEach((input) => { + input.addEventListener('input', () => { + markSettingsDirty(true); + scheduleSettingsAutoSave(); + }); + input.addEventListener('blur', () => { + saveSettings({ silent: true }).catch(() => { }); + }); + }); + inputCodex2ApiAdminKey.addEventListener('input', () => { markSettingsDirty(true); scheduleSettingsAutoSave(); diff --git a/tests/sidepanel-flow-source-registry.test.js b/tests/sidepanel-flow-source-registry.test.js index f2485453..020998d5 100644 --- a/tests/sidepanel-flow-source-registry.test.js +++ b/tests/sidepanel-flow-source-registry.test.js @@ -50,6 +50,14 @@ test('sidepanel html exposes flow selector and kiro source fields', () => { 'id="row-grok-register-status"', 'id="row-grok-sso-status"', 'id="row-grok-sso-settings"', + 'id="row-grok-remote-account-inject-url"', + 'id="input-grok-remote-account-inject-url"', + 'id="row-grok-remote-account-inject-admin-key"', + 'id="input-grok-remote-account-inject-admin-key"', + 'id="row-remote-account-inject-url"', + 'id="input-remote-account-inject-url"', + 'id="row-remote-account-inject-admin-key"', + 'id="input-remote-account-inject-admin-key"', 'id="btn-copy-grok-sso"', 'id="btn-export-grok-sso"', 'id="btn-clear-grok-sso"', From 42e4b2b17e23ded31c97d5a5371114e4f1225250 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sat, 23 May 2026 16:16:49 +0800 Subject: [PATCH 4/4] fix(openai): gate remote injection behind explicit toggle --- background.js | 17 +++- core/flow-kernel/settings-schema.js | 21 ++--- .../background/steps/remote-account-inject.js | 84 +------------------ flows/openai/index.js | 1 + flows/openai/workflow.js | 9 +- sidepanel/sidepanel.html | 10 +++ sidepanel/sidepanel.js | 56 +++++++++++-- ...ground-settings-schema-persistence.test.js | 10 +++ tests/background-step-registry.test.js | 2 + tests/flow-registry-settings-schema.test.js | 12 ++- tests/openai-remote-account-inject.test.js | 46 ++++++++++ tests/sidepanel-flow-source-registry.test.js | 14 ++++ tests/step-definitions-module.test.js | 75 ++++++++++------- ...76\350\267\257\350\257\264\346\230\216.md" | 10 ++- ...23\346\236\204\350\257\264\346\230\216.md" | 9 +- 15 files changed, 231 insertions(+), 145 deletions(-) diff --git a/background.js b/background.js index d02a821f..8e5eb718 100644 --- a/background.js +++ b/background.js @@ -931,6 +931,10 @@ function buildResolvedStepDefinitionState(state = {}) { plusModeEnabled: stepDefinitionOptions.plusModeEnabled === undefined ? plusModeEnabled : Boolean(stepDefinitionOptions.plusModeEnabled), + ...(Boolean( + stepDefinitionOptions.remoteAccountInjectEnabled + ?? state?.remoteAccountInjectEnabled + ) ? { remoteAccountInjectEnabled: true } : {}), plusPaymentMethod, plusAccountAccessStrategy: normalizePlusAccountAccessStrategy( stepDefinitionOptions.plusAccountAccessStrategy @@ -949,14 +953,18 @@ function getStepDefinitionsForState(state = {}) { if (rootScope.MultiPageStepDefinitions?.getSteps) { const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai'; const activeFlowId = String(resolvedState?.activeFlowId || '').trim().toLowerCase() || defaultFlowId; - const definitions = rootScope.MultiPageStepDefinitions.getSteps({ + const stepOptions = { activeFlowId, plusModeEnabled: Boolean(resolvedState?.plusModeEnabled), plusPaymentMethod: normalizePlusPaymentMethod(resolvedState?.plusPaymentMethod), plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(resolvedState?.plusAccountAccessStrategy), signupMethod: getSignupMethodForStepDefinitions(resolvedState), phoneSignupReloginAfterBindEmailEnabled: Boolean(resolvedState?.phoneSignupReloginAfterBindEmailEnabled), - }); + }; + if (Boolean(resolvedState?.remoteAccountInjectEnabled)) { + stepOptions.remoteAccountInjectEnabled = true; + } + const definitions = rootScope.MultiPageStepDefinitions.getSteps(stepOptions); if (Array.isArray(definitions)) { return definitions; } @@ -1295,6 +1303,7 @@ const PERSISTED_SETTING_DEFAULTS = { ipProxyRegion: '', codex2apiUrl: DEFAULT_CODEX2API_URL, codex2apiAdminKey: '', + remoteAccountInjectEnabled: false, remoteAccountInjectUrl: '', remoteAccountInjectAdminKey: '', grokRemoteAccountInjectUrl: '', @@ -1471,6 +1480,7 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([ 'sub2apiDefaultProxyName', 'codex2apiUrl', 'codex2apiAdminKey', + 'remoteAccountInjectEnabled', 'remoteAccountInjectUrl', 'remoteAccountInjectAdminKey', 'grokRemoteAccountInjectUrl', @@ -3259,6 +3269,8 @@ function normalizePersistentSettingValue(key, value) { return normalizeCodex2ApiUrl(value); case 'codex2apiAdminKey': return String(value || '').trim(); + case 'remoteAccountInjectEnabled': + return Boolean(value); case 'remoteAccountInjectUrl': return String(value || '').trim(); case 'remoteAccountInjectAdminKey': @@ -3840,6 +3852,7 @@ function buildSettingsStatePatchFromFlatUpdates(updates = {}) { assignIfUpdated('sub2apiDefaultProxyName', ['flows', 'openai', 'targets', 'sub2api', 'sub2apiDefaultProxyName']); assignIfUpdated('codex2apiUrl', ['flows', 'openai', 'targets', 'codex2api', 'codex2apiUrl']); assignIfUpdated('codex2apiAdminKey', ['flows', 'openai', 'targets', 'codex2api', 'codex2apiAdminKey']); + assignIfUpdated('remoteAccountInjectEnabled', ['flows', 'openai', 'remoteAccountInjectEnabled']); assignIfUpdated('remoteAccountInjectUrl', ['flows', 'openai', 'remoteAccountInjectUrl']); assignIfUpdated('remoteAccountInjectAdminKey', ['flows', 'openai', 'remoteAccountInjectAdminKey']); assignIfUpdated('grokRemoteAccountInjectUrl', ['flows', 'grok', 'grokRemoteAccountInjectUrl']); diff --git a/core/flow-kernel/settings-schema.js b/core/flow-kernel/settings-schema.js index 651811a5..2c12d3e9 100644 --- a/core/flow-kernel/settings-schema.js +++ b/core/flow-kernel/settings-schema.js @@ -206,6 +206,7 @@ }; if (flowId === 'openai') { return mergePlainObjects(base, { + remoteAccountInjectEnabled: false, remoteAccountInjectUrl: '', remoteAccountInjectAdminKey: '', signup: { @@ -301,20 +302,6 @@ apiKey: String(targetState.apiKey ?? ''), }; } - if (flowId === 'openai') { - return { - ...targetState, - remoteAccountInjectUrl: String(targetState.remoteAccountInjectUrl ?? '').trim(), - remoteAccountInjectAdminKey: String(targetState.remoteAccountInjectAdminKey ?? '').trim(), - }; - } - if (flowId === 'grok') { - return { - ...targetState, - grokRemoteAccountInjectUrl: String(targetState.grokRemoteAccountInjectUrl ?? '').trim(), - grokRemoteAccountInjectAdminKey: String(targetState.grokRemoteAccountInjectAdminKey ?? '').trim(), - }; - } return targetState; } @@ -418,6 +405,11 @@ }; return { ...currentFlow, + remoteAccountInjectEnabled: Boolean( + input?.remoteAccountInjectEnabled + ?? currentFlow.remoteAccountInjectEnabled + ?? defaults.flows.openai.remoteAccountInjectEnabled + ), remoteAccountInjectUrl: String( input?.remoteAccountInjectUrl ?? currentFlow.remoteAccountInjectUrl @@ -663,6 +655,7 @@ next.sub2apiDefaultProxyName = openaiState.targets.sub2api?.sub2apiDefaultProxyName || ''; next.codex2apiUrl = openaiState.targets.codex2api?.codex2apiUrl || ''; next.codex2apiAdminKey = openaiState.targets.codex2api?.codex2apiAdminKey || ''; + next.remoteAccountInjectEnabled = Boolean(openaiState.remoteAccountInjectEnabled); next.remoteAccountInjectUrl = openaiState.remoteAccountInjectUrl || ''; next.remoteAccountInjectAdminKey = openaiState.remoteAccountInjectAdminKey || ''; next.grokRemoteAccountInjectUrl = grokState.grokRemoteAccountInjectUrl || ''; diff --git a/flows/openai/background/steps/remote-account-inject.js b/flows/openai/background/steps/remote-account-inject.js index 8ffc221d..58c9796a 100644 --- a/flows/openai/background/steps/remote-account-inject.js +++ b/flows/openai/background/steps/remote-account-inject.js @@ -67,68 +67,6 @@ } } - 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 (error) { - 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) { @@ -141,18 +79,6 @@ : 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) @@ -173,15 +99,7 @@ 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 页面。'); + throw new Error('未找到当前流程记录的 ChatGPT 会话标签页,已停止远程注入 accessToken。'); } async function getResolvedSessionTab(tabId, visibleStep) { diff --git a/flows/openai/index.js b/flows/openai/index.js index 111af3f1..09474d1a 100644 --- a/flows/openai/index.js +++ b/flows/openai/index.js @@ -404,6 +404,7 @@ "id": "openai-remote-account-inject", "label": "远程账号注入", "rowIds": [ + "row-remote-account-inject-enabled", "row-remote-account-inject-url", "row-remote-account-inject-admin-key" ] diff --git a/flows/openai/workflow.js b/flows/openai/workflow.js index 67b2a330..b450922e 100644 --- a/flows/openai/workflow.js +++ b/flows/openai/workflow.js @@ -3140,6 +3140,10 @@ return Boolean(options?.phoneSignupReloginAfterBindEmailEnabled); } + function isRemoteAccountInjectEnabled(options = {}) { + return Boolean(options?.remoteAccountInjectEnabled); + } + function normalizePlusAccountAccessStrategy(value = '') { const normalized = String(value || '').trim().toLowerCase(); if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) { @@ -3224,7 +3228,9 @@ if (isPlusMode) { steps = insertPlusRegistrationWaitStep(steps); } - steps = insertRemoteAccountInjectStep(steps); + if (isRemoteAccountInjectEnabled(options)) { + steps = insertRemoteAccountInjectStep(steps); + } if ( isPlusMode && normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod) === PLUS_PAYMENT_METHOD_NONE @@ -3279,6 +3285,7 @@ normalizePlusPaymentMethod, normalizePlusAccountAccessStrategy, normalizeSignupMethod, + isRemoteAccountInjectEnabled, resolveStepTitle, }; }); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 114a8c2a..0d7ef75f 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -323,6 +323,16 @@ data-hide-label="隐藏远程注入密钥" aria-label="显示远程注入密钥" title="显示远程注入密钥">
+