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 @@
+
+ 远程地址
+
+
+
+
+ 远程注入
+
+ 默认关闭;开启后才插入 AT 注入节点
+
+
+ 远程地址
+
+
+
账户密码
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/`