diff --git a/background.js b/background.js index fa8c68eb..8e5eb718 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', @@ -929,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 @@ -947,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; } @@ -1293,6 +1303,11 @@ const PERSISTED_SETTING_DEFAULTS = { ipProxyRegion: '', codex2apiUrl: DEFAULT_CODEX2API_URL, codex2apiAdminKey: '', + remoteAccountInjectEnabled: false, + remoteAccountInjectUrl: '', + remoteAccountInjectAdminKey: '', + grokRemoteAccountInjectUrl: '', + grokRemoteAccountInjectAdminKey: '', customPassword: '', plusModeEnabled: false, plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD, @@ -1465,6 +1480,11 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([ 'sub2apiDefaultProxyName', 'codex2apiUrl', 'codex2apiAdminKey', + 'remoteAccountInjectEnabled', + 'remoteAccountInjectUrl', + 'remoteAccountInjectAdminKey', + 'grokRemoteAccountInjectUrl', + 'grokRemoteAccountInjectAdminKey', 'customPassword', 'signupMethod', 'phoneVerificationEnabled', @@ -3249,6 +3269,16 @@ 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': + 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 +3852,11 @@ 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']); + assignIfUpdated('grokRemoteAccountInjectAdminKey', ['flows', 'grok', 'grokRemoteAccountInjectAdminKey']); assignIfUpdated('customPassword', ['services', 'account', 'customPassword']); assignIfUpdated('signupMethod', ['flows', 'openai', 'signup', 'signupMethod']); assignIfUpdated('phoneVerificationEnabled', ['flows', 'openai', 'signup', 'phoneVerificationEnabled']); @@ -9379,7 +9414,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 +10948,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 +10972,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 +12071,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 +13952,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 +13995,7 @@ const grokRegisterRunner = self.MultiPageBackgroundGrokRegisterRunner?.createGro chrome, ensureContentScriptReadyOnTab, completeNodeFromBackground, + fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null, generatePassword, generateRandomName, getTabId, @@ -14089,6 +14142,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 +14169,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/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/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/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/core/flow-kernel/settings-schema.js b/core/flow-kernel/settings-schema.js index 8571706d..2c12d3e9 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,9 @@ }; if (flowId === 'openai') { return mergePlainObjects(base, { + remoteAccountInjectEnabled: false, + remoteAccountInjectUrl: '', + remoteAccountInjectAdminKey: '', signup: { signupMethod: 'email', phoneVerificationEnabled: false, @@ -398,6 +405,21 @@ }; return { ...currentFlow, + remoteAccountInjectEnabled: Boolean( + input?.remoteAccountInjectEnabled + ?? currentFlow.remoteAccountInjectEnabled + ?? defaults.flows.openai.remoteAccountInjectEnabled + ), + 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 +559,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 +640,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 +655,11 @@ 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 || ''; + 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/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/flows/openai/background/steps/remote-account-inject.js b/flows/openai/background/steps/remote-account-inject.js new file mode 100644 index 00000000..58c9796a --- /dev/null +++ b/flows/openai/background/steps/remote-account-inject.js @@ -0,0 +1,210 @@ +(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; + } + } + + 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 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; + } + + throw new Error('未找到当前流程记录的 ChatGPT 会话标签页,已停止远程注入 accessToken。'); + } + + 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..09474d1a 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,15 @@ "rowIds": [ "row-step6-cookie-settings" ] + }, + "openai-remote-account-inject": { + "id": "openai-remote-account-inject", + "label": "远程账号注入", + "rowIds": [ + "row-remote-account-inject-enabled", + "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..b450922e 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, @@ -3116,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) { @@ -3200,6 +3228,9 @@ if (isPlusMode) { steps = insertPlusRegistrationWaitStep(steps); } + if (isRemoteAccountInjectEnabled(options)) { + steps = insertRemoteAccountInjectStep(steps); + } if ( isPlusMode && normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod) === PLUS_PAYMENT_METHOD_NONE @@ -3212,9 +3243,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); }); @@ -3252,6 +3285,7 @@ normalizePlusPaymentMethod, normalizePlusAccountAccessStrategy, normalizeSignupMethod, + isRemoteAccountInjectEnabled, resolveStepTitle, }; }); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 6b0c79e5..0d7ef75f 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -308,6 +308,46 @@ + + + + +
账户密码
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 56a3b014..9a4ffb32 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -197,6 +197,11 @@ 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 inputGrokRemoteAccountInjectUrl = document.getElementById('input-grok-remote-account-inject-url'); +const inputGrokRemoteAccountInjectAdminKey = document.getElementById('input-grok-remote-account-inject-admin-key'); +const inputRemoteAccountInjectEnabled = document.getElementById('input-remote-account-inject-enabled'); +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'); @@ -890,10 +895,13 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) { const accountContributionEnabled = typeof options === 'string' ? Boolean(typeof latestState !== 'undefined' ? latestState?.accountContributionEnabled : false) : Boolean(options.accountContributionEnabled ?? (typeof latestState !== 'undefined' ? latestState?.accountContributionEnabled : false)); + const remoteAccountInjectEnabled = typeof options === 'string' + ? Boolean(typeof latestState !== 'undefined' ? latestState?.remoteAccountInjectEnabled : false) + : Boolean(options.remoteAccountInjectEnabled ?? (typeof latestState !== 'undefined' ? latestState?.remoteAccountInjectEnabled : false)); const activeFlowId = typeof options === 'string' ? ((typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId) : (options.activeFlowId || (typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId); - return (window.MultiPageStepDefinitions?.getSteps?.({ + const stepOptions = { activeFlowId: String(activeFlowId || '').trim().toLowerCase() || defaultFlowId, plusModeEnabled, plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod), @@ -901,7 +909,11 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) { signupMethod: normalizeSignupMethod(rawSignupMethod), phoneSignupReloginAfterBindEmailEnabled, accountContributionEnabled, - }) || []) + }; + if (remoteAccountInjectEnabled) { + stepOptions.remoteAccountInjectEnabled = true; + } + return (window.MultiPageStepDefinitions?.getSteps?.(stepOptions) || []) .sort((left, right) => { const leftOrder = Number.isFinite(left.order) ? left.order : left.id; const rightOrder = Number.isFinite(right.order) ? right.order : right.id; @@ -929,10 +941,13 @@ function getWorkflowNodesForMode(plusModeEnabled = false, options = {}) { const accountContributionEnabled = typeof options === 'string' ? Boolean(typeof latestState !== 'undefined' ? latestState?.accountContributionEnabled : false) : Boolean(options.accountContributionEnabled ?? (typeof latestState !== 'undefined' ? latestState?.accountContributionEnabled : false)); + const remoteAccountInjectEnabled = typeof options === 'string' + ? Boolean(typeof latestState !== 'undefined' ? latestState?.remoteAccountInjectEnabled : false) + : Boolean(options.remoteAccountInjectEnabled ?? (typeof latestState !== 'undefined' ? latestState?.remoteAccountInjectEnabled : false)); const activeFlowId = typeof options === 'string' ? ((typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId) : (options.activeFlowId || (typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId); - const nodes = window.MultiPageStepDefinitions?.getNodes?.({ + const nodeOptions = { activeFlowId: String(activeFlowId || '').trim().toLowerCase() || defaultFlowId, plusModeEnabled, plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod), @@ -940,7 +955,11 @@ function getWorkflowNodesForMode(plusModeEnabled = false, options = {}) { signupMethod: normalizeSignupMethod(rawSignupMethod), phoneSignupReloginAfterBindEmailEnabled, accountContributionEnabled, - }); + }; + if (remoteAccountInjectEnabled) { + nodeOptions.remoteAccountInjectEnabled = true; + } + const nodes = window.MultiPageStepDefinitions?.getNodes?.(nodeOptions); if (Array.isArray(nodes) && nodes.length) { return nodes.slice().sort((left, right) => { const leftOrder = Number.isFinite(Number(left.displayOrder)) ? Number(left.displayOrder) : 0; @@ -1011,6 +1030,10 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) { options.accountContributionEnabled ?? (typeof latestState !== 'undefined' ? latestState?.accountContributionEnabled : false) ); + const remoteAccountInjectEnabled = Boolean( + options.remoteAccountInjectEnabled + ?? (typeof latestState !== 'undefined' ? latestState?.remoteAccountInjectEnabled : false) + ); currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod); currentPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(rawPlusAccountAccessStrategy); currentSignupMethod = normalizeSignupMethod(rawSignupMethod); @@ -1030,6 +1053,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) { signupMethod: currentSignupMethod, phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled, accountContributionEnabled, + remoteAccountInjectEnabled, }); const nextWorkflowNodes = typeof getWorkflowNodesForMode === 'function' ? getWorkflowNodesForMode(currentPlusModeEnabled, { @@ -1039,6 +1063,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) { signupMethod: currentSignupMethod, phoneSignupReloginAfterBindEmailEnabled: currentPhoneSignupReloginAfterBindEmailEnabled, accountContributionEnabled, + remoteAccountInjectEnabled, }) : stepDefinitions.map((step) => ({ nodeId: String(step.key || step.id || '').trim(), @@ -4939,6 +4964,21 @@ function collectSettingsPayload() { sub2apiUrl: inputSub2ApiUrl.value.trim(), sub2apiEmail: inputSub2ApiEmail.value.trim(), sub2apiPassword: inputSub2ApiPassword.value, + remoteAccountInjectEnabled: (typeof inputRemoteAccountInjectEnabled !== 'undefined' && inputRemoteAccountInjectEnabled) + ? Boolean(inputRemoteAccountInjectEnabled.checked) + : false, + 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( @@ -10854,6 +10894,10 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr options.accountContributionEnabled ?? (typeof latestState !== 'undefined' ? latestState?.accountContributionEnabled : false) ); + const nextRemoteAccountInjectEnabled = Boolean( + options.remoteAccountInjectEnabled + ?? (typeof latestState !== 'undefined' ? latestState?.remoteAccountInjectEnabled : false) + ); const nextPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod); const nextActiveFlowId = String( options.activeFlowId @@ -10872,6 +10916,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr plusAccountAccessStrategy: nextPlusAccountAccessStrategy, signupMethod: nextSignupMethod, phoneSignupReloginAfterBindEmailEnabled: nextPhoneSignupReloginAfterBindEmailEnabled, + remoteAccountInjectEnabled: Boolean(options.remoteAccountInjectEnabled ?? latestState?.remoteAccountInjectEnabled), }); const paymentTitleChanged = Boolean(nextPlusModeEnabled && currentPaymentStep && nextPaymentTitle && currentPaymentStep.title !== nextPaymentTitle); const shouldRender = Boolean(options.render) @@ -10880,6 +10925,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr || nextPlusAccountAccessStrategy !== currentPlusAccountAccessStrategy || nextSignupMethod !== currentSignupMethod || nextPhoneSignupReloginAfterBindEmailEnabled !== currentPhoneSignupReloginAfterBindEmailEnabled + || nextRemoteAccountInjectEnabled !== Boolean(typeof latestState !== 'undefined' ? latestState?.remoteAccountInjectEnabled : false) || nextAccountContributionEnabled !== Boolean(typeof latestState !== 'undefined' ? latestState?.accountContributionEnabled : false) || nextActiveFlowId !== currentFlowId || paymentTitleChanged; @@ -10894,6 +10940,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr signupMethod: nextSignupMethod, phoneSignupReloginAfterBindEmailEnabled: nextPhoneSignupReloginAfterBindEmailEnabled, accountContributionEnabled: nextAccountContributionEnabled, + remoteAccountInjectEnabled: nextRemoteAccountInjectEnabled, }); renderStepsList(); } @@ -10921,6 +10968,7 @@ function syncStepDefinitionsFromUiState(stateOverrides = {}) { signupMethod: stepDefinitionState.signupMethod, phoneSignupReloginAfterBindEmailEnabled: Boolean(nextState?.phoneSignupReloginAfterBindEmailEnabled), accountContributionEnabled: Boolean(nextState?.accountContributionEnabled), + remoteAccountInjectEnabled: Boolean(nextState?.remoteAccountInjectEnabled), }); return stepDefinitionState; } @@ -10945,6 +10993,7 @@ function applySettingsState(state) { signupMethod: stepDefinitionState.signupMethod, phoneSignupReloginAfterBindEmailEnabled: Boolean(state?.phoneSignupReloginAfterBindEmailEnabled), accountContributionEnabled: Boolean(state?.accountContributionEnabled), + remoteAccountInjectEnabled: Boolean(state?.remoteAccountInjectEnabled), }); } const fallbackIpProxyService = '711proxy'; @@ -11122,6 +11171,21 @@ function applySettingsState(state) { inputSub2ApiAccountPriority.value = String(normalizeSub2ApiAccountPriorityValue(state?.sub2apiAccountPriority)); } inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || ''; + if (typeof inputRemoteAccountInjectEnabled !== 'undefined' && inputRemoteAccountInjectEnabled) { + inputRemoteAccountInjectEnabled.checked = Boolean(state?.remoteAccountInjectEnabled); + } + 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(); } @@ -15400,6 +15464,16 @@ inputPlusModeEnabled?.addEventListener('change', () => { saveSettings({ silent: true }).catch(() => { }); }); +inputRemoteAccountInjectEnabled?.addEventListener('change', () => { + syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled?.checked), getSelectedPlusPaymentMethod(), { + render: true, + remoteAccountInjectEnabled: Boolean(inputRemoteAccountInjectEnabled.checked), + signupMethod: getSelectedSignupMethod(), + }); + markSettingsDirty(true); + saveSettings({ silent: true }).catch(() => { }); +}); + btnGpcCardKeyPurchase?.addEventListener('click', () => { openExternalUrl('https://pay.ldxp.cn/shop/gpc'); }); @@ -16139,6 +16213,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/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/background-settings-schema-persistence.test.js b/tests/background-settings-schema-persistence.test.js index a5e53476..b7888786 100644 --- a/tests/background-settings-schema-persistence.test.js +++ b/tests/background-settings-schema-persistence.test.js @@ -73,6 +73,7 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([ 'sub2apiDefaultProxyName', 'codex2apiUrl', 'codex2apiAdminKey', + 'remoteAccountInjectEnabled', 'customPassword', 'signupMethod', 'phoneVerificationEnabled', @@ -93,6 +94,7 @@ const PERSISTED_SETTING_DEFAULTS = { activeFlowId: DEFAULT_ACTIVE_FLOW_ID, targetId: 'cpa', signupMethod: 'email', + remoteAccountInjectEnabled: false, plusModeEnabled: false, plusPaymentMethod: 'paypal', plusAccountAccessStrategy: 'oauth', @@ -229,6 +231,7 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown flows: { openai: { selectedTargetId: 'cpa', + remoteAccountInjectEnabled: true, targets: { cpa: { vpsUrl: '', @@ -286,6 +289,8 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false); assert.equal(payload.settingsSchemaVersion, 5); assert.equal(payload.settingsState.flows.openai.plus.plusAccountAccessStrategy, 'oauth'); + assert.equal(payload.settingsState.flows.openai.remoteAccountInjectEnabled, true); + assert.equal(payload.remoteAccountInjectEnabled, true); }); test('getPersistedSettings reads schema keys alongside legacy flat settings keys', async () => { @@ -398,6 +403,8 @@ const chrome = { assert.equal(state.kiroRsUrl, 'https://kiro.example.com/admin'); assert.equal(state.kiroRsKey, 'stored-key'); assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session'); + assert.equal(state.remoteAccountInjectEnabled, false); + assert.equal(state.settingsState.flows.openai.remoteAccountInjectEnabled, false); assert.equal(Object.prototype.hasOwnProperty.call(state, 'kiroRegion'), false); assert.deepEqual(state.stepExecutionRangeByFlow.kiro, { enabled: true, @@ -445,6 +452,7 @@ function getRemovedKeys() { flows: { openai: { selectedTargetId: 'cpa', + remoteAccountInjectEnabled: true, targets: { cpa: { vpsUrl: '', @@ -502,6 +510,7 @@ function getRemovedKeys() { assert.equal(persisted.kiroRsUrl, 'https://kiro.example.com/admin'); assert.equal(persisted.kiroRsKey, 'nested-only-key'); assert.equal(persisted.plusAccountAccessStrategy, 'sub2api_codex_session'); + assert.equal(persisted.remoteAccountInjectEnabled, true); assert.equal(Object.prototype.hasOwnProperty.call(persisted, 'kiroRegion'), false); assert.equal(persisted.settingsSchemaVersion, 5); assert.equal(Object.prototype.hasOwnProperty.call(write, 'activeFlowId'), false); @@ -510,6 +519,7 @@ function getRemovedKeys() { assert.equal(Object.prototype.hasOwnProperty.call(write, 'kiroRegion'), false); assert.equal(write.settingsSchemaVersion, 5); assert.equal(write.settingsState.activeFlowId, 'kiro'); + assert.equal(write.settingsState.flows.openai.remoteAccountInjectEnabled, true); assert.equal(write.settingsState.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session'); assert.equal(write.settingsState.flows.kiro.selectedTargetId, 'kiro-rs'); assert.ok(api.getRemovedKeys().includes('kiroRsUrl')); diff --git a/tests/background-step-registry.test.js b/tests/background-step-registry.test.js index a3cf0c98..5914b5ba 100644 --- a/tests/background-step-registry.test.js +++ b/tests/background-step-registry.test.js @@ -11,6 +11,8 @@ test('background imports node registry and wires the rebuilt Kiro executors', () assert.match(source, /buildNodeRegistry\(definitions/); assert.match(source, /const stepRegistryCache = new Map\(\);/); assert.match(source, /const definitions = getNodeDefinitionsForState\(state\);/); + assert.match(source, /if \(Boolean\(resolvedState\?\.remoteAccountInjectEnabled\)\) \{\s*stepOptions\.remoteAccountInjectEnabled = true;\s*\}/); + assert.match(source, /\.\.\.\(Boolean\(\s*stepDefinitionOptions\.remoteAccountInjectEnabled[\s\S]*?state\?\.remoteAccountInjectEnabled[\s\S]*?\) \? \{ remoteAccountInjectEnabled: true \} : \{\}\),/); assert.match(source, /stepRegistryCache\.set\(cacheKey, buildStepRegistry\(definitions\)\)/); assert.match(source, /flows\/kiro\/background\/register-runner\.js/); @@ -39,7 +41,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 +54,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..46853d7a 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-enabled', '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,11 @@ test('settings schema normalizes view input into canonical nested namespaces', ( plusAccountAccessStrategy: 'sub2api_codex_session', kiroRsUrl: 'https://kiro.example.com/admin', kiroRsKey: 'secret-key', + remoteAccountInjectEnabled: true, + 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 +104,22 @@ 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.remoteAccountInjectEnabled, true); + 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'); + assert.equal(normalized.flows.openai.targets.cpa.remoteAccountInjectEnabled, undefined); + assert.equal(normalized.flows.openai.targets.cpa.remoteAccountInjectUrl, undefined); + assert.equal(normalized.flows.openai.targets.cpa.remoteAccountInjectAdminKey, undefined); + assert.equal(normalized.flows.grok.targets.webchat2api.grokRemoteAccountInjectUrl, undefined); + assert.equal(normalized.flows.grok.targets.webchat2api.grokRemoteAccountInjectAdminKey, undefined); + const view = schema.buildSettingsView(normalized); + assert.equal(view.remoteAccountInjectEnabled, true); + 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, @@ -148,12 +173,14 @@ test('settings schema can project canonical state into a read view without legac assert.equal(view.kiroRsUrl, 'https://kiro.example.com/admin'); assert.equal(view.kiroRsKey, 'key-123'); assert.equal(view.plusAccountAccessStrategy, 'sub2api_codex_session'); + assert.equal(view.remoteAccountInjectEnabled, false); + assert.equal(view.settingsState.flows.openai.remoteAccountInjectEnabled, false); assert.equal(view.settingsSchemaVersion, 5); assert.equal(view.settingsState.activeFlowId, 'kiro'); assert.deepEqual(view.stepExecutionRangeByFlow.grok, { enabled: false, fromStep: 1, - toStep: 5, + toStep: 6, }); }); 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( diff --git a/tests/openai-remote-account-inject.test.js b/tests/openai-remote-account-inject.test.js new file mode 100644 index 00000000..bed1ebe7 --- /dev/null +++ b/tests/openai-remote-account-inject.test.js @@ -0,0 +1,174 @@ +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 does not scan arbitrary ChatGPT tabs', async () => { + const moduleApi = loadRemoteAccountInjectStepModule(); + const queryCalls = []; + let sentMessage = false; + + const executor = moduleApi.createRemoteAccountInjectExecutor({ + addLog: async () => {}, + chrome: { + tabs: { + get: async () => null, + query: async (query) => { + queryCalls.push(query); + return [{ id: 12, url: 'https://chatgpt.com/' }]; + }, + }, + }, + completeNodeFromBackground: async () => {}, + createRemoteAccountInjectApi: () => ({ + injectRemoteAccounts: async () => ({ skipped: false }), + }), + ensureContentScriptReadyOnTabUntilStopped: async () => {}, + getTabId: async () => null, + isTabAlive: async () => false, + registerTab: async () => {}, + sendTabMessageUntilStopped: async () => { + sentMessage = true; + return { accessToken: 'openai-access-token' }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + waitForTabCompleteUntilStopped: async () => {}, + }); + + await assert.rejects( + () => executor.executeRemoteAccountInject({ + nodeId: 'remote-account-inject', + visibleStep: 7, + remoteAccountInjectUrl: 'https://remote.example.com/panel', + remoteAccountInjectAdminKey: 'admin-secret', + }), + /当前流程记录的 ChatGPT 会话标签页/ + ); + assert.deepEqual(queryCalls, []); + assert.equal(sentMessage, false); +}); + +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/sidepanel-flow-source-registry.test.js b/tests/sidepanel-flow-source-registry.test.js index f2485453..c61219c7 100644 --- a/tests/sidepanel-flow-source-registry.test.js +++ b/tests/sidepanel-flow-source-registry.test.js @@ -50,6 +50,16 @@ 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-enabled"', + 'id="input-remote-account-inject-enabled"', + '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"', @@ -68,6 +78,18 @@ test('sidepanel html exposes flow selector and kiro source fields', () => { ); }); +test('sidepanel wires OpenAI remote account inject toggle through DOM, save payload, and step refresh', () => { + assert.match(sidepanelSource, /const inputRemoteAccountInjectEnabled = document\.getElementById\('input-remote-account-inject-enabled'\)/); + assert.match(sidepanelSource, /remoteAccountInjectEnabled:\s*\(typeof inputRemoteAccountInjectEnabled !== 'undefined' && inputRemoteAccountInjectEnabled\)\s*\?\s*Boolean\(inputRemoteAccountInjectEnabled\.checked\)\s*:\s*false/); + assert.match(sidepanelSource, /inputRemoteAccountInjectEnabled\.checked = Boolean\(state\?\.remoteAccountInjectEnabled\)/); + assert.match(sidepanelSource, /inputRemoteAccountInjectEnabled\?\.addEventListener\('change'/); + assert.match(sidepanelSource, /remoteAccountInjectEnabled:\s*Boolean\(inputRemoteAccountInjectEnabled\.checked\)/); + assert.match(sidepanelSource, /const stepOptions = \{/); + assert.match(sidepanelSource, /if \(remoteAccountInjectEnabled\) \{\s*stepOptions\.remoteAccountInjectEnabled = true;\s*\}/); + assert.match(sidepanelSource, /const nodeOptions = \{/); + assert.match(sidepanelSource, /if \(remoteAccountInjectEnabled\) \{\s*nodeOptions\.remoteAccountInjectEnabled = true;\s*\}/); +}); + test('sidepanel Grok SSO clear action goes through background message instead of direct storage writes', () => { const clearButtonIndex = sidepanelSource.indexOf("btnClearGrokSso?.addEventListener('click'"); assert.notEqual(clearButtonIndex, -1); 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..affa8dd4 100644 --- a/tests/step-definitions-module.test.js +++ b/tests/step-definitions-module.test.js @@ -220,6 +220,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 +229,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,6 +240,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'], [], ] ); @@ -321,6 +323,28 @@ test('step definitions module exposes ordered normal and Plus step metadata', () assert.equal(gpcSteps[7].title, '等待 GPC 任务完成'); }); + +test('OpenAI remote account inject node is inserted only when explicitly enabled', () => { + const globalScope = {}; + const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope); + const defaultKeys = api.getSteps().map((step) => step.key); + const phoneKeys = api.getSteps({ signupMethod: 'phone' }).map((step) => step.key); + const oauthKeys = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'none' }).map((step) => step.key); + const enabledNodes = api.getNodes({ remoteAccountInjectEnabled: true }); + + assert.equal(defaultKeys.includes('remote-account-inject'), false); + assert.equal(phoneKeys.includes('remote-account-inject'), false); + assert.equal(oauthKeys.includes('remote-account-inject'), false); + assert.deepStrictEqual( + enabledNodes.map((node) => node.nodeId).slice(5, 8), + ['wait-registration-success', 'remote-account-inject', 'oauth-login'] + ); + assert.deepStrictEqual( + enabledNodes.find((node) => node.nodeId === 'wait-registration-success')?.next, + ['remote-account-inject'] + ); +}); + test('Plus no-payment mode removes only payment chain nodes', () => { const globalScope = {}; const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope); diff --git "a/\351\241\271\347\233\256\345\256\214\346\225\264\351\223\276\350\267\257\350\257\264\346\230\216.md" "b/\351\241\271\347\233\256\345\256\214\346\225\264\351\223\276\350\267\257\350\257\264\346\230\216.md" index 90bb7d06..d7e414b3 100644 --- "a/\351\241\271\347\233\256\345\256\214\346\225\264\351\223\276\350\267\257\350\257\264\346\230\216.md" +++ "b/\351\241\271\347\233\256\345\256\214\346\225\264\351\223\276\350\267\257\350\257\264\346\230\216.md" @@ -14,7 +14,7 @@ - `openai`:OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路 - `kiro`:AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路 -- `grok`:Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 本地导出链路 +- `grok`:Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 本地导出链路,可选把 Grok SSO Cookie 注入远端账号池 它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路: @@ -45,7 +45,7 @@ - 接收后台广播并更新 UI - 动态渲染步骤列表 - 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target,`kiro` / `grok` flow 使用各自的 `flows..targetId` -- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态;Grok flow 只显示 `webchat2api` SSO 导出状态、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置 +- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态;Grok flow 只显示 `webchat2api` SSO 导出状态、可选远程 SSO 注入 URL / Admin Key、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置 - 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源 - 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态 - 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现 @@ -193,6 +193,8 @@ - 内容脚本通过 `log(message, level, { step, stepKey })` 上报结构化日志,`reportComplete` / `reportError` 的步骤号只走消息字段和日志元数据。 - [sidepanel/sidepanel.js](./sidepanel/sidepanel.js) 只读取 `entry.step` 渲染步骤标签;日志正文只作为正文展示,不参与步骤号判断。 - Plus 模式后半段不再固定只有一条 OAuth 复用尾链;当 `plusAccountAccessStrategy` 选择会话导入时,尾节点会直接变成 `sub2api-session-import` 或 `cpa-session-import`,并按当前可见步骤写日志、完成信号和错误信号。只有走 `oauth` 时,才会进入 `oauth-login -> fetch-login-code -> confirm-oauth -> platform-verify` 这组复用节点。 +- OpenAI 远程账号注入是显式边界能力:默认普通、手机号、OAuth/Plus 步骤都不会插入 `remote-account-inject` 节点;只有侧边栏开启远程注入开关并持久化 `remoteAccountInjectEnabled: true` 后,后台把该 flow 级设置传入步骤定义,才会在 `wait-registration-success` 与后续 OAuth/Plus 链路之间插入该节点。该节点继续读取 flow 级 `remoteAccountInjectUrl / remoteAccountInjectAdminKey` 配置,但执行时只使用当前流程已登记或状态中保存的 ChatGPT 标签页,不扫描任意已打开的 ChatGPT/OpenAI 标签页。 +- Grok 远程 SSO 注入是 Grok flow 的可选边界能力:`grok-remote-sso-inject` 节点仅消费当前 Grok 运行态中的 SSO Cookie,并读取 flow 级 `grokRemoteAccountInjectUrl / grokRemoteAccountInjectAdminKey`;未配置 URL 或 Admin Key 时按跳过处理,不影响本地 SSO Cookie 导出。 - 平台验证链路只在 OAuth 尾链下出现;CPA / SUB2API / Codex2API 都按当前 `platform-verify` 可见步骤上报。若尾链改为会话导入,则不再经过 `platform-verify`,而是由各自的 session-import 节点直接完成平台接入。 ## 4. 状态与存储链路 @@ -256,10 +258,12 @@ 保存持久配置与账号运行历史: - 当前 flow 与目标配置:`activeFlowId`、OpenAI 的 `panelMode`(归一到 integration target)、Kiro 的 `flows.kiro.targetId` -- flow-aware 配置树:`flows.openai.*`、`flows.kiro.*` +- flow-aware 配置树:`flows.openai.*`、`flows.kiro.*`、`flows.grok.*` - CPA / SUB2API 配置 - SUB2API 持久配置中的 `sub2apiGroupName / sub2apiGroupNames / sub2apiAccountPriority / sub2apiDefaultProxyName` - Codex2API 配置 +- OpenAI 远程账号注入配置 `flows.openai.remoteAccountInjectEnabled / remoteAccountInjectUrl / remoteAccountInjectAdminKey`,只作为 flow 级设置保存;默认步骤不会启用远程注入节点,必须由侧边栏独立开关显式开启。 +- Grok 远程 SSO 注入配置 `flows.grok.grokRemoteAccountInjectUrl / grokRemoteAccountInjectAdminKey`,只作为 flow 级设置保存;缺失时 Grok 注入节点跳过 - IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值 - Plus 模式开关 `plusModeEnabled` - Plus 账号接入策略 `plusAccountAccessStrategy` diff --git "a/\351\241\271\347\233\256\346\226\207\344\273\266\347\273\223\346\236\204\350\257\264\346\230\216.md" "b/\351\241\271\347\233\256\346\226\207\344\273\266\347\273\223\346\236\204\350\257\264\346\230\216.md" index af6564e8..b7347eb7 100644 --- "a/\351\241\271\347\233\256\346\226\207\344\273\266\347\273\223\346\236\204\350\257\264\346\230\216.md" +++ "b/\351\241\271\347\233\256\346\226\207\344\273\266\347\273\223\346\236\204\350\257\264\346\230\216.md" @@ -60,7 +60,7 @@ - `flows/kiro/background/desktop-authorize-runner.js`:Kiro 桌面授权执行器,负责步骤 7~8 的标签页打开、授权页轮询、localhost callback 捕获,以及桌面授权完成后的凭据落库。 - `flows/kiro/background/publisher-kiro-rs.js`:Kiro 发布器,负责 `kiro.rs` 地址归一化、连通性探测、BuilderId profileArn 固定映射、machineId 计算、上传 payload 构建与最终凭据上传。 - `flows/grok/background/state.js`:Grok 独立运行态模型与状态工具,负责 `runtimeState.flowState.grok.session / register / sso` 的默认值、归一化、兼容字段投影、节点完成回写、下游重置和 fresh-attempt keep-state 构建。 -- `flows/grok/background/register-runner.js`:Grok 注册页执行器,负责步骤 1~5 的编排、Grok/xAI 注册标签页管理、邮箱提交、验证码轮询提交、资料/密码提交、SSO Cookie 提取、去重保存与账号记录收尾。 +- `flows/grok/background/register-runner.js`:Grok 注册页执行器,负责步骤 1~5 的编排、Grok/xAI 注册标签页管理、邮箱提交、验证码轮询提交、资料/密码提交、SSO Cookie 提取、去重保存与账号记录收尾,并在配置了 flow 级远程注入 URL/Admin Key 时可选执行 Grok SSO Cookie 远程账号池注入。 - `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。 - `background/ip-proxy-provider-711proxy.js`:711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。 @@ -82,6 +82,7 @@ ## `background/steps/` +- `flows/openai/background/steps/remote-account-inject.js`:OpenAI 显式远程账号注入节点实现,负责从当前流程已登记或状态保存的 ChatGPT 标签页读取 accessToken,并按 flow 级 `remoteAccountInjectEnabled / remoteAccountInjectUrl / remoteAccountInjectAdminKey` 提交到远端账号池;默认 workflow 不插入该节点,只有侧边栏显式开启远程注入开关后才会插入,缺少当前流程标签页时失败而不扫描任意 ChatGPT/OpenAI 标签页。 - `flows/openai/background/steps/wait-registration-success.js`:步骤 6 实现,负责注册资料提交后等待 20 秒,让注册成功状态和页面跳转稳定;默认不清理 cookies,只有侧栏第六步 `清 Cookies` 开关开启时才会在等待结束后清理 ChatGPT / OpenAI 相关 cookies。 - `flows/openai/background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。 - `flows/openai/background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 PayPal / GoPay checkout session,或在 GPC 模式下读取 accessToken 并通过 `https://gpc.qlhazycoder.top/api/gp/tasks` 创建队列任务,记录 checkout / GPC task 运行态。 @@ -132,7 +133,7 @@ - `data/account-run-history.txt`:账号运行历史的文本快照样例文件,保留本地 helper 旧追加格式的成功/失败/停止记录,便于人工查看与兼容排查。 - `data/names.js`:随机姓名、生日等测试数据源。 - `data/address-sources.js`:Plus 模式本地地址 seed 表,负责按国家选择用于触发 checkout 内置 Google 地址推荐的查询词和结构化地址 fallback;第一版不实时抓取外部地址网站。 -- `data/step-definitions.js`:共享步骤元数据,前后台共同使用,用于动态渲染、workflow 生成和步骤注册;当前 `openai` flow 会按 `plusPaymentMethod / signupMethod` 动态生成普通/Plus workflow,`kiro` flow 则提供独立的 9 节点注册/桌面授权/上传 workflow。 +- `data/step-definitions.js`:共享步骤元数据,前后台共同使用,用于动态渲染、workflow 生成和步骤注册;当前 `openai` flow 会按 `plusPaymentMethod / signupMethod` 动态生成普通/Plus workflow,且只有侧边栏持久化 `remoteAccountInjectEnabled: true` 并传入步骤定义时才在 OpenAI workflow 中插入 `remote-account-inject`,`kiro` flow 则提供独立的 9 节点注册/桌面授权/上传 workflow。 ## `docs/` @@ -156,7 +157,7 @@ - `flows/openai/mail-rules.js`:OpenAI flow 的验证码邮件规则定义,负责按注册/登录节点输出 code pattern、关键词、目标邮箱提示、2925 receive 弱匹配与轮询参数。 - `flows/kiro/mail-rules.js`:Kiro flow 的 AWS Builder ID 邮件规则定义,负责按注册验证码节点与桌面授权验证码节点输出 AWS 发件人、主题、关键词、code pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。 -- `flows/grok/index.js`:Grok flow 定义,声明 `webchat2api` target、Grok 注册页 runtime source、Grok 专属设置分组、能力边界与本地 SSO 导出属性;该 flow 当前没有 publication target,也不支持贡献 adapter。 +- `flows/grok/index.js`:Grok flow 定义,声明 `webchat2api` target、Grok 注册页 runtime source、Grok 专属设置分组、可选远程 SSO 注入 URL/Admin Key 行、能力边界与本地 SSO 导出属性;该 flow 当前没有 publication target,也不支持贡献 adapter。 - `flows/grok/workflow.js`:Grok workflow 定义,固定输出打开注册页、提交邮箱、提交验证码、提交资料、提取 SSO Cookie 五个节点。 - `flows/grok/mail-rules.js`:Grok flow 的 xAI / Grok 验证码邮件规则定义,负责输出发件人、主题、关键词、`ABC-123` 与 6 位码 pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。 - `flows/grok/content/register-page.js`:Grok 注册页内容脚本,负责五个 Grok 节点的 DOM 操作、页面状态读取与局部点击事件坐标补齐,不修改全局浏览器原型。 @@ -189,7 +190,7 @@ - `core/flow-kernel/flow-registry.js`:flow/source/settings group 总注册表,定义 `openai / kiro / grok` 三套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。 - `shared/kiro-timeouts.js`:Kiro 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。 - `shared/contribution-registry.js`:贡献 adapter 与教程入口注册表,负责 OpenAI / Kiro 贡献 adapter、教程入口和发布贡献 flow 校验;默认发布校验只覆盖声明了 publication target 或支持贡献的 flow,Grok 这类本地 SSO 导出 flow 不自动纳入贡献 adapter 缺失检查。 -- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*` 与 `settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro/Grok 的 `flows..targetId / targets` 与 `stepExecutionRangeByFlow`。 +- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*` 与 `settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro/Grok 的 `flows..targetId / targets` 与 `stepExecutionRangeByFlow`;OpenAI 的远程注入开关、URL/Admin Key 与 Grok 远程注入 URL/Admin Key 均归一在 flow 级,不写入 target 级副本。 - `core/flow-kernel/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。 ## `sidepanel/`