重构多 flow 邮件轮询并接入 Grok SSO

This commit is contained in:
QLHazyCoder
2026-05-23 04:29:37 +08:00
parent 5610262f7d
commit 2ea9ad978e
37 changed files with 4345 additions and 843 deletions
+162 -67
View File
@@ -5,6 +5,8 @@ importScripts(
'flows/openai/workflow.js',
'flows/kiro/index.js',
'flows/kiro/workflow.js',
'flows/grok/index.js',
'flows/grok/workflow.js',
'flows/index.js',
'core/flow-kernel/flow-registry.js',
'shared/contribution-registry.js',
@@ -34,9 +36,11 @@ importScripts(
'core/flow-kernel/workflow-engine.js',
'core/flow-kernel/runtime-state.js',
'flows/kiro/background/state.js',
'flows/grok/background/state.js',
'flows/kiro/background/credential-artifact.js',
'background/contribution/adapters/kiro-builder-id.js',
'flows/kiro/background/register-runner.js',
'flows/grok/background/register-runner.js',
'flows/kiro/background/desktop-client.js',
'flows/kiro/background/desktop-authorize-runner.js',
'flows/kiro/background/publisher-kiro-rs.js',
@@ -44,6 +48,9 @@ importScripts(
'background/signup-flow-helpers.js',
'background/mail-rule-registry.js',
'flows/openai/mail-rules.js',
'flows/kiro/mail-rules.js',
'flows/grok/mail-rules.js',
'background/flow-mail-polling.js',
'background/message-router.js',
'background/verification-flow.js',
'background/auto-run-controller.js',
@@ -424,6 +431,7 @@ const runtimeStateHelpers = self.MultiPageBackgroundRuntimeState?.createRuntimeS
defaultNodeStatuses: DEFAULT_NODE_STATUSES,
}) || null;
const kiroStateHelpers = self.MultiPageBackgroundKiroState || null;
const grokStateHelpers = self.MultiPageBackgroundGrokState || null;
const DEFAULT_REGISTRATION_EMAIL_STATE = registrationEmailStateHelpers?.DEFAULT_REGISTRATION_EMAIL_STATE || {
current: '',
previous: '',
@@ -496,6 +504,9 @@ function buildStateViewWithRuntimeState(state = {}) {
if (kiroStateHelpers?.buildStateView) {
nextState = kiroStateHelpers.buildStateView(nextState);
}
if (grokStateHelpers?.buildStateView) {
nextState = grokStateHelpers.buildStateView(nextState);
}
return nextState;
}
@@ -3809,20 +3820,20 @@ function buildSettingsStatePatchFromFlatUpdates(updates = {}) {
assignIfUpdated('kiroRsKey', ['flows', 'kiro', 'targets', 'kiro-rs', 'apiKey']);
if (hasUpdate('stepExecutionRangeByFlow') && isPlainObjectValue(updates.stepExecutionRangeByFlow)) {
if (isPlainObjectValue(updates.stepExecutionRangeByFlow.openai)) {
Object.entries(updates.stepExecutionRangeByFlow).forEach(([rawFlowId, range]) => {
if (!isPlainObjectValue(range)) {
return;
}
const flowId = normalizePatchFlowId(rawFlowId, '');
if (!flowId) {
return;
}
setSettingsStatePatchValue(
patch,
['flows', 'openai', 'autoRun', 'stepExecutionRange'],
updates.stepExecutionRangeByFlow.openai
['flows', flowId, 'autoRun', 'stepExecutionRange'],
range
);
}
if (isPlainObjectValue(updates.stepExecutionRangeByFlow.kiro)) {
setSettingsStatePatchValue(
patch,
['flows', 'kiro', 'autoRun', 'stepExecutionRange'],
updates.stepExecutionRangeByFlow.kiro
);
}
});
}
return patch;
@@ -3941,6 +3952,25 @@ function buildAutoRunFreshResetSettingsState(prevState = {}, activeFlowId = DEFA
})
: currentSettingsState;
const normalizedStepExecutionRangeByFlow = normalizeStepExecutionRangeByFlow(prevState?.stepExecutionRangeByFlow || {});
const flowIds = typeof self.MultiPageFlowRegistry?.getRegisteredFlowIds === 'function'
? self.MultiPageFlowRegistry.getRegisteredFlowIds()
.map((flowId) => self.MultiPageFlowRegistry.normalizeFlowId?.(flowId, '') || String(flowId || '').trim().toLowerCase())
.filter(Boolean)
: ['openai', 'kiro'];
const flowPatch = {};
flowIds.forEach((flowId) => {
const selectedTargetId = settingsSchemaApi?.getSelectedTargetId
? settingsSchemaApi.getSelectedTargetId(normalizedCurrentSettingsState, flowId)
: undefined;
flowPatch[flowId] = {
selectedTargetId,
autoRun: normalizedStepExecutionRangeByFlow[flowId]
? {
stepExecutionRange: normalizedStepExecutionRangeByFlow[flowId],
}
: undefined,
};
});
const nextSettingsStatePatch = {
activeFlowId,
services: {
@@ -3956,28 +3986,7 @@ function buildAutoRunFreshResetSettingsState(prevState = {}, activeFlowId = DEFA
mode: prevState?.ipProxyMode,
},
},
flows: {
openai: {
selectedTargetId: settingsSchemaApi?.getSelectedTargetId
? settingsSchemaApi.getSelectedTargetId(normalizedCurrentSettingsState, 'openai')
: undefined,
autoRun: normalizedStepExecutionRangeByFlow.openai
? {
stepExecutionRange: normalizedStepExecutionRangeByFlow.openai,
}
: undefined,
},
kiro: {
selectedTargetId: settingsSchemaApi?.getSelectedTargetId
? settingsSchemaApi.getSelectedTargetId(normalizedCurrentSettingsState, 'kiro')
: undefined,
autoRun: normalizedStepExecutionRangeByFlow.kiro
? {
stepExecutionRange: normalizedStepExecutionRangeByFlow.kiro,
}
: undefined,
},
},
flows: flowPatch,
};
return mergeAutoRunKeepStateValue(currentSettingsState, nextSettingsStatePatch);
@@ -4021,6 +4030,9 @@ function buildFreshAutoRunKeepState(prevState = {}) {
if (typeof kiroStateHelpers?.buildFreshKeepState === 'function') {
Object.assign(keepState, kiroStateHelpers.buildFreshKeepState(sourceState));
}
if (typeof grokStateHelpers?.buildFreshKeepState === 'function') {
Object.assign(keepState, grokStateHelpers.buildFreshKeepState(sourceState));
}
if (Object.prototype.hasOwnProperty.call(sourceState, 'settingsSchemaVersion')) {
keepState.settingsSchemaVersion = Number(sourceState.settingsSchemaVersion) || 0;
}
@@ -4329,6 +4341,28 @@ function broadcastDataUpdate(payload) {
}).catch(() => { });
}
async function clearGrokSsoCookies() {
const currentState = await getState();
const patch = typeof grokStateHelpers?.buildRuntimeStatePatch === 'function'
? grokStateHelpers.buildRuntimeStatePatch(currentState, {
sso: {
currentCookie: '',
cookies: [],
extractedAt: 0,
},
})
: {
grokSsoCookie: '',
grokSsoCookies: [],
grokSsoExtractedAt: 0,
};
await setState(patch);
const nextState = await getState();
broadcastDataUpdate(patch);
await addLog('Grok SSO Cookie 已清空。', 'info', { nodeId: 'grok-extract-sso-cookie' });
return { ok: true, state: nextState };
}
function broadcastIcloudAliasesChanged(payload = {}) {
chrome.runtime.sendMessage({
type: 'ICLOUD_ALIASES_CHANGED',
@@ -9607,6 +9641,14 @@ function getDownstreamStateResets(step, state = {}) {
};
}
}
if (String(stepKey || '').trim().toLowerCase().startsWith('grok-')) {
const grokResets = typeof grokStateHelpers?.buildDownstreamResetPatch === 'function'
? grokStateHelpers.buildDownstreamResetPatch(stepKey, state)
: {};
if (Object.keys(grokResets).length > 0) {
return grokResets;
}
}
const plusRuntimeResets = {
plusCheckoutTabId: null,
plusCheckoutUrl: null,
@@ -10801,6 +10843,16 @@ async function handleNodeData(nodeId, payload) {
}
return;
}
if (String(nodeDefinition?.flowId || '').trim().toLowerCase() === 'grok') {
const updates = typeof grokStateHelpers?.applyNodeCompletionPayload === 'function'
? grokStateHelpers.applyNodeCompletionPayload(state, payload || {})
: {};
if (Object.keys(updates).length > 0) {
await setState(updates);
broadcastDataUpdate(updates);
}
return;
}
const step = getStepIdByNodeIdForState(nodeId, state);
if (!Number.isInteger(step) || step <= 0) {
return;
@@ -10858,6 +10910,11 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
'kiro-start-desktop-authorize',
'kiro-complete-desktop-authorize',
'kiro-upload-credential',
'grok-open-signup-page',
'grok-submit-email',
'grok-submit-verification-code',
'grok-submit-profile',
'grok-extract-sso-cookie',
]);
const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([
'fill-password',
@@ -13369,8 +13426,9 @@ async function resumeAutoRun() {
const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
const OPENAI_AUTH_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'flows/openai/content/auth-page-recovery.js', 'flows/openai/content/phone-country-utils.js', 'flows/openai/content/phone-auth.js', 'flows/openai/content/openai-auth.js'];
const KIRO_REGISTER_INJECT_FILES = ['flows/openai/index.js', 'flows/kiro/index.js', 'flows/index.js', 'core/flow-kernel/flow-registry.js', 'core/flow-kernel/source-registry.js', 'shared/kiro-timeouts.js', 'content/utils.js', 'flows/kiro/content/register-page.js'];
const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = ['flows/openai/index.js', 'flows/kiro/index.js', 'flows/index.js', 'core/flow-kernel/flow-registry.js', 'core/flow-kernel/source-registry.js', 'shared/kiro-timeouts.js', 'content/utils.js', 'flows/kiro/content/desktop-authorize-page.js'];
const KIRO_REGISTER_INJECT_FILES = ['flows/openai/index.js', 'flows/kiro/index.js', 'flows/grok/index.js', 'flows/index.js', 'core/flow-kernel/flow-registry.js', 'core/flow-kernel/source-registry.js', 'shared/kiro-timeouts.js', 'content/utils.js', 'flows/kiro/content/register-page.js'];
const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = ['flows/openai/index.js', 'flows/kiro/index.js', 'flows/grok/index.js', 'flows/index.js', 'core/flow-kernel/flow-registry.js', 'core/flow-kernel/source-registry.js', 'shared/kiro-timeouts.js', 'content/utils.js', 'flows/kiro/content/desktop-authorize-page.js'];
const GROK_REGISTER_INJECT_FILES = ['flows/openai/index.js', 'flows/kiro/index.js', 'flows/grok/index.js', 'flows/index.js', 'core/flow-kernel/flow-registry.js', 'core/flow-kernel/source-registry.js', 'content/utils.js', 'flows/grok/content/register-page.js'];
const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
chrome,
addLog,
@@ -13428,12 +13486,50 @@ const openAiMailRules = self.MultiPageOpenAiMailRules?.createOpenAiMailRules({
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
});
const kiroMailRules = self.MultiPageKiroMailRules?.createKiroMailRules({
LUCKMAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
});
const grokMailRules = self.MultiPageGrokMailRules?.createGrokMailRules({
LUCKMAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
});
const mailRuleRegistry = self.MultiPageBackgroundMailRuleRegistry?.createMailRuleRegistry({
defaultFlowId: DEFAULT_ACTIVE_FLOW_ID,
flowBuilders: {
openai: openAiMailRules,
kiro: kiroMailRules,
grok: grokMailRules,
},
});
const flowMailPollingService = self.MultiPageBackgroundFlowMailPolling?.createFlowMailPollingService({
addLog,
buildVerificationPollPayloadForNode: mailRuleRegistry?.buildVerificationPollPayloadForNode,
chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
ensureMail2925MailboxSession,
getMailConfig,
getTabId,
handleMail2925LimitReachedError,
HOTMAIL_PROVIDER,
isMail2925LimitReachedError,
isStopError,
isTabAlive,
LUCKMAIL_PROVIDER,
pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode,
pollHotmailVerificationCode,
pollLuckmailVerificationCode,
pollYydsMailVerificationCode,
reuseOrCreateTab,
sendToMailContentScriptResilient,
throwIfStopped,
YYDS_MAIL_PROVIDER,
});
const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({
addLog,
buildVerificationPollPayload: mailRuleRegistry?.buildVerificationPollPayload,
@@ -13802,33 +13898,18 @@ const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKir
chrome,
ensureContentScriptReadyOnTab,
completeNodeFromBackground,
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
ensureMail2925MailboxSession,
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
generatePassword,
generateRandomName,
getMailConfig,
getTabId,
getState,
HOTMAIL_PROVIDER,
isTabAlive,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
YYDS_MAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
isRetryableContentScriptTransportError,
pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode,
pollHotmailVerificationCode,
pollLuckmailVerificationCode,
pollYydsMailVerificationCode,
pollFlowVerificationCode: flowMailPollingService?.pollFlowVerificationCode,
registerTab,
resolveSignupEmailForFlow,
reuseOrCreateTab,
sendToContentScriptResilient,
sendToMailContentScriptResilient,
setPasswordState,
setState,
sleepWithStop,
@@ -13836,6 +13917,29 @@ const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKir
waitForTabStableComplete,
KIRO_REGISTER_INJECT_FILES,
});
const grokRegisterRunner = self.MultiPageBackgroundGrokRegisterRunner?.createGrokRegisterRunner({
addLog,
chrome,
ensureContentScriptReadyOnTab,
completeNodeFromBackground,
generatePassword,
generateRandomName,
getTabId,
getState,
isTabAlive,
pollFlowVerificationCode: flowMailPollingService?.pollFlowVerificationCode,
registerTab,
resolveSignupEmailForFlow,
reuseOrCreateTab,
sendToContentScriptResilient,
setPasswordState,
setState,
sleepWithStop,
throwIfStopped,
waitForTabStableComplete,
GROK_REGISTER_INJECT_FILES,
markCurrentRegistrationAccountUsed,
});
const kiroBuilderIdContributionAdapter = self.MultiPageBackgroundKiroBuilderIdContributionAdapter?.createKiroBuilderIdContributionAdapter?.({
addLog,
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
@@ -13867,31 +13971,16 @@ const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeR
chrome,
completeNodeFromBackground,
ensureContentScriptReadyOnTab,
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
ensureMail2925MailboxSession,
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getMailConfig,
getTabId,
getState,
HOTMAIL_PROVIDER,
isTabAlive,
KIRO_REGISTER_INJECT_FILES,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
YYDS_MAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
maybeSubmitFlowContribution,
pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode,
pollHotmailVerificationCode,
pollLuckmailVerificationCode,
pollYydsMailVerificationCode,
pollFlowVerificationCode: flowMailPollingService?.pollFlowVerificationCode,
registerTab,
reuseOrCreateTab,
sendToContentScriptResilient,
sendToMailContentScriptResilient,
setState,
sleepWithStop,
throwIfStopped,
@@ -14000,6 +14089,11 @@ const stepExecutorsByKey = {
'kiro-start-desktop-authorize': (state) => kiroDesktopAuthorizeRunner.executeKiroStartDesktopAuthorize(state),
'kiro-complete-desktop-authorize': (state) => kiroDesktopAuthorizeRunner.executeKiroCompleteDesktopAuthorize(state),
'kiro-upload-credential': (state) => kiroPublisher.executeKiroUploadCredential(state),
'grok-open-signup-page': (state) => grokRegisterRunner.executeGrokOpenSignupPage(state),
'grok-submit-email': (state) => grokRegisterRunner.executeGrokSubmitEmail(state),
'grok-submit-verification-code': (state) => grokRegisterRunner.executeGrokSubmitVerificationCode(state),
'grok-submit-profile': (state) => grokRegisterRunner.executeGrokSubmitProfile(state),
'grok-extract-sso-cookie': (state) => grokRegisterRunner.executeGrokExtractSsoCookie(state),
};
const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
addLog,
@@ -14016,6 +14110,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
clearAutoRunTimerAlarm,
clearFreeReusablePhoneActivation,
clearGrokSsoCookies,
clearLuckmailRuntimeState,
clearYydsMailRuntimeState,
clearStopRequest,
+354
View File
@@ -0,0 +1,354 @@
(function attachBackgroundFlowMailPolling(root, factory) {
root.MultiPageBackgroundFlowMailPolling = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundFlowMailPollingModule() {
const ICLOUD_MAIL_POLL_MIN_ATTEMPTS = 5;
const ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS = 25000;
function cleanString(value = '') {
return String(value ?? '').trim();
}
function normalizeProviderId(value = '') {
return cleanString(value).toLowerCase();
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS);
}
function isIcloudMail(mail = {}) {
return mail?.source === 'icloud-mail' || mail?.provider === 'icloud';
}
function normalizeIcloudMailPollPayload(mail = {}, payload = {}) {
if (!isIcloudMail(mail)) {
return payload;
}
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
if (maxAttempts >= ICLOUD_MAIL_POLL_MIN_ATTEMPTS) {
return payload;
}
return {
...payload,
maxAttempts: ICLOUD_MAIL_POLL_MIN_ATTEMPTS,
};
}
function resolveMailPollingTimeouts(mail = {}, payload = {}) {
const normalizedPayload = normalizeIcloudMailPollPayload(mail, payload);
const responseTimeoutMs = getMailPollingResponseTimeoutMs(normalizedPayload);
return {
payload: normalizedPayload,
responseTimeoutMs,
timeoutMs: responseTimeoutMs,
};
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = cleanString(state?.currentMail2925AccountId);
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => cleanString(account?.id) === currentAccountId) || null;
const accountEmail = cleanString(currentAccount?.email).toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return cleanString(state?.mail2925BaseEmail).toLowerCase();
}
function createFlowMailPollingService(deps = {}) {
const {
addLog = async () => {},
buildVerificationPollPayloadForNode = null,
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
getMailConfig = null,
getTabId = async () => null,
handleMail2925LimitReachedError = null,
HOTMAIL_PROVIDER = 'hotmail-api',
isMail2925LimitReachedError = null,
isStopError = null,
isTabAlive = async () => false,
LUCKMAIL_PROVIDER = 'luckmail-api',
pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
reuseOrCreateTab = async () => null,
sendToMailContentScriptResilient = null,
throwIfStopped = () => {},
YYDS_MAIL_PROVIDER = 'yyds-mail',
} = deps;
const apiProviderHandlers = new Map([
[normalizeProviderId(HOTMAIL_PROVIDER), {
label: 'Hotmail',
poll: pollHotmailVerificationCode,
}],
[normalizeProviderId(LUCKMAIL_PROVIDER), {
label: 'LuckMail',
poll: pollLuckmailVerificationCode,
}],
[normalizeProviderId(CLOUDFLARE_TEMP_EMAIL_PROVIDER), {
label: 'Cloudflare Temp Email',
poll: pollCloudflareTempEmailVerificationCode,
}],
[normalizeProviderId(CLOUD_MAIL_PROVIDER), {
label: 'Cloud Mail',
poll: pollCloudMailVerificationCode,
}],
[normalizeProviderId(YYDS_MAIL_PROVIDER), {
label: 'YYDS Mail',
poll: pollYydsMailVerificationCode,
}],
]);
async function log(message, level = 'info', options = {}) {
const logOptions = options && typeof options === 'object' ? { ...options } : {};
await addLog(message, level, logOptions);
}
async function activateTab(tabId) {
if (!Number.isInteger(tabId) || !chrome?.tabs?.update) {
return;
}
await chrome.tabs.update(tabId, { active: true });
}
async function focusOrOpenMailTab(mail = {}) {
if (!mail?.source) {
return;
}
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
if (Number.isInteger(tabId)) {
await activateTab(tabId);
}
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
function assertPollResult(result, notFoundMessage) {
if (result?.error) {
throw new Error(result.error);
}
if (!result?.code) {
throw new Error(notFoundMessage || '邮箱轮询结束,但未获取到验证码。');
}
return result;
}
function isMail2925Provider(mail = {}) {
return normalizeProviderId(mail?.provider) === '2925';
}
async function pollThroughApiProvider(providerId, step, state, pollPayload, mail, options) {
const handler = apiProviderHandlers.get(providerId);
if (!handler) {
return null;
}
if (typeof handler.poll !== 'function') {
throw new Error(`${handler.label} 邮箱轮询能力未接入,无法继续执行。`);
}
await log(
`步骤 ${step}:正在通过 ${mail.label || handler.label} 轮询${options.actionLabel || '验证码'}...`,
'info',
options.logOptions
);
const result = await handler.poll(step, state, pollPayload);
return assertPollResult(result, options.notFoundMessage);
}
async function ensureBrowserMailSession(step, state, mail, options) {
if (isIcloudMail(mail) && typeof ensureIcloudMailSession === 'function') {
await log(
`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`,
'info',
options.logOptions
);
await ensureIcloudMailSession({
state,
step,
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
});
return;
}
if (isMail2925Provider(mail) && typeof ensureMail2925MailboxSession === 'function') {
await log(
`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`,
'info',
options.logOptions
);
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
});
return;
}
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', options.logOptions);
await focusOrOpenMailTab(mail);
}
async function pollThroughBrowserProvider(step, state, mail, pollPayload, options) {
await ensureBrowserMailSession(step, state, mail, options);
if (typeof sendToMailContentScriptResilient !== 'function') {
throw new Error(options.missingContentScriptMessage || '当前验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
}
const timeoutWindow = resolveMailPollingTimeouts(mail, pollPayload);
try {
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: timeoutWindow.payload,
},
{
timeoutMs: timeoutWindow.timeoutMs,
responseTimeoutMs: timeoutWindow.responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: options.logStep || step,
logStepKey: options.logStepKey || options.nodeId || '',
}
);
return assertPollResult(result, options.notFoundMessage);
} catch (error) {
if (typeof isStopError === 'function' && isStopError(error)) {
throw error;
}
if (
isMail2925Provider(mail)
&& typeof isMail2925LimitReachedError === 'function'
&& isMail2925LimitReachedError(error)
&& typeof handleMail2925LimitReachedError === 'function'
) {
throw await handleMail2925LimitReachedError(step, error);
}
throw error;
}
}
async function pollFlowVerificationCode(options = {}) {
const {
actionLabel = '验证码',
filterAfterTimestamp,
flowId = '',
logStep,
logStepKey = '',
missingCapabilityMessage = '当前验证码步骤缺少共享邮件轮询能力,无法继续执行。',
nodeId = '',
notFoundMessage = '',
payloadOverrides = {},
state = {},
step = 0,
} = options;
if (typeof getMailConfig !== 'function') {
throw new Error('当前验证码步骤缺少邮箱配置能力,无法继续执行。');
}
if (typeof buildVerificationPollPayloadForNode !== 'function') {
throw new Error(missingCapabilityMessage);
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
}
const ruleState = flowId
? {
...state,
activeFlowId: flowId,
flowId,
}
: state;
const nextOverrides = {
...(payloadOverrides || {}),
};
if (filterAfterTimestamp !== undefined) {
nextOverrides.filterAfterTimestamp = filterAfterTimestamp;
}
const pollPayload = buildVerificationPollPayloadForNode(nodeId, ruleState, nextOverrides);
const normalizedStep = Math.max(1, Math.floor(Number(pollPayload?.step || step) || 1));
const providerId = normalizeProviderId(mail?.provider);
const logOptions = {};
const normalizedLogStep = Math.floor(Number(logStep || normalizedStep) || 0);
if (normalizedLogStep > 0) {
logOptions.step = normalizedLogStep;
}
if (logStepKey || nodeId) {
logOptions.stepKey = logStepKey || nodeId;
logOptions.nodeId = nodeId || logStepKey;
}
throwIfStopped();
const apiResult = await pollThroughApiProvider(providerId, normalizedStep, ruleState, pollPayload, mail, {
actionLabel,
logOptions,
notFoundMessage,
});
if (apiResult) {
return apiResult;
}
return pollThroughBrowserProvider(normalizedStep, ruleState, mail, pollPayload, {
actionLabel,
logOptions,
logStep: normalizedLogStep || normalizedStep,
logStepKey: logStepKey || nodeId,
missingContentScriptMessage: missingCapabilityMessage,
nodeId,
notFoundMessage: notFoundMessage || `步骤 ${normalizedStep}:邮箱轮询结束,但未获取到验证码。`,
});
}
return {
focusOrOpenMailTab,
getExpectedMail2925MailboxEmail,
getMailPollingResponseTimeoutMs,
pollFlowVerificationCode,
resolveMailPollingTimeouts,
};
}
return {
createFlowMailPollingService,
getExpectedMail2925MailboxEmail,
getMailPollingResponseTimeoutMs,
resolveMailPollingTimeouts,
};
});
+8
View File
@@ -17,6 +17,7 @@
deleteAccountRunHistoryRecords,
clearAutoRunTimerAlarm,
clearFreeReusablePhoneActivation,
clearGrokSsoCookies,
clearLuckmailRuntimeState,
clearYydsMailRuntimeState,
clearStopRequest,
@@ -1165,6 +1166,13 @@
return await clearFreeReusablePhoneActivation();
}
case 'CLEAR_GROK_SSO_COOKIES': {
if (typeof clearGrokSsoCookies !== 'function') {
throw new Error('Grok SSO 清空能力未接入。');
}
return await clearGrokSsoCookies();
}
case 'SET_FREE_REUSABLE_PHONE': {
if (typeof setFreeReusablePhoneActivation !== 'function') {
throw new Error('白嫖复用手机号记录能力未接入。');
+441 -335
View File
@@ -17,6 +17,23 @@
return value;
}
function mergePlainObjects(baseValue = {}, patchValue = {}) {
if (Array.isArray(patchValue)) {
return patchValue.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchValue)) {
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
}
const baseObject = isPlainObject(baseValue) ? baseValue : {};
const next = {
...cloneValue(baseObject),
};
Object.entries(patchValue).forEach(([key, value]) => {
next[key] = mergePlainObjects(baseObject[key], value);
});
return next;
}
function normalizeStepExecutionRangeEntry(value = {}, fallback = {}) {
const source = isPlainObject(value) ? value : {};
const fallbackSource = isPlainObject(fallback) ? fallback : {};
@@ -47,6 +64,19 @@
const normalizeTargetId = typeof flowRegistry.normalizeTargetId === 'function'
? flowRegistry.normalizeTargetId
: ((_flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase());
const getRegisteredFlowIds = typeof flowRegistry.getRegisteredFlowIds === 'function'
? flowRegistry.getRegisteredFlowIds
: (() => ['openai', 'kiro']);
const getFlowDefinition = typeof flowRegistry.getFlowDefinition === 'function'
? flowRegistry.getFlowDefinition
: (() => null);
const getDefaultTargetId = typeof flowRegistry.getDefaultTargetId === 'function'
? flowRegistry.getDefaultTargetId
: ((flowId) => (flowId === 'kiro' ? defaultKiroTargetId : defaultOpenAiTargetId));
const getTargetDefinitions = typeof flowRegistry.getTargetDefinitions === 'function'
? flowRegistry.getTargetDefinitions
: (() => ({}));
const normalizePlusAccountAccessStrategy = (value = '') => {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'sub2api_codex_session') {
@@ -58,6 +88,146 @@
return 'oauth';
};
function getCanonicalFlowIds() {
const ids = Array.isArray(getRegisteredFlowIds())
? getRegisteredFlowIds()
: [];
const seen = new Set();
return ids
.map((flowId) => normalizeFlowId(flowId, defaultFlowId))
.filter((flowId) => {
if (!flowId || seen.has(flowId)) {
return false;
}
seen.add(flowId);
return true;
});
}
function getFlowSettingsDefaults(flowId) {
const definition = getFlowDefinition(flowId) || {};
return isPlainObject(definition.settingsDefaults) ? definition.settingsDefaults : {};
}
function getDefaultStepExecutionRange(flowId) {
const flowDefaults = getFlowSettingsDefaults(flowId);
const range = flowDefaults?.autoRun?.stepExecutionRange;
if (isPlainObject(range)) {
return normalizeStepExecutionRangeEntry(range, { enabled: false, fromStep: 1, toStep: 1 });
}
if (flowId === 'openai') {
return { enabled: false, fromStep: 1, toStep: 11 };
}
if (flowId === 'kiro') {
return { enabled: false, fromStep: 1, toStep: 9 };
}
const lastStep = Math.max(1, Number(getFlowDefinition(flowId)?.workflowStepCount) || 1);
return { enabled: false, fromStep: 1, toStep: lastStep };
}
function buildDefaultTargetState(flowId, targetId) {
if (flowId === 'openai' && targetId === 'cpa') {
return {
vpsUrl: '',
vpsPassword: '',
localCpaStep9Mode: 'submit',
};
}
if (flowId === 'openai' && targetId === 'sub2api') {
return {
sub2apiUrl: '',
sub2apiEmail: '',
sub2apiPassword: '',
sub2apiGroupName: 'codex',
sub2apiGroupNames: ['codex', 'openai-plus'],
sub2apiAccountPriority: 1,
sub2apiDefaultProxyName: '',
};
}
if (flowId === 'openai' && targetId === 'codex2api') {
return {
codex2apiUrl: '',
codex2apiAdminKey: '',
};
}
if (flowId === 'kiro' && targetId === 'kiro-rs') {
return {
baseUrl: defaultKiroRsUrl,
apiKey: '',
};
}
const definition = getFlowDefinition(flowId) || {};
const flowDefaults = getFlowSettingsDefaults(flowId);
const targetDefaults = isPlainObject(flowDefaults?.targets?.[targetId])
? flowDefaults.targets[targetId]
: {};
const sharedTargetDefaults = isPlainObject(definition.defaultTargetState)
? definition.defaultTargetState
: {};
const targetDefinition = isPlainObject(getTargetDefinitions(flowId)?.[targetId])
? getTargetDefinitions(flowId)[targetId]
: {};
const targetDefinitionDefaults = isPlainObject(targetDefinition.defaultState)
? targetDefinition.defaultState
: {};
return mergePlainObjects(
mergePlainObjects(sharedTargetDefaults, targetDefinitionDefaults),
targetDefaults
);
}
function buildDefaultTargets(flowId) {
const targetDefinitions = getTargetDefinitions(flowId) || {};
const targetIds = Object.keys(targetDefinitions);
const defaults = {};
targetIds.forEach((targetId) => {
defaults[targetId] = buildDefaultTargetState(flowId, targetId);
});
return defaults;
}
function buildDefaultFlowSettings(flowId) {
const defaultTargetId = normalizeTargetId(
flowId,
getDefaultTargetId(flowId),
flowId === 'kiro' ? defaultKiroTargetId : defaultOpenAiTargetId
);
const base = {
selectedTargetId: defaultTargetId,
targets: buildDefaultTargets(flowId),
autoRun: {
stepExecutionRange: getDefaultStepExecutionRange(flowId),
},
};
if (flowId === 'openai') {
return mergePlainObjects(base, {
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
},
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal-hosted',
plusAccountAccessStrategy: 'oauth',
hostedCheckoutVerificationUrl: '',
hostedCheckoutPhoneNumber: '',
plusHostedCheckoutOauthDelaySeconds: 3,
},
});
}
return base;
}
function buildDefaultFlows() {
const flows = {};
getCanonicalFlowIds().forEach((flowId) => {
flows[flowId] = buildDefaultFlowSettings(flowId);
});
return flows;
}
function buildDefaultSettingsState() {
return {
schemaVersion: 5,
@@ -75,67 +245,7 @@
mode: 'account',
},
},
flows: {
openai: {
selectedTargetId: defaultOpenAiTargetId,
targets: {
cpa: {
vpsUrl: '',
vpsPassword: '',
localCpaStep9Mode: 'submit',
},
sub2api: {
sub2apiUrl: '',
sub2apiEmail: '',
sub2apiPassword: '',
sub2apiGroupName: 'codex',
sub2apiGroupNames: ['codex', 'openai-plus'],
sub2apiAccountPriority: 1,
sub2apiDefaultProxyName: '',
},
codex2api: {
codex2apiUrl: '',
codex2apiAdminKey: '',
},
},
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
},
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal-hosted',
plusAccountAccessStrategy: 'oauth',
hostedCheckoutVerificationUrl: '',
hostedCheckoutPhoneNumber: '',
plusHostedCheckoutOauthDelaySeconds: 3,
},
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 11,
},
},
},
kiro: {
selectedTargetId: defaultKiroTargetId,
targets: {
'kiro-rs': {
baseUrl: defaultKiroRsUrl,
apiKey: '',
},
},
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 9,
},
},
},
},
flows: buildDefaultFlows(),
};
}
@@ -147,6 +257,223 @@
return cloneValue(resolvedValue);
}
function normalizeFlowTargetState(flowId, targetId, nested = {}, defaults = {}) {
const targetState = mergePlainObjects(defaults, nested);
if (flowId === 'openai' && targetId === 'cpa') {
return {
...targetState,
vpsUrl: String(targetState.vpsUrl ?? '').trim(),
vpsPassword: String(targetState.vpsPassword ?? ''),
localCpaStep9Mode: String(targetState.localCpaStep9Mode ?? 'submit').trim() || 'submit',
};
}
if (flowId === 'openai' && targetId === 'sub2api') {
return {
...targetState,
sub2apiUrl: String(targetState.sub2apiUrl ?? '').trim(),
sub2apiEmail: String(targetState.sub2apiEmail ?? '').trim(),
sub2apiPassword: String(targetState.sub2apiPassword ?? ''),
sub2apiGroupName: String(targetState.sub2apiGroupName ?? 'codex').trim() || 'codex',
sub2apiGroupNames: Array.isArray(targetState.sub2apiGroupNames)
? targetState.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: ['codex', 'openai-plus'],
sub2apiAccountPriority: Math.max(1, Number(targetState.sub2apiAccountPriority) || 1),
sub2apiDefaultProxyName: String(targetState.sub2apiDefaultProxyName ?? '').trim(),
};
}
if (flowId === 'openai' && targetId === 'codex2api') {
return {
...targetState,
codex2apiUrl: String(targetState.codex2apiUrl ?? '').trim(),
codex2apiAdminKey: String(targetState.codex2apiAdminKey ?? '').trim(),
};
}
if (flowId === 'kiro' && targetId === 'kiro-rs') {
return {
...targetState,
baseUrl: String(targetState.baseUrl ?? defaultKiroRsUrl).trim() || defaultKiroRsUrl,
apiKey: String(targetState.apiKey ?? ''),
};
}
return targetState;
}
function buildNormalizedTargets(flowId, nestedFlow = {}, defaultFlow = {}) {
const targetIds = Array.from(new Set([
...Object.keys(defaultFlow.targets || {}),
...Object.keys(isPlainObject(nestedFlow.targets) ? nestedFlow.targets : {}),
...Object.keys(getTargetDefinitions(flowId) || {}),
]));
const targets = {};
targetIds.forEach((targetId) => {
targets[targetId] = normalizeFlowTargetState(
flowId,
targetId,
isPlainObject(nestedFlow.targets?.[targetId]) ? nestedFlow.targets[targetId] : {},
defaultFlow.targets?.[targetId] || buildDefaultTargetState(flowId, targetId)
);
});
return targets;
}
function normalizeFlowSettings(flowId, input = {}, nested = {}, defaults = {}) {
const nestedFlow = isPlainObject(nested?.flows?.[flowId]) ? nested.flows[flowId] : {};
const activeFlowId = normalizeFlowId(
input?.activeFlowId
?? nested?.activeFlowId
?? defaults.activeFlowId,
defaults.activeFlowId
);
const mergedFlow = mergePlainObjects(defaults.flows?.[flowId] || buildDefaultFlowSettings(flowId), nestedFlow);
const defaultTargetId = defaults.flows?.[flowId]?.selectedTargetId || getDefaultTargetId(flowId);
const selectedTargetId = normalizeTargetId(
flowId,
nestedFlow?.selectedTargetId
?? nestedFlow?.targetId
?? (flowId === 'openai' ? nestedFlow?.integrationTargetId : undefined)
?? (activeFlowId === flowId ? (input?.selectedTargetId ?? input?.targetId) : undefined)
?? defaultTargetId,
defaultTargetId
);
const stepExecutionRangeByFlow = isPlainObject(input?.stepExecutionRangeByFlow)
? input.stepExecutionRangeByFlow
: {};
const autoRun = {
...(isPlainObject(mergedFlow.autoRun) ? mergedFlow.autoRun : {}),
stepExecutionRange: normalizeStepExecutionRangeEntry(
stepExecutionRangeByFlow[flowId]
?? nestedFlow?.autoRun?.stepExecutionRange
?? {},
defaults.flows?.[flowId]?.autoRun?.stepExecutionRange || getDefaultStepExecutionRange(flowId)
),
};
return {
...mergedFlow,
selectedTargetId,
targets: buildNormalizedTargets(flowId, nestedFlow, defaults.flows?.[flowId] || {}),
autoRun,
};
}
function normalizeOpenAiSettings(input = {}, nested = {}, defaults = {}, currentFlow = {}) {
const cpaSource = {
...currentFlow.targets.cpa,
...getTargetValue(
nested,
(state) => state.flows?.openai?.integrationTargets?.cpa,
null,
{}
),
vpsUrl: input?.vpsUrl ?? currentFlow.targets.cpa.vpsUrl,
vpsPassword: input?.vpsPassword ?? currentFlow.targets.cpa.vpsPassword,
localCpaStep9Mode: input?.localCpaStep9Mode ?? currentFlow.targets.cpa.localCpaStep9Mode,
};
const sub2apiSource = {
...currentFlow.targets.sub2api,
...getTargetValue(
nested,
(state) => state.flows?.openai?.integrationTargets?.sub2api,
null,
{}
),
sub2apiUrl: input?.sub2apiUrl ?? currentFlow.targets.sub2api.sub2apiUrl,
sub2apiEmail: input?.sub2apiEmail ?? currentFlow.targets.sub2api.sub2apiEmail,
sub2apiPassword: input?.sub2apiPassword ?? currentFlow.targets.sub2api.sub2apiPassword,
sub2apiGroupName: input?.sub2apiGroupName ?? currentFlow.targets.sub2api.sub2apiGroupName,
sub2apiGroupNames: input?.sub2apiGroupNames ?? currentFlow.targets.sub2api.sub2apiGroupNames,
sub2apiAccountPriority: input?.sub2apiAccountPriority ?? currentFlow.targets.sub2api.sub2apiAccountPriority,
sub2apiDefaultProxyName: input?.sub2apiDefaultProxyName ?? currentFlow.targets.sub2api.sub2apiDefaultProxyName,
};
const codex2apiSource = {
...currentFlow.targets.codex2api,
...getTargetValue(
nested,
(state) => state.flows?.openai?.integrationTargets?.codex2api,
null,
{}
),
codex2apiUrl: input?.codex2apiUrl ?? currentFlow.targets.codex2api.codex2apiUrl,
codex2apiAdminKey: input?.codex2apiAdminKey ?? currentFlow.targets.codex2api.codex2apiAdminKey,
};
return {
...currentFlow,
targets: {
...currentFlow.targets,
cpa: normalizeFlowTargetState('openai', 'cpa', cpaSource, defaults.flows.openai.targets.cpa),
sub2api: normalizeFlowTargetState('openai', 'sub2api', sub2apiSource, defaults.flows.openai.targets.sub2api),
codex2api: normalizeFlowTargetState('openai', 'codex2api', codex2apiSource, defaults.flows.openai.targets.codex2api),
},
signup: {
signupMethod: String(
input?.signupMethod
?? currentFlow.signup?.signupMethod
?? defaults.flows.openai.signup.signupMethod
).trim().toLowerCase() === 'phone' ? 'phone' : 'email',
phoneVerificationEnabled: Boolean(
input?.phoneVerificationEnabled
?? currentFlow.signup?.phoneVerificationEnabled
?? defaults.flows.openai.signup.phoneVerificationEnabled
),
phoneSignupReloginAfterBindEmailEnabled: Boolean(
input?.phoneSignupReloginAfterBindEmailEnabled
?? currentFlow.signup?.phoneSignupReloginAfterBindEmailEnabled
?? defaults.flows.openai.signup.phoneSignupReloginAfterBindEmailEnabled
),
},
plus: {
plusModeEnabled: Boolean(
input?.plusModeEnabled
?? currentFlow.plus?.plusModeEnabled
?? defaults.flows.openai.plus.plusModeEnabled
),
plusPaymentMethod: String(
input?.plusPaymentMethod
?? currentFlow.plus?.plusPaymentMethod
?? defaults.flows.openai.plus.plusPaymentMethod
).trim() || defaults.flows.openai.plus.plusPaymentMethod,
plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(
input?.plusAccountAccessStrategy
?? currentFlow.plus?.plusAccountAccessStrategy
?? defaults.flows.openai.plus.plusAccountAccessStrategy
),
hostedCheckoutVerificationUrl: String(
input?.hostedCheckoutVerificationUrl
?? currentFlow.plus?.hostedCheckoutVerificationUrl
?? defaults.flows.openai.plus.hostedCheckoutVerificationUrl
).trim(),
hostedCheckoutPhoneNumber: String(
input?.hostedCheckoutPhoneNumber
?? currentFlow.plus?.hostedCheckoutPhoneNumber
?? defaults.flows.openai.plus.hostedCheckoutPhoneNumber
).trim(),
plusHostedCheckoutOauthDelaySeconds: (() => {
const numeric = Number(
input?.plusHostedCheckoutOauthDelaySeconds
?? currentFlow.plus?.plusHostedCheckoutOauthDelaySeconds
?? defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds
);
return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds)));
})(),
},
};
}
function normalizeKiroSettings(input = {}, defaults = {}, currentFlow = {}) {
const targetSource = {
...currentFlow.targets['kiro-rs'],
baseUrl: input?.kiroRsUrl ?? input?.kiroRsBaseUrl ?? currentFlow.targets['kiro-rs'].baseUrl,
apiKey: input?.kiroRsKey ?? input?.kiroRsApiKey ?? currentFlow.targets['kiro-rs'].apiKey,
};
return {
...currentFlow,
targets: {
...currentFlow.targets,
'kiro-rs': normalizeFlowTargetState('kiro', 'kiro-rs', targetSource, defaults.flows.kiro.targets['kiro-rs']),
},
};
}
function normalizeSettingsState(input = {}, options = {}) {
const defaults = buildDefaultSettingsState();
const nested = isPlainObject(input?.settingsState)
@@ -159,31 +486,7 @@
?? defaults.activeFlowId,
defaults.activeFlowId
);
const openaiSelectedTargetId = normalizeTargetId(
'openai',
nested?.flows?.openai?.selectedTargetId
?? nested?.flows?.openai?.integrationTargetId
?? (activeFlowId === 'openai'
? (input?.selectedTargetId ?? input?.targetId)
: undefined)
?? defaults.flows.openai.selectedTargetId,
defaults.flows.openai.selectedTargetId
);
const kiroSelectedTargetId = normalizeTargetId(
'kiro',
nested?.flows?.kiro?.selectedTargetId
?? nested?.flows?.kiro?.targetId
?? (activeFlowId === 'kiro'
? (input?.selectedTargetId ?? input?.targetId)
: undefined)
?? defaults.flows.kiro.selectedTargetId,
defaults.flows.kiro.selectedTargetId
);
const stepExecutionRangeByFlow = isPlainObject(input?.stepExecutionRangeByFlow)
? input.stepExecutionRangeByFlow
: {};
return {
const normalized = {
schemaVersion: Number(input?.settingsSchemaVersion || nested?.schemaVersion || defaults.schemaVersion) || defaults.schemaVersion,
activeFlowId,
services: {
@@ -219,203 +522,22 @@
).trim(),
},
},
flows: {
openai: {
selectedTargetId: openaiSelectedTargetId,
targets: {
cpa: {
...defaults.flows.openai.targets.cpa,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.cpa,
(state) => state.flows?.openai?.integrationTargets?.cpa
),
vpsUrl: String(
input?.vpsUrl
?? nested?.flows?.openai?.targets?.cpa?.vpsUrl
?? nested?.flows?.openai?.integrationTargets?.cpa?.vpsUrl
?? ''
).trim(),
vpsPassword: String(
input?.vpsPassword
?? nested?.flows?.openai?.targets?.cpa?.vpsPassword
?? nested?.flows?.openai?.integrationTargets?.cpa?.vpsPassword
?? ''
),
localCpaStep9Mode: String(
input?.localCpaStep9Mode
?? nested?.flows?.openai?.targets?.cpa?.localCpaStep9Mode
?? nested?.flows?.openai?.integrationTargets?.cpa?.localCpaStep9Mode
?? defaults.flows.openai.targets.cpa.localCpaStep9Mode
).trim() || defaults.flows.openai.targets.cpa.localCpaStep9Mode,
},
sub2api: {
...defaults.flows.openai.targets.sub2api,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.sub2api,
(state) => state.flows?.openai?.integrationTargets?.sub2api
),
sub2apiUrl: String(
input?.sub2apiUrl
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiUrl
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiUrl
?? ''
).trim(),
sub2apiEmail: String(
input?.sub2apiEmail
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiEmail
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiEmail
?? ''
).trim(),
sub2apiPassword: String(
input?.sub2apiPassword
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiPassword
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiPassword
?? ''
),
sub2apiGroupName: String(
input?.sub2apiGroupName
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiGroupName
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiGroupName
?? defaults.flows.openai.targets.sub2api.sub2apiGroupName
).trim() || defaults.flows.openai.targets.sub2api.sub2apiGroupName,
sub2apiGroupNames: Array.isArray(input?.sub2apiGroupNames)
? input.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: (Array.isArray(nested?.flows?.openai?.targets?.sub2api?.sub2apiGroupNames)
? nested.flows.openai.targets.sub2api.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: (Array.isArray(nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiGroupNames)
? nested.flows.openai.integrationTargets.sub2api.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: [...defaults.flows.openai.targets.sub2api.sub2apiGroupNames])),
sub2apiAccountPriority: Math.max(1, Number(
input?.sub2apiAccountPriority
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiAccountPriority
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiAccountPriority
?? defaults.flows.openai.targets.sub2api.sub2apiAccountPriority
) || defaults.flows.openai.targets.sub2api.sub2apiAccountPriority),
sub2apiDefaultProxyName: String(
input?.sub2apiDefaultProxyName
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiDefaultProxyName
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiDefaultProxyName
?? ''
).trim(),
},
codex2api: {
...defaults.flows.openai.targets.codex2api,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.codex2api,
(state) => state.flows?.openai?.integrationTargets?.codex2api
),
codex2apiUrl: String(
input?.codex2apiUrl
?? nested?.flows?.openai?.targets?.codex2api?.codex2apiUrl
?? nested?.flows?.openai?.integrationTargets?.codex2api?.codex2apiUrl
?? ''
).trim(),
codex2apiAdminKey: String(
input?.codex2apiAdminKey
?? nested?.flows?.openai?.targets?.codex2api?.codex2apiAdminKey
?? nested?.flows?.openai?.integrationTargets?.codex2api?.codex2apiAdminKey
?? ''
).trim(),
},
},
signup: {
signupMethod: String(
input?.signupMethod
?? nested?.flows?.openai?.signup?.signupMethod
?? defaults.flows.openai.signup.signupMethod
).trim().toLowerCase() === 'phone' ? 'phone' : 'email',
phoneVerificationEnabled: Boolean(
input?.phoneVerificationEnabled
?? nested?.flows?.openai?.signup?.phoneVerificationEnabled
?? defaults.flows.openai.signup.phoneVerificationEnabled
),
phoneSignupReloginAfterBindEmailEnabled: Boolean(
input?.phoneSignupReloginAfterBindEmailEnabled
?? nested?.flows?.openai?.signup?.phoneSignupReloginAfterBindEmailEnabled
?? defaults.flows.openai.signup.phoneSignupReloginAfterBindEmailEnabled
),
},
plus: {
plusModeEnabled: Boolean(
input?.plusModeEnabled
?? nested?.flows?.openai?.plus?.plusModeEnabled
?? defaults.flows.openai.plus.plusModeEnabled
),
plusPaymentMethod: String(
input?.plusPaymentMethod
?? nested?.flows?.openai?.plus?.plusPaymentMethod
?? defaults.flows.openai.plus.plusPaymentMethod
).trim() || defaults.flows.openai.plus.plusPaymentMethod,
plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(
input?.plusAccountAccessStrategy
?? nested?.flows?.openai?.plus?.plusAccountAccessStrategy
?? defaults.flows.openai.plus.plusAccountAccessStrategy
),
hostedCheckoutVerificationUrl: String(
input?.hostedCheckoutVerificationUrl
?? nested?.flows?.openai?.plus?.hostedCheckoutVerificationUrl
?? defaults.flows.openai.plus.hostedCheckoutVerificationUrl
).trim(),
hostedCheckoutPhoneNumber: String(
input?.hostedCheckoutPhoneNumber
?? nested?.flows?.openai?.plus?.hostedCheckoutPhoneNumber
?? defaults.flows.openai.plus.hostedCheckoutPhoneNumber
).trim(),
plusHostedCheckoutOauthDelaySeconds: (() => {
const numeric = Number(
input?.plusHostedCheckoutOauthDelaySeconds
?? nested?.flows?.openai?.plus?.plusHostedCheckoutOauthDelaySeconds
?? defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds
);
return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds)));
})(),
},
autoRun: {
stepExecutionRange: normalizeStepExecutionRangeEntry(
stepExecutionRangeByFlow.openai
?? nested?.flows?.openai?.autoRun?.stepExecutionRange
?? {},
defaults.flows.openai.autoRun.stepExecutionRange
),
},
},
kiro: {
selectedTargetId: kiroSelectedTargetId,
targets: {
'kiro-rs': {
...defaults.flows.kiro.targets['kiro-rs'],
...getTargetValue(
nested,
(state) => state.flows?.kiro?.targets?.['kiro-rs']
),
baseUrl: String(
input?.kiroRsUrl
?? input?.kiroRsBaseUrl
?? nested?.flows?.kiro?.targets?.['kiro-rs']?.baseUrl
?? defaults.flows.kiro.targets['kiro-rs'].baseUrl
).trim() || defaults.flows.kiro.targets['kiro-rs'].baseUrl,
apiKey: String(
input?.kiroRsKey
?? input?.kiroRsApiKey
?? nested?.flows?.kiro?.targets?.['kiro-rs']?.apiKey
?? defaults.flows.kiro.targets['kiro-rs'].apiKey
),
},
},
autoRun: {
stepExecutionRange: normalizeStepExecutionRangeEntry(
stepExecutionRangeByFlow.kiro
?? nested?.flows?.kiro?.autoRun?.stepExecutionRange
?? {},
defaults.flows.kiro.autoRun.stepExecutionRange
),
},
},
},
flows: {},
};
getCanonicalFlowIds().forEach((flowId) => {
normalized.flows[flowId] = normalizeFlowSettings(flowId, {
...input,
activeFlowId,
}, nested, defaults);
});
if (normalized.flows.openai) {
normalized.flows.openai = normalizeOpenAiSettings(input, nested, defaults, normalized.flows.openai);
}
if (normalized.flows.kiro) {
normalized.flows.kiro = normalizeKiroSettings(input, defaults, normalized.flows.kiro);
}
return normalized;
}
function mergeSettingsState(baseValue = {}, patchValue = {}) {
@@ -425,24 +547,8 @@
activeFlowId: patchValue?.activeFlowId ?? baseSettingsState.activeFlowId,
});
function mergeRecursive(baseNode, patchNode) {
if (Array.isArray(patchNode)) {
return patchNode.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchNode)) {
return patchNode === undefined ? cloneValue(baseNode) : patchNode;
}
const next = {
...cloneValue(isPlainObject(baseNode) ? baseNode : {}),
};
Object.entries(patchNode).forEach(([key, value]) => {
next[key] = mergeRecursive(baseNode?.[key], value);
});
return next;
}
return normalizeSettingsState({
settingsState: mergeRecursive(baseSettingsState, patchSettingsState),
settingsState: mergePlainObjects(baseSettingsState, patchSettingsState),
});
}
@@ -459,7 +565,7 @@
return normalizeTargetId(
normalizedFlowId,
flowSettings?.selectedTargetId,
normalizedFlowId === 'kiro' ? defaultKiroTargetId : defaultOpenAiTargetId
getDefaultTargetId(normalizedFlowId)
);
}
@@ -476,16 +582,16 @@
function buildStepExecutionRangeByFlow(settingsState = {}) {
const normalizedState = normalizeSettingsState(settingsState);
return {
openai: normalizeStepExecutionRangeEntry(
normalizedState?.flows?.openai?.autoRun?.stepExecutionRange,
buildDefaultSettingsState().flows.openai.autoRun.stepExecutionRange
),
kiro: normalizeStepExecutionRangeEntry(
normalizedState?.flows?.kiro?.autoRun?.stepExecutionRange,
buildDefaultSettingsState().flows.kiro.autoRun.stepExecutionRange
),
};
const defaults = buildDefaultSettingsState();
return Object.fromEntries(
Object.entries(normalizedState.flows || {}).map(([flowId, flowSettings]) => [
flowId,
normalizeStepExecutionRangeEntry(
flowSettings?.autoRun?.stepExecutionRange,
defaults.flows?.[flowId]?.autoRun?.stepExecutionRange || getDefaultStepExecutionRange(flowId)
),
])
);
}
function buildSettingsView(settingsState = {}, baseInput = {}) {
@@ -493,38 +599,38 @@
const next = {
...(isPlainObject(baseInput) ? cloneValue(baseInput) : {}),
};
const openaiState = normalizedState.flows.openai;
const kiroState = normalizedState.flows.kiro;
const openaiState = normalizedState.flows.openai || buildDefaultFlowSettings('openai');
const kiroState = normalizedState.flows.kiro || buildDefaultFlowSettings('kiro');
next.activeFlowId = normalizedState.activeFlowId;
next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId);
next.vpsUrl = openaiState.targets.cpa.vpsUrl;
next.vpsPassword = openaiState.targets.cpa.vpsPassword;
next.localCpaStep9Mode = openaiState.targets.cpa.localCpaStep9Mode;
next.sub2apiUrl = openaiState.targets.sub2api.sub2apiUrl;
next.sub2apiEmail = openaiState.targets.sub2api.sub2apiEmail;
next.sub2apiPassword = openaiState.targets.sub2api.sub2apiPassword;
next.sub2apiGroupName = openaiState.targets.sub2api.sub2apiGroupName;
next.sub2apiGroupNames = cloneValue(openaiState.targets.sub2api.sub2apiGroupNames);
next.sub2apiAccountPriority = openaiState.targets.sub2api.sub2apiAccountPriority;
next.sub2apiDefaultProxyName = openaiState.targets.sub2api.sub2apiDefaultProxyName;
next.codex2apiUrl = openaiState.targets.codex2api.codex2apiUrl;
next.codex2apiAdminKey = openaiState.targets.codex2api.codex2apiAdminKey;
next.vpsUrl = openaiState.targets.cpa?.vpsUrl || '';
next.vpsPassword = openaiState.targets.cpa?.vpsPassword || '';
next.localCpaStep9Mode = openaiState.targets.cpa?.localCpaStep9Mode || 'submit';
next.sub2apiUrl = openaiState.targets.sub2api?.sub2apiUrl || '';
next.sub2apiEmail = openaiState.targets.sub2api?.sub2apiEmail || '';
next.sub2apiPassword = openaiState.targets.sub2api?.sub2apiPassword || '';
next.sub2apiGroupName = openaiState.targets.sub2api?.sub2apiGroupName || 'codex';
next.sub2apiGroupNames = cloneValue(openaiState.targets.sub2api?.sub2apiGroupNames || ['codex', 'openai-plus']);
next.sub2apiAccountPriority = openaiState.targets.sub2api?.sub2apiAccountPriority || 1;
next.sub2apiDefaultProxyName = openaiState.targets.sub2api?.sub2apiDefaultProxyName || '';
next.codex2apiUrl = openaiState.targets.codex2api?.codex2apiUrl || '';
next.codex2apiAdminKey = openaiState.targets.codex2api?.codex2apiAdminKey || '';
next.customPassword = normalizedState.services.account.customPassword;
next.signupMethod = openaiState.signup.signupMethod;
next.phoneVerificationEnabled = openaiState.signup.phoneVerificationEnabled;
next.phoneSignupReloginAfterBindEmailEnabled = openaiState.signup.phoneSignupReloginAfterBindEmailEnabled;
next.plusModeEnabled = openaiState.plus.plusModeEnabled;
next.plusPaymentMethod = openaiState.plus.plusPaymentMethod;
next.plusAccountAccessStrategy = openaiState.plus.plusAccountAccessStrategy;
next.hostedCheckoutVerificationUrl = openaiState.plus.hostedCheckoutVerificationUrl;
next.hostedCheckoutPhoneNumber = openaiState.plus.hostedCheckoutPhoneNumber;
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus.plusHostedCheckoutOauthDelaySeconds;
next.signupMethod = openaiState.signup?.signupMethod || 'email';
next.phoneVerificationEnabled = Boolean(openaiState.signup?.phoneVerificationEnabled);
next.phoneSignupReloginAfterBindEmailEnabled = Boolean(openaiState.signup?.phoneSignupReloginAfterBindEmailEnabled);
next.plusModeEnabled = Boolean(openaiState.plus?.plusModeEnabled);
next.plusPaymentMethod = openaiState.plus?.plusPaymentMethod || 'paypal-hosted';
next.plusAccountAccessStrategy = openaiState.plus?.plusAccountAccessStrategy || 'oauth';
next.hostedCheckoutVerificationUrl = openaiState.plus?.hostedCheckoutVerificationUrl || '';
next.hostedCheckoutPhoneNumber = openaiState.plus?.hostedCheckoutPhoneNumber || '';
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus?.plusHostedCheckoutOauthDelaySeconds ?? 3;
next.mailProvider = normalizedState.services.email.provider;
next.ipProxyEnabled = normalizedState.services.proxy.enabled;
next.ipProxyService = normalizedState.services.proxy.provider;
next.ipProxyMode = normalizedState.services.proxy.mode;
next.kiroRsUrl = kiroState.targets['kiro-rs'].baseUrl;
next.kiroRsKey = kiroState.targets['kiro-rs'].apiKey;
next.kiroRsUrl = kiroState.targets['kiro-rs']?.baseUrl || '';
next.kiroRsKey = kiroState.targets['kiro-rs']?.apiKey || '';
next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState);
next.settingsSchemaVersion = normalizedState.schemaVersion;
next.settingsState = cloneValue(normalizedState);
+691
View File
@@ -0,0 +1,691 @@
(function attachBackgroundGrokRegisterRunner(root, factory) {
root.MultiPageBackgroundGrokRegisterRunner = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokRegisterRunnerModule() {
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;
const GROK_POST_PROFILE_CF_WAIT_MS = 20 * 1000;
const GROK_PRE_SSO_EXTRACT_WAIT_MS = 10 * 1000;
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
const GROK_COOKIE_CLEAR_DOMAINS = Object.freeze([
'x.ai',
'accounts.x.ai',
'grok.com',
]);
function cleanString(value = '') {
return String(value ?? '').trim();
}
function getErrorMessage(error) {
return error instanceof Error ? error.message : cleanString(error) || '未知错误';
}
function createGeneratedPassword() {
const alphabet = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*';
let output = '';
for (let index = 0; index < 18; index += 1) {
output += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return `${output}aA1!`;
}
function createGrokRegisterRunner(deps = {}) {
const {
addLog = async () => {},
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
completeNodeFromBackground,
ensureContentScriptReadyOnTab = null,
generatePassword = null,
generateRandomName = null,
getState = async () => ({}),
getTabId = async () => null,
isTabAlive = async () => false,
pollFlowVerificationCode = null,
registerTab = async () => {},
resolveSignupEmailForFlow = null,
reuseOrCreateTab = async () => null,
sendToContentScriptResilient = null,
setPasswordState = async () => {},
setState = async () => {},
sleepWithStop = async (ms) => {
await new Promise((resolve) => setTimeout(resolve, ms));
},
throwIfStopped = () => {},
waitForTabStableComplete = null,
GROK_REGISTER_INJECT_FILES = null,
markCurrentRegistrationAccountUsed = null,
} = deps;
if (typeof completeNodeFromBackground !== 'function') {
throw new Error('Grok register runner requires completeNodeFromBackground.');
}
async function log(message, level = 'info', nodeId = '') {
await addLog(message, level, nodeId ? { nodeId } : {});
}
async function activateTab(tabId) {
if (!Number.isInteger(tabId) || !chrome?.tabs?.update) {
return;
}
await chrome.tabs.update(tabId, { active: true });
}
async function getExecutionState(state = {}) {
if (state && typeof state === 'object' && !Array.isArray(state) && Object.keys(state).length) {
return state;
}
return getState();
}
async function persistState(patch = {}) {
await setState(patch);
return patch;
}
function buildGrokRuntimePatch(patch = {}) {
return {
runtimeState: {
flowState: {
grok: patch,
},
},
};
}
async function completeNode(nodeId, patch = {}) {
await persistState(patch);
await completeNodeFromBackground(nodeId, patch);
return patch;
}
async function isUsableTabId(tabId) {
if (!Number.isInteger(tabId)) {
return false;
}
if (typeof isTabAlive === 'function' && await isTabAlive(GROK_REGISTER_PAGE_SOURCE_ID)) {
return true;
}
if (chrome?.tabs?.get) {
const tab = await chrome.tabs.get(tabId).catch(() => null);
return Boolean(tab?.id === tabId);
}
return true;
}
async function ensureGrokRegisterTab(state = {}, options = {}) {
const existingTabId = Number(
state?.grokRegisterTabId
|| state?.runtimeState?.flowState?.grok?.session?.registerTabId
|| state?.tabRegistry?.[GROK_REGISTER_PAGE_SOURCE_ID]?.tabId
|| 0
);
if (Number.isInteger(existingTabId) && existingTabId > 0 && await isUsableTabId(existingTabId)) {
await registerTab(GROK_REGISTER_PAGE_SOURCE_ID, existingTabId);
return existingTabId;
}
const tabId = await getTabId(GROK_REGISTER_PAGE_SOURCE_ID);
if (Number.isInteger(tabId) && await isUsableTabId(tabId)) {
await registerTab(GROK_REGISTER_PAGE_SOURCE_ID, tabId);
return tabId;
}
if (!options.openIfMissing) {
throw new Error(options.missingMessage || '缺少 Grok 注册页,请先执行步骤 1。');
}
const openedTabId = await reuseOrCreateTab(GROK_REGISTER_PAGE_SOURCE_ID, GROK_SIGNUP_URL, {
inject: Array.isArray(GROK_REGISTER_INJECT_FILES) ? GROK_REGISTER_INJECT_FILES : null,
injectSource: GROK_REGISTER_PAGE_SOURCE_ID,
});
if (!Number.isInteger(openedTabId)) {
throw new Error('无法打开 Grok 注册页。');
}
await registerTab(GROK_REGISTER_PAGE_SOURCE_ID, openedTabId);
return openedTabId;
}
async function ensureContentReady(tabId, options = {}) {
if (!Number.isInteger(tabId)) {
throw new Error('缺少 Grok 注册页标签页,无法连接内容脚本。');
}
if (typeof waitForTabStableComplete === 'function') {
await waitForTabStableComplete(tabId, {
timeoutMs: options.timeoutMs || DEFAULT_GROK_PAGE_TIMEOUT_MS,
retryDelayMs: 300,
stableMs: Number(options.stableMs) || 1200,
initialDelayMs: Number(options.initialDelayMs) || 120,
});
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(GROK_REGISTER_PAGE_SOURCE_ID, tabId, {
inject: Array.isArray(GROK_REGISTER_INJECT_FILES) ? GROK_REGISTER_INJECT_FILES : null,
injectSource: GROK_REGISTER_PAGE_SOURCE_ID,
timeoutMs: options.timeoutMs || DEFAULT_GROK_PAGE_TIMEOUT_MS,
retryDelayMs: 700,
logMessage: options.logMessage || 'Grok 注册页内容脚本未就绪,正在等待页面恢复...',
});
}
}
async function sendGrokCommand(nodeId, payload = {}, options = {}) {
if (typeof sendToContentScriptResilient !== 'function') {
throw new Error('Grok 注册页通信能力不可用。');
}
const result = await sendToContentScriptResilient(GROK_REGISTER_PAGE_SOURCE_ID, {
type: 'EXECUTE_NODE',
nodeId,
step: options.step || 0,
source: 'background',
payload,
}, {
timeoutMs: options.timeoutMs || 45000,
retryDelayMs: 700,
logMessage: options.logMessage || '',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
function shouldClearGrokCookie(cookie = {}) {
const domain = cleanString(cookie.domain).replace(/^\.+/, '').toLowerCase();
return GROK_COOKIE_CLEAR_DOMAINS.some((target) => (
domain === target || domain.endsWith(`.${target}`)
));
}
function buildCookieRemovalUrl(cookie = {}) {
const host = cleanString(cookie.domain).replace(/^\.+/, '').toLowerCase();
const path = cleanString(cookie.path) || '/';
return `https://${host}${path.startsWith('/') ? path : `/${path}`}`;
}
async function clearGrokCookiesBeforeStep1() {
if (!chrome?.cookies?.getAll || !chrome.cookies?.remove) {
await log('步骤 1:当前浏览器不支持 cookies API,跳过 Grok Cookie 清理。', 'warn', 'grok-open-signup-page');
return;
}
const stores = chrome.cookies.getAllCookieStores
? await chrome.cookies.getAllCookieStores()
: [{ id: undefined }];
let removedCount = 0;
const seen = new Set();
for (const store of stores) {
const storeId = store?.id;
const cookies = await chrome.cookies.getAll(storeId ? { storeId } : {}).catch(() => []);
for (const cookie of cookies || []) {
if (!shouldClearGrokCookie(cookie)) {
continue;
}
const key = [
cookie.storeId || storeId || '',
cookie.domain || '',
cookie.path || '',
cookie.name || '',
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
].join('|');
if (seen.has(key)) {
continue;
}
seen.add(key);
try {
const details = {
url: buildCookieRemovalUrl(cookie),
name: cookie.name,
};
if (cookie.storeId) {
details.storeId = cookie.storeId;
}
if (cookie.partitionKey) {
details.partitionKey = cookie.partitionKey;
}
const removed = await chrome.cookies.remove(details);
if (removed) {
removedCount += 1;
}
} catch (error) {
console.warn('[MultiPage:grok-register] remove cookie failed', {
domain: cookie?.domain,
name: cookie?.name,
message: getErrorMessage(error),
});
}
}
}
await log(`步骤 1:已清理 Grok/xAI Cookie ${removedCount} 个。`, removedCount ? 'ok' : 'info', 'grok-open-signup-page');
}
function resolveProfile(currentState = {}) {
const firstFromState = cleanString(currentState.grokFirstName);
const lastFromState = cleanString(currentState.grokLastName);
if (firstFromState && lastFromState) {
return {
firstName: firstFromState,
lastName: lastFromState,
};
}
const generated = typeof generateRandomName === 'function' ? generateRandomName() : null;
const fullName = cleanString(generated?.fullName || generated?.name || 'Alex Morgan');
const parts = fullName.split(/\s+/).filter(Boolean);
return {
firstName: firstFromState || cleanString(generated?.firstName || parts[0] || 'Alex'),
lastName: lastFromState || cleanString(generated?.lastName || parts.slice(1).join(' ') || 'Morgan'),
};
}
function resolvePassword(currentState = {}) {
return cleanString(currentState.grokPassword || currentState.customPassword || currentState.password)
|| (typeof generatePassword === 'function' ? generatePassword() : createGeneratedPassword());
}
function normalizeGrokVerificationCode(value = '') {
return cleanString(value).replace(/[^A-Za-z0-9]/g, '');
}
async function readSsoCookieFromChrome() {
if (!chrome?.cookies?.get) {
return '';
}
const candidates = [
{ url: 'https://x.ai/', name: 'sso' },
{ url: 'https://grok.com/', name: 'sso' },
{ url: 'https://accounts.x.ai/', name: 'sso' },
];
for (const details of candidates) {
const cookie = await chrome.cookies.get(details).catch(() => null);
const value = cleanString(cookie?.value);
if (value) {
return value;
}
}
return '';
}
async function executeGrokOpenSignupPage(state = {}) {
const nodeId = cleanString(state?.nodeId) || 'grok-open-signup-page';
const currentState = await getExecutionState(state);
try {
await clearGrokCookiesBeforeStep1();
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: true });
await activateTab(tabId);
await persistState({
grokRegisterTabId: tabId,
grokSignupUrl: GROK_SIGNUP_URL,
...buildGrokRuntimePatch({
session: {
registerTabId: tabId,
startedAt: Date.now(),
pageUrl: GROK_SIGNUP_URL,
lastError: '',
},
}),
});
await ensureContentReady(tabId);
const result = await sendGrokCommand(nodeId, {}, {
step: 1,
timeoutMs: DEFAULT_GROK_PAGE_TIMEOUT_MS,
logMessage: '步骤 1:正在打开 Grok 邮箱注册入口...',
});
await log('步骤 1:已打开 Grok 邮箱注册页。', 'ok', nodeId);
await completeNode(nodeId, {
grokRegisterTabId: tabId,
grokPageState: result.state || 'email_signup_ready',
grokPageUrl: result.url || GROK_SIGNUP_URL,
...buildGrokRuntimePatch({
session: {
registerTabId: tabId,
startedAt: Date.now(),
pageState: result.state || 'email_signup_ready',
pageUrl: result.url || GROK_SIGNUP_URL,
lastError: '',
},
register: {
status: 'signup_page_opened',
},
}),
});
} catch (error) {
const message = getErrorMessage(error);
await persistState(buildGrokRuntimePatch({
session: {
lastError: message,
},
}));
await log(`步骤 1${message}`, 'error', nodeId);
throw error;
}
}
async function executeGrokSubmitEmail(state = {}) {
const nodeId = cleanString(state?.nodeId) || 'grok-submit-email';
const currentState = await getExecutionState(state);
try {
if (typeof resolveSignupEmailForFlow !== 'function') {
throw new Error('Grok 邮箱步骤缺少公共邮箱解析能力,无法继续执行。');
}
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
await activateTab(tabId);
await ensureContentReady(tabId);
const resolvedEmail = await resolveSignupEmailForFlow(currentState, {
preserveAccountIdentity: true,
});
const email = cleanString(resolvedEmail).toLowerCase();
if (!email) {
throw new Error('Grok 注册邮箱为空,无法继续执行。');
}
const requestedAt = Date.now();
await persistState({
grokEmail: email,
email,
accountIdentifierType: 'email',
accountIdentifier: email,
...buildGrokRuntimePatch({
register: {
email,
verificationRequestedAt: requestedAt,
status: 'email_submitting',
},
}),
});
const result = await sendGrokCommand(nodeId, { email }, {
step: 2,
logMessage: '步骤 2:正在提交 Grok 注册邮箱...',
});
await log(`步骤 2:已提交 Grok 注册邮箱 ${email}`, 'ok', nodeId);
await completeNode(nodeId, {
grokEmail: email,
grokVerificationRequestedAt: requestedAt,
grokPageState: result.state || '',
grokPageUrl: result.url || '',
email,
accountIdentifierType: 'email',
accountIdentifier: email,
...buildGrokRuntimePatch({
session: {
pageState: result.state || '',
pageUrl: result.url || '',
lastError: '',
},
register: {
email,
verificationRequestedAt: requestedAt,
status: 'verification_requested',
},
}),
});
} catch (error) {
const message = getErrorMessage(error);
await persistState(buildGrokRuntimePatch({
session: {
lastError: message,
},
register: {
status: 'error',
},
}));
await log(`步骤 2${message}`, 'error', nodeId);
throw error;
}
}
async function executeGrokSubmitVerificationCode(state = {}) {
const nodeId = cleanString(state?.nodeId) || 'grok-submit-verification-code';
const currentState = await getExecutionState(state);
try {
if (typeof pollFlowVerificationCode !== 'function') {
throw new Error('Grok 验证码步骤缺少共享邮件轮询能力,无法继续执行。');
}
const requestedAt = Math.max(
0,
Number(
currentState.grokVerificationRequestedAt
|| currentState.runtimeState?.flowState?.grok?.register?.verificationRequestedAt
) || Date.now()
);
const filterAfterTimestamp = cleanString(currentState?.mailProvider).toLowerCase() === '2925'
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: requestedAt;
const email = cleanString(
currentState.grokEmail
|| currentState.runtimeState?.flowState?.grok?.register?.email
|| currentState.email
).toLowerCase();
const pollResult = await pollFlowVerificationCode({
actionLabel: 'Grok 验证码',
filterAfterTimestamp,
flowId: 'grok',
logStep: 3,
logStepKey: nodeId,
nodeId,
notFoundMessage: '步骤 3:邮箱轮询结束,但未获取到 xAI 验证码。',
state: {
...currentState,
activeFlowId: 'grok',
flowId: 'grok',
visibleStep: 3,
grokEmail: email,
email,
},
step: 3,
});
const code = normalizeGrokVerificationCode(pollResult?.code);
if (!code) {
throw new Error('未能获取到 xAI 邮箱验证码。');
}
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
await activateTab(tabId);
await ensureContentReady(tabId);
const result = await sendGrokCommand(nodeId, { code }, {
step: 3,
logMessage: '步骤 3:正在填写 xAI 邮箱验证码...',
});
await log(`步骤 3:已提交 xAI 邮箱验证码,当前页面状态:${result.state || 'unknown'}`, 'ok', nodeId);
await completeNode(nodeId, {
grokVerificationCode: code,
grokVerificationRawCode: cleanString(pollResult?.code),
grokVerificationMessageId: cleanString(pollResult?.messageId || pollResult?.mailId),
grokPageState: result.state || '',
grokPageUrl: result.url || '',
...buildGrokRuntimePatch({
session: {
pageState: result.state || '',
pageUrl: result.url || '',
lastError: '',
},
register: {
verificationCode: code,
status: 'verified',
},
}),
});
} catch (error) {
const message = getErrorMessage(error);
await persistState(buildGrokRuntimePatch({
session: {
lastError: message,
},
register: {
status: 'error',
},
}));
await log(`步骤 3${message}`, 'error', nodeId);
throw error;
}
}
async function executeGrokSubmitProfile(state = {}) {
const nodeId = cleanString(state?.nodeId) || 'grok-submit-profile';
const currentState = await getExecutionState(state);
try {
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
const profile = resolveProfile(currentState);
const password = resolvePassword(currentState);
await persistState({
grokFirstName: profile.firstName,
grokLastName: profile.lastName,
grokPassword: password,
...buildGrokRuntimePatch({
register: {
firstName: profile.firstName,
lastName: profile.lastName,
password,
status: 'profile_submitting',
},
}),
});
if (typeof setPasswordState === 'function') {
await setPasswordState(password);
}
await activateTab(tabId);
await ensureContentReady(tabId);
const result = await sendGrokCommand(nodeId, {
firstName: profile.firstName,
lastName: profile.lastName,
password,
}, {
step: 4,
logMessage: '步骤 4:正在填写 xAI 注册资料...',
});
await log(`步骤 4:已提交 Grok 注册资料,等待 ${Math.floor(GROK_POST_PROFILE_CF_WAIT_MS / 1000)} 秒完成注册验证...`, 'info', nodeId);
await sleepWithStop(GROK_POST_PROFILE_CF_WAIT_MS);
await ensureContentReady(tabId, { timeoutMs: DEFAULT_GROK_PAGE_TIMEOUT_MS });
await log('步骤 4:已提交 Grok 注册资料并完成等待。', 'ok', nodeId);
await completeNode(nodeId, {
grokFirstName: profile.firstName,
grokLastName: profile.lastName,
grokPassword: password,
grokPageState: result.state || 'profile_submitted',
grokPageUrl: result.url || '',
...buildGrokRuntimePatch({
session: {
pageState: result.state || 'profile_submitted',
pageUrl: result.url || '',
lastError: '',
},
register: {
firstName: profile.firstName,
lastName: profile.lastName,
password,
status: 'profile_submitted',
},
}),
});
} catch (error) {
const message = getErrorMessage(error);
await persistState(buildGrokRuntimePatch({
session: {
lastError: message,
},
register: {
status: 'error',
},
}));
await log(`步骤 4${message}`, 'error', nodeId);
throw error;
}
}
async function executeGrokExtractSsoCookie(state = {}) {
const nodeId = cleanString(state?.nodeId) || 'grok-extract-sso-cookie';
const currentState = await getExecutionState(state);
try {
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
await activateTab(tabId);
await log(`步骤 5:等待 ${Math.floor(GROK_PRE_SSO_EXTRACT_WAIT_MS / 1000)} 秒后提取 Grok SSO...`, 'info', nodeId);
await sleepWithStop(GROK_PRE_SSO_EXTRACT_WAIT_MS);
let ssoCookie = await readSsoCookieFromChrome();
if (!ssoCookie) {
await ensureContentReady(tabId);
const result = await sendGrokCommand(nodeId, {}, {
step: 5,
logMessage: '步骤 5:正在从 Grok 注册页读取 sso Cookie...',
});
ssoCookie = cleanString(result?.ssoCookie);
}
if (!ssoCookie) {
throw new Error('未找到 x.ai/grok sso Cookie。');
}
const latestState = await getState();
const existingSsoCookies = Array.isArray(latestState?.grokSsoCookies)
? latestState.grokSsoCookies
.map((entry) => cleanString(entry))
.filter(Boolean)
: [];
const nextSsoCookies = existingSsoCookies.includes(ssoCookie)
? existingSsoCookies
: [...existingSsoCookies, ssoCookie];
const completedAt = Date.now();
const completionPatch = {
grokSsoCookie: ssoCookie,
grokSsoCookies: nextSsoCookies,
grokSsoExtractedAt: completedAt,
grokCompletedAt: completedAt,
grokRegisterStatus: 'completed',
...buildGrokRuntimePatch({
register: {
status: 'completed',
completedAt,
},
sso: {
currentCookie: ssoCookie,
cookies: nextSsoCookies,
extractedAt: completedAt,
},
session: {
lastError: '',
},
}),
};
if (typeof markCurrentRegistrationAccountUsed === 'function') {
await markCurrentRegistrationAccountUsed({
...currentState,
...completionPatch,
}, {
logPrefix: 'Grok 注册成功',
level: 'ok',
});
}
await log('步骤 5:已提取 Grok SSO Cookie。', 'ok', nodeId);
await completeNode(nodeId, completionPatch);
} catch (error) {
const message = getErrorMessage(error);
await persistState(buildGrokRuntimePatch({
session: {
lastError: message,
},
register: {
status: 'error',
},
}));
await log(`步骤 5${message}`, 'error', nodeId);
throw error;
}
}
return {
executeGrokExtractSsoCookie,
executeGrokOpenSignupPage,
executeGrokSubmitEmail,
executeGrokSubmitProfile,
executeGrokSubmitVerificationCode,
};
}
return {
DEFAULT_GROK_PAGE_TIMEOUT_MS,
GROK_COOKIE_CLEAR_DOMAINS,
GROK_POST_PROFILE_CF_WAIT_MS,
GROK_PRE_SSO_EXTRACT_WAIT_MS,
GROK_REGISTER_PAGE_SOURCE_ID,
GROK_SIGNUP_URL,
createGrokRegisterRunner,
};
});
+374
View File
@@ -0,0 +1,374 @@
(function attachBackgroundGrokState(root, factory) {
root.MultiPageBackgroundGrokState = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokStateModule() {
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function cloneValue(value) {
if (Array.isArray(value)) {
return value.map((entry) => cloneValue(entry));
}
if (isPlainObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
);
}
return value;
}
function deepMerge(baseValue, patchValue) {
if (Array.isArray(patchValue)) {
return patchValue.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchValue)) {
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
}
const baseObject = isPlainObject(baseValue) ? baseValue : {};
const next = {
...cloneValue(baseObject),
};
Object.entries(patchValue).forEach(([key, value]) => {
next[key] = deepMerge(baseObject[key], value);
});
return next;
}
function cleanString(value = '') {
return String(value ?? '').trim();
}
function normalizeInteger(value, fallback = 0) {
const numeric = Math.floor(Number(value));
return Number.isInteger(numeric) ? numeric : fallback;
}
function normalizeNullableInteger(value, fallback = null) {
if (value === null || value === undefined || value === '') {
return fallback;
}
const numeric = Math.floor(Number(value));
return Number.isInteger(numeric) ? numeric : fallback;
}
function normalizeSsoCookies(values = []) {
if (!Array.isArray(values)) {
return [];
}
return Array.from(new Set(
values
.map((entry) => cleanString(entry))
.filter(Boolean)
));
}
function buildDefaultRuntimeState() {
return {
session: {
registerTabId: null,
startedAt: 0,
pageState: '',
pageUrl: '',
lastError: '',
},
register: {
email: '',
firstName: '',
lastName: '',
password: '',
verificationRequestedAt: 0,
verificationCode: '',
status: '',
completedAt: 0,
},
sso: {
currentCookie: '',
cookies: [],
extractedAt: 0,
},
};
}
function normalizeRuntimeState(runtimeState = {}) {
const merged = deepMerge(buildDefaultRuntimeState(), runtimeState);
return {
session: {
registerTabId: normalizeNullableInteger(merged.session?.registerTabId),
startedAt: Math.max(0, normalizeInteger(merged.session?.startedAt)),
pageState: cleanString(merged.session?.pageState),
pageUrl: cleanString(merged.session?.pageUrl),
lastError: cleanString(merged.session?.lastError),
},
register: {
email: cleanString(merged.register?.email).toLowerCase(),
firstName: cleanString(merged.register?.firstName),
lastName: cleanString(merged.register?.lastName),
password: cleanString(merged.register?.password),
verificationRequestedAt: Math.max(0, normalizeInteger(merged.register?.verificationRequestedAt)),
verificationCode: cleanString(merged.register?.verificationCode),
status: cleanString(merged.register?.status),
completedAt: Math.max(0, normalizeInteger(merged.register?.completedAt)),
},
sso: {
currentCookie: cleanString(merged.sso?.currentCookie || merged.ssoCookie),
cookies: normalizeSsoCookies(merged.sso?.cookies || merged.ssoCookies),
extractedAt: Math.max(0, normalizeInteger(merged.sso?.extractedAt)),
},
};
}
function buildCanonicalRuntimeStatePatch(state = {}, runtimeState = {}) {
const normalizedRuntimeState = normalizeRuntimeState(runtimeState);
const baseRuntimeState = isPlainObject(state?.runtimeState)
? cloneValue(state.runtimeState)
: {};
const baseFlowState = isPlainObject(baseRuntimeState.flowState)
? cloneValue(baseRuntimeState.flowState)
: {};
return {
...baseRuntimeState,
flowState: {
...baseFlowState,
grok: normalizedRuntimeState,
},
};
}
function projectRuntimeFields(runtimeState = {}) {
const normalizedRuntimeState = normalizeRuntimeState(runtimeState);
return {
grokRegisterTabId: normalizedRuntimeState.session.registerTabId,
grokPageState: normalizedRuntimeState.session.pageState,
grokPageUrl: normalizedRuntimeState.session.pageUrl,
grokEmail: normalizedRuntimeState.register.email,
grokFirstName: normalizedRuntimeState.register.firstName,
grokLastName: normalizedRuntimeState.register.lastName,
grokPassword: normalizedRuntimeState.register.password,
grokVerificationRequestedAt: normalizedRuntimeState.register.verificationRequestedAt,
grokVerificationCode: normalizedRuntimeState.register.verificationCode,
grokRegisterStatus: normalizedRuntimeState.register.status,
grokCompletedAt: normalizedRuntimeState.register.completedAt,
grokSsoCookie: normalizedRuntimeState.sso.currentCookie,
grokSsoCookies: normalizedRuntimeState.sso.cookies,
grokSsoExtractedAt: normalizedRuntimeState.sso.extractedAt,
};
}
function ensureRuntimeState(state = {}) {
const runtimeFlowState = isPlainObject(state?.runtimeState?.flowState)
? state.runtimeState.flowState
: {};
const legacyFlowState = isPlainObject(state?.flowState?.grok)
? state.flowState.grok
: {};
const flatRuntime = {
session: {
registerTabId: state.grokRegisterTabId,
pageState: state.grokPageState,
pageUrl: state.grokPageUrl || state.grokPostVerificationUrl,
},
register: {
email: state.grokEmail || state.email,
firstName: state.grokFirstName,
lastName: state.grokLastName,
password: state.grokPassword,
verificationRequestedAt: state.grokVerificationRequestedAt,
verificationCode: state.grokVerificationCode,
status: state.grokRegisterStatus,
completedAt: state.grokCompletedAt,
},
sso: {
currentCookie: state.grokSsoCookie,
cookies: state.grokSsoCookies,
extractedAt: state.grokSsoExtractedAt || state.grokCompletedAt,
},
};
return normalizeRuntimeState(deepMerge(deepMerge(runtimeFlowState.grok || {}, legacyFlowState), flatRuntime));
}
function buildStateView(state = {}) {
const runtimeState = ensureRuntimeState(state);
const canonicalRuntimeState = buildCanonicalRuntimeStatePatch(state, runtimeState);
return {
...state,
...projectRuntimeFields(runtimeState),
runtimeState: canonicalRuntimeState,
flowState: {
...(isPlainObject(state?.flowState) ? state.flowState : {}),
grok: runtimeState,
},
flows: {
...(isPlainObject(state?.flows) ? state.flows : {}),
grok: runtimeState,
},
};
}
function buildRuntimeStatePatch(currentState = {}, patch = {}) {
if (!isPlainObject(patch)) {
return {};
}
const nextRuntimeState = normalizeRuntimeState(
deepMerge(ensureRuntimeState(currentState), patch)
);
return {
...projectRuntimeFields(nextRuntimeState),
runtimeState: buildCanonicalRuntimeStatePatch(currentState, nextRuntimeState),
};
}
function buildSessionStatePatch(currentState = {}, updates = {}) {
const runtimePatch = isPlainObject(updates?.runtimeState?.flowState?.grok)
? updates.runtimeState.flowState.grok
: (isPlainObject(updates?.flowState?.grok) ? updates.flowState.grok : null);
if (!runtimePatch) {
return {};
}
return buildRuntimeStatePatch(currentState, runtimePatch);
}
function buildRuntimeResetPatch(currentState = {}, patch = {}) {
return buildRuntimeStatePatch(currentState, patch);
}
function buildStartRegisterResetPatch(currentState = {}) {
return buildRuntimeStatePatch(currentState, buildDefaultRuntimeState());
}
function buildRegisterOnlyResetPatch(currentState = {}, registerPatch = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
return buildRuntimeStatePatch(currentState, {
...currentRuntimeState,
session: {
...currentRuntimeState.session,
pageState: '',
pageUrl: '',
lastError: '',
},
register: {
...buildDefaultRuntimeState().register,
email: currentRuntimeState.register?.email || '',
...registerPatch,
},
});
}
function buildSsoResetPatch(currentState = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
return buildRuntimeStatePatch(currentState, {
...currentRuntimeState,
sso: buildDefaultRuntimeState().sso,
});
}
function buildDownstreamResetPatch(stepKey = '', currentState = {}) {
switch (cleanString(stepKey)) {
case 'grok-open-signup-page':
return {
flowStartTime: null,
...buildStartRegisterResetPatch(currentState),
};
case 'grok-submit-email':
return buildRegisterOnlyResetPatch(currentState, {
email: '',
});
case 'grok-submit-verification-code':
return buildRegisterOnlyResetPatch(currentState, {});
case 'grok-submit-profile':
case 'grok-extract-sso-cookie':
return buildSsoResetPatch(currentState);
default:
return {};
}
}
function applyNodeCompletionPayload(currentState = {}, payload = {}) {
const runtimePatch = isPlainObject(payload?.runtimeState?.flowState?.grok)
? payload.runtimeState.flowState.grok
: (isPlainObject(payload?.flowState?.grok) ? payload.flowState.grok : null);
if (runtimePatch) {
return buildRuntimeStatePatch(currentState, runtimePatch);
}
const patch = {};
if (Object.prototype.hasOwnProperty.call(payload, 'grokRegisterTabId')) {
patch.session = { ...(patch.session || {}), registerTabId: payload.grokRegisterTabId };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokPageState')) {
patch.session = { ...(patch.session || {}), pageState: payload.grokPageState };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokPageUrl') || Object.prototype.hasOwnProperty.call(payload, 'grokSignupUrl') || Object.prototype.hasOwnProperty.call(payload, 'grokPostVerificationUrl')) {
patch.session = {
...(patch.session || {}),
pageUrl: payload.grokPageUrl || payload.grokPostVerificationUrl || payload.grokSignupUrl || '',
};
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokEmail') || Object.prototype.hasOwnProperty.call(payload, 'email')) {
patch.register = {
...(patch.register || {}),
email: payload.grokEmail || payload.email || '',
};
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokFirstName')) {
patch.register = { ...(patch.register || {}), firstName: payload.grokFirstName };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokLastName')) {
patch.register = { ...(patch.register || {}), lastName: payload.grokLastName };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokPassword')) {
patch.register = { ...(patch.register || {}), password: payload.grokPassword };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokVerificationRequestedAt')) {
patch.register = {
...(patch.register || {}),
verificationRequestedAt: payload.grokVerificationRequestedAt,
};
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokVerificationCode')) {
patch.register = { ...(patch.register || {}), verificationCode: payload.grokVerificationCode };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokRegisterStatus')) {
patch.register = { ...(patch.register || {}), status: payload.grokRegisterStatus };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokCompletedAt')) {
patch.register = { ...(patch.register || {}), completedAt: payload.grokCompletedAt };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokSsoCookie')) {
patch.sso = { ...(patch.sso || {}), currentCookie: payload.grokSsoCookie };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokSsoCookies')) {
patch.sso = { ...(patch.sso || {}), cookies: payload.grokSsoCookies };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokSsoExtractedAt') || Object.prototype.hasOwnProperty.call(payload, 'grokCompletedAt')) {
patch.sso = {
...(patch.sso || {}),
extractedAt: payload.grokSsoExtractedAt || payload.grokCompletedAt || 0,
};
}
if (!Object.keys(patch).length) {
return {};
}
return buildRuntimeStatePatch(currentState, patch);
}
function buildFreshKeepState(currentState = {}) {
const currentRuntimeState = ensureRuntimeState(currentState);
const nextRuntimeState = buildDefaultRuntimeState();
nextRuntimeState.sso = currentRuntimeState.sso || buildDefaultRuntimeState().sso;
return buildRuntimeStatePatch(currentState, nextRuntimeState);
}
return {
applyNodeCompletionPayload,
buildDefaultRuntimeState,
buildDownstreamResetPatch,
buildFreshKeepState,
buildRuntimeStatePatch,
buildSessionStatePatch,
buildStateView,
ensureRuntimeState,
normalizeSsoCookies,
projectRuntimeFields,
};
});
+317
View File
@@ -0,0 +1,317 @@
console.log('[MultiPage:grok-register-page] Content script loaded on', location.href);
const GROK_REGISTER_PAGE_LISTENER_SENTINEL = 'data-multipage-grok-register-page-listener';
const GROK_SIGNUP_URL = 'https://accounts.x.ai/sign-up?redirect=grok-com';
const GROK_EMAIL_SIGNUP_TEXT_PATTERN = /使用邮箱注册|sign\s*up\s*with\s*email|continue\s*with\s*email|email/i;
const GROK_CONTINUE_TEXT_PATTERN = /continue|next|sign\s*up|submit|verify|继续|下一步|注册|提交|验证/i;
const GROK_PROFILE_TEXT_PATTERN = /given\s*name|family\s*name|first\s*name|last\s*name|password|名字|姓氏|密码/i;
const GROK_PROFILE_SUBMIT_PRE_CLICK_DELAY_MS = 2000;
function isVisibleGrokElement(element) {
if (!element || !(element instanceof Element)) return false;
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function getGrokElementText(element) {
if (!element) return '';
return String(
element.innerText
|| element.textContent
|| element.getAttribute?.('aria-label')
|| element.getAttribute?.('title')
|| ''
)
.replace(/\s+/g, ' ')
.trim();
}
function queryVisibleGrokElement(selector) {
return Array.from(document.querySelectorAll(selector)).find(isVisibleGrokElement) || null;
}
function findGrokClickableByText(pattern) {
const selectors = 'button, a, [role="button"], input[type="button"], input[type="submit"]';
return Array.from(document.querySelectorAll(selectors)).find((element) => {
if (!isVisibleGrokElement(element)) return false;
const text = element instanceof HTMLInputElement ? element.value : getGrokElementText(element);
return pattern.test(text);
}) || null;
}
function simulateGrokClick(element) {
throwIfStopped();
if (!element) {
throw new Error('无法点击空元素。');
}
const rect = element.getBoundingClientRect();
const clientX = Math.max(0, Math.floor(rect.left + Math.min(rect.width - 1, Math.max(1, rect.width / 2))));
const clientY = Math.max(0, Math.floor(rect.top + Math.min(rect.height - 1, Math.max(1, rect.height / 2))));
const eventOptions = {
bubbles: true,
cancelable: true,
view: window,
clientX,
clientY,
screenX: window.screenX + clientX,
screenY: window.screenY + clientY,
};
element.dispatchEvent(new MouseEvent('mouseover', eventOptions));
element.dispatchEvent(new MouseEvent('mousedown', eventOptions));
element.dispatchEvent(new MouseEvent('mouseup', eventOptions));
if (typeof element.click === 'function') {
element.click();
return;
}
element.dispatchEvent(new MouseEvent('click', eventOptions));
}
async function waitForGrok(predicate, options = {}) {
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 30000);
const intervalMs = Math.max(100, Number(options.intervalMs) || 250);
const deadline = Date.now() + timeoutMs;
let lastValue = null;
while (Date.now() <= deadline) {
throwIfStopped();
lastValue = predicate();
if (lastValue) return lastValue;
await sleep(intervalMs);
}
return lastValue;
}
function findGrokEmailInput() {
return queryVisibleGrokElement([
'input[type="email"]',
'input[name="email" i]',
'input[autocomplete="email"]',
'input[placeholder*="email" i]',
'input[inputmode="email"]',
].join(', '));
}
function findGrokOtpInputs() {
const inputs = Array.from(document.querySelectorAll([
'input[autocomplete="one-time-code"]',
'input[inputmode="numeric"]',
'input[name*="otp" i]',
'input[name*="code" i]',
'input[aria-label*="code" i]',
'input[placeholder*="code" i]',
].join(', '))).filter(isVisibleGrokElement);
if (inputs.length) return inputs;
const oneCharInputs = Array.from(document.querySelectorAll('input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"])'))
.filter((input) => isVisibleGrokElement(input) && Number(input.maxLength || 0) === 1);
return oneCharInputs.length >= 4 ? oneCharInputs : [];
}
function findGrokProfileInput(names) {
const selectors = names.flatMap((name) => [
`input[name="${name}" i]`,
`input[id="${name}" i]`,
`input[autocomplete="${name}" i]`,
`input[placeholder*="${name}" i]`,
`input[aria-label*="${name}" i]`,
]).join(', ');
return queryVisibleGrokElement(selectors);
}
function findGrokPasswordInputs() {
return Array.from(document.querySelectorAll([
'input[type="password"]',
'input[name*="password" i]',
'input[autocomplete="new-password"]',
'input[placeholder*="password" i]',
'input[aria-label*="password" i]',
].join(', '))).filter(isVisibleGrokElement);
}
function findGrokSubmitButton(contextPattern = GROK_CONTINUE_TEXT_PATTERN) {
return findGrokClickableByText(contextPattern)
|| Array.from(document.querySelectorAll('button:not([disabled]), [role="button"]')).filter(isVisibleGrokElement).at(-1)
|| null;
}
function getGrokPageState() {
const pageText = document.body?.innerText || '';
if (/grok|xai|x\.ai/i.test(location.hostname) && /(?:^|;\s*)sso=/.test(document.cookie || '')) return 'signed_in';
if (findGrokProfileInput(['givenName', 'firstName']) || findGrokPasswordInputs().length || GROK_PROFILE_TEXT_PATTERN.test(pageText)) return 'profile_entry';
if (findGrokOtpInputs().length) return 'verification_code_entry';
if (findGrokEmailInput()) return 'email_entry';
return 'unknown';
}
async function openGrokSignupPage() {
if (!/accounts\.x\.ai$/i.test(location.hostname) || !/\/sign-up/i.test(location.pathname)) {
location.href = GROK_SIGNUP_URL;
return { submitted: true, state: 'navigating', url: location.href };
}
const emailButton = await waitForGrok(() => (
findGrokClickableByText(GROK_EMAIL_SIGNUP_TEXT_PATTERN) || findGrokEmailInput()
), { timeoutMs: 30000 });
if (!emailButton) throw new Error('未找到 x.ai 邮箱注册入口。');
if (!(emailButton instanceof HTMLInputElement)) {
simulateGrokClick(emailButton);
await sleep(500);
}
return { submitted: true, state: getGrokPageState(), url: location.href };
}
function getGrokEmailErrorText() {
const text = String(document.body?.innerText || '').trim();
const patterns = [
/Your email domain[^\n]+has been rejected[^\n]*/i,
/Please use a different email address[^\n]*/i,
/邮箱域名[^\n]*(?:被拒绝|不可用|不支持)[^\n]*/i,
/请使用其他邮箱[^\n]*/i,
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match?.[0]) return match[0].trim();
}
return '';
}
async function submitGrokEmail(payload = {}) {
const email = String(payload.email || '').trim();
if (!email) throw new Error('缺少 Grok 注册邮箱。');
const input = await waitForGrok(findGrokEmailInput, { timeoutMs: 45000 });
if (!input) throw new Error('未找到 x.ai 邮箱输入框。');
fillInput(input, email);
await sleep(200);
const button = findGrokSubmitButton();
if (!button) throw new Error('未找到 x.ai 邮箱提交按钮。');
simulateGrokClick(button);
await sleep(1200);
const errorText = getGrokEmailErrorText();
if (errorText) {
throw new Error(errorText);
}
return { submitted: true, state: getGrokPageState(), url: location.href };
}
function getGrokVerificationErrorText() {
const text = String(document.body?.innerText || '').trim();
const patterns = [
/(?:verification|confirmation)?\s*code\s*(?:is\s*)?(?:invalid|incorrect|expired)[^\n]*/i,
/invalid\s*(?:verification|confirmation)?\s*code[^\n]*/i,
/验证码[^\n]*(?:错误|无效|过期)[^\n]*/i,
/代码[^\n]*(?:错误|无效|过期)[^\n]*/i,
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match?.[0]) return match[0].trim();
}
return '';
}
async function submitGrokVerificationCode(payload = {}) {
const normalizedCode = String(payload.code || '').replace(/[^A-Za-z0-9]/g, '').trim();
if (!normalizedCode) throw new Error('缺少 xAI 验证码。');
const inputs = await waitForGrok(() => findGrokOtpInputs(), { timeoutMs: 45000 });
if (!inputs?.length) throw new Error('未找到 xAI 验证码输入框。');
if (inputs.length === 1) {
fillInput(inputs[0], normalizedCode);
} else {
normalizedCode.split('').forEach((char, index) => {
if (inputs[index]) fillInput(inputs[index], char);
});
}
await sleep(200);
const button = findGrokSubmitButton();
if (button) simulateGrokClick(button);
const settledState = await waitForGrok(() => {
const errorText = getGrokVerificationErrorText();
if (errorText) return { state: 'verification_error', error: errorText };
const state = getGrokPageState();
return state && state !== 'verification_code_entry' ? { state } : null;
}, { timeoutMs: 20000, intervalMs: 500 });
const finalState = settledState?.state || getGrokPageState();
if (settledState?.error) {
throw new Error(settledState.error);
}
if (finalState === 'email_entry') {
throw new Error('x.ai 验证码提交后回到邮箱注册页,可能是验证码无效、会话过期或注册风控重置。');
}
if (!['profile_entry', 'signed_in'].includes(finalState)) {
throw new Error(`x.ai 验证码提交后进入未知页面状态:${finalState || 'unknown'}`);
}
return { submitted: true, state: finalState, url: location.href };
}
async function submitGrokProfile(payload = {}) {
const firstName = String(payload.firstName || '').trim();
const lastName = String(payload.lastName || '').trim();
const password = String(payload.password || '');
if (!firstName || !lastName || !password) throw new Error('缺少 Grok 注册资料。');
const ready = await waitForGrok(() => {
const firstInput = findGrokProfileInput(['givenName', 'firstName', 'given-name']);
const lastInput = findGrokProfileInput(['familyName', 'lastName', 'family-name']);
const passwordInputs = findGrokPasswordInputs();
return firstInput && lastInput && passwordInputs.length ? { firstInput, lastInput, passwordInputs } : null;
}, { timeoutMs: 45000 });
if (!ready) throw new Error('未找到 x.ai 资料或密码表单。');
fillInput(ready.firstInput, firstName);
fillInput(ready.lastInput, lastName);
ready.passwordInputs.forEach((input) => fillInput(input, password));
await sleep(GROK_PROFILE_SUBMIT_PRE_CLICK_DELAY_MS);
const button = findGrokSubmitButton();
if (!button) throw new Error('未找到 x.ai 资料提交按钮。');
simulateGrokClick(button);
return { submitted: true, state: 'profile_submitted', url: location.href };
}
async function extractGrokSsoCookie() {
const match = String(document.cookie || '').match(/(?:^|;\s*)sso=([^;]+)/);
return {
submitted: true,
state: match ? 'sso_cookie_found' : getGrokPageState(),
ssoCookie: match ? decodeURIComponent(match[1]) : '',
url: location.href,
};
}
async function executeGrokCommand(command, payload = {}) {
switch (command) {
case 'grok-open-signup-page':
return openGrokSignupPage(payload);
case 'grok-submit-email':
return submitGrokEmail(payload);
case 'grok-submit-verification-code':
return submitGrokVerificationCode(payload);
case 'grok-submit-profile':
return submitGrokProfile(payload);
case 'grok-extract-sso-cookie':
return extractGrokSsoCookie(payload);
case 'GET_PAGE_STATE':
return { state: getGrokPageState(), url: location.href };
default:
throw new Error(`未知 Grok 注册命令:${command}`);
}
}
if (!document.documentElement.hasAttribute(GROK_REGISTER_PAGE_LISTENER_SENTINEL)) {
document.documentElement.setAttribute(GROK_REGISTER_PAGE_LISTENER_SENTINEL, '1');
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type !== 'EXECUTE_NODE' && message?.type !== 'GET_PAGE_STATE') return false;
resetStopState();
const command = message.command || message.nodeId || message.type;
executeGrokCommand(command, message.payload || {})
.then((result) => sendResponse({ ok: true, ...result }))
.catch((error) => {
if (isStopError(error)) {
sendResponse({ stopped: true, error: error.message });
return;
}
sendResponse({ ok: false, error: error?.message || String(error) });
});
return true;
});
}
window.__MULTIPAGE_GROK_REGISTER_PAGE__ = {
executeGrokCommand,
getGrokPageState,
};
+141
View File
@@ -0,0 +1,141 @@
(function attachMultiPageGrokFlowDefinition(root, factory) {
root.MultiPageGrokFlowDefinition = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createMultiPageGrokFlowDefinition() {
function freezeDeep(entry) {
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
return entry;
}
Object.getOwnPropertyNames(entry).forEach((key) => {
freezeDeep(entry[key]);
});
return Object.freeze(entry);
}
const VALUE = freezeDeep({
id: 'grok',
label: 'Grok / xAI',
services: [
'account',
'email',
'proxy',
],
capabilities: {
supportsEmailSignup: true,
supportsPhoneSignup: false,
supportsPhoneVerificationSettings: false,
supportsPlusMode: false,
supportsContributionMode: false,
supportsAccountContribution: false,
supportsOpenAiOAuthContribution: false,
contributionAdapterIds: [],
supportedTargetIds: ['webchat2api'],
supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
canSwitchFlow: true,
stepDefinitionMode: 'grok',
targetSelectorLabel: '来源',
},
baseGroups: ['grok-runtime-status'],
targets: {
webchat2api: {
id: 'webchat2api',
label: 'webchat2api',
groups: [
'grok-target-webchat2api',
],
},
},
publicationTargets: {},
runtimeSources: {
'grok-register-page': {
flowId: 'grok',
kind: 'flow-page',
label: 'Grok 注册页',
readyPolicy: 'top-frame-only',
family: 'grok-register-page-family',
driverId: 'flows/grok/content/register-page',
cleanupScopes: [],
detectionMatchers: [
{
hostnames: [
'accounts.x.ai',
'x.ai',
'grok.com',
],
hostnameEndsWith: [
'.x.ai',
'.grok.com',
],
matchMode: 'any',
},
],
familyMatchers: [
{
hostnames: [
'accounts.x.ai',
'x.ai',
'grok.com',
],
hostnameEndsWith: [
'.x.ai',
'.grok.com',
],
matchMode: 'any',
},
],
},
},
driverDefinitions: {
'flows/grok/content/register-page': {
sourceId: 'grok-register-page',
commands: [
'grok-open-signup-page',
'grok-submit-email',
'grok-submit-verification-code',
'grok-submit-profile',
'grok-extract-sso-cookie',
],
},
'flows/grok/background/register-runner': {
sourceId: 'grok-register-page',
commands: [
'grok-open-signup-page',
'grok-submit-email',
'grok-submit-verification-code',
'grok-submit-profile',
'grok-extract-sso-cookie',
],
},
},
defaultTargetId: 'webchat2api',
settingsDefaults: {
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 5,
},
},
},
settingsGroups: {
'grok-target-webchat2api': {
id: 'grok-target-webchat2api',
label: 'webchat2api',
rowIds: [
'row-grok-sso-settings',
],
},
'grok-runtime-status': {
id: 'grok-runtime-status',
label: 'Grok 运行态',
rowIds: [
'row-grok-register-status',
'row-grok-sso-status',
],
},
},
sourceAliases: {},
});
return VALUE;
});
+157
View File
@@ -0,0 +1,157 @@
(function attachGrokMailRules(root, factory) {
root.MultiPageGrokMailRules = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createGrokMailRulesModule(root) {
const grokStateApi = root.MultiPageBackgroundGrokState || null;
const SUBMIT_VERIFICATION_CODE_RULE_ID = 'grok-submit-verification-code';
const SUBMIT_VERIFICATION_CODE_NODE_ID = 'grok-submit-verification-code';
const GROK_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
flags: 'gi',
}),
Object.freeze({
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '(?:验证码|代码|确认码)[:\\s为]+(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '(?<!#)\\b(\\d{6})\\b',
flags: 'g',
}),
]);
const GROK_SENDER_FILTERS = Object.freeze([
'x.ai',
'xai',
'grok',
]);
const GROK_SUBJECT_FILTERS = Object.freeze([
'xai',
'x.ai',
'grok',
'verification',
'confirmation',
'code',
'验证码',
'确认码',
]);
const GROK_REQUIRED_KEYWORDS = Object.freeze([
'xai',
'x.ai',
'grok',
'verification',
'confirmation',
'code',
'验证码',
'确认码',
]);
function cleanString(value = '') {
return String(value ?? '').trim();
}
function readGrokRuntime(state = {}) {
if (typeof grokStateApi?.ensureRuntimeState === 'function') {
return grokStateApi.ensureRuntimeState(state);
}
return state?.runtimeState?.flowState?.grok || state?.flowState?.grok || {};
}
function buildTargetEmailHints(targetEmail = '') {
const normalizedTarget = cleanString(targetEmail).toLowerCase();
return normalizedTarget ? [normalizedTarget] : [];
}
function getVisibleStep(state = {}) {
const explicitStep = Number(state?.visibleStep || state?.step);
return Number.isInteger(explicitStep) && explicitStep > 0 ? explicitStep : 3;
}
function isMail2925Provider(state = {}) {
return cleanString(state?.mailProvider).toLowerCase() === '2925';
}
function shouldMatchMail2925TargetEmail(state = {}) {
return isMail2925Provider(state)
&& cleanString(state?.mail2925Mode).toLowerCase() === 'receive';
}
function createGrokMailRules(deps = {}) {
const {
LUCKMAIL_PROVIDER = 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
} = deps;
function getRuleDefinition(_input, state = {}) {
const runtimeState = readGrokRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.grokEmail || state?.email).toLowerCase();
const normalizedProvider = cleanString(state?.mailProvider).toLowerCase();
const mail2925Provider = isMail2925Provider(state);
const luckmailProvider = normalizedProvider === cleanString(LUCKMAIL_PROVIDER).toLowerCase();
return {
flowId: 'grok',
ruleId: SUBMIT_VERIFICATION_CODE_RULE_ID,
nodeId: SUBMIT_VERIFICATION_CODE_NODE_ID,
step: getVisibleStep(state),
artifactType: 'code',
codePatterns: GROK_VERIFICATION_CODE_PATTERNS,
filterAfterTimestamp: 0,
requiredKeywords: GROK_REQUIRED_KEYWORDS,
senderFilters: GROK_SENDER_FILTERS,
subjectFilters: GROK_SUBJECT_FILTERS,
targetEmail,
targetEmailHints: buildTargetEmailHints(targetEmail),
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
maxAttempts: luckmailProvider
? 3
: (mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5),
intervalMs: luckmailProvider
? 15000
: (mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 5000),
};
}
function getRuleDefinitionForNode(nodeId, state = {}) {
const normalizedNodeId = cleanString(nodeId);
if (normalizedNodeId && normalizedNodeId !== SUBMIT_VERIFICATION_CODE_NODE_ID) {
throw new Error(`Grok 邮件规则不支持节点:${normalizedNodeId}`);
}
return getRuleDefinition({ nodeId: SUBMIT_VERIFICATION_CODE_NODE_ID }, state);
}
function buildVerificationPollPayload(input, state = {}, overrides = {}) {
return {
...getRuleDefinition(input, state),
...(overrides || {}),
};
}
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
return {
...getRuleDefinitionForNode(nodeId, state),
...(overrides || {}),
};
}
return {
buildVerificationPollPayload,
buildVerificationPollPayloadForNode,
getRuleDefinition,
getRuleDefinitionForNode,
};
}
return {
GROK_REQUIRED_KEYWORDS,
GROK_SENDER_FILTERS,
GROK_SUBJECT_FILTERS,
GROK_VERIFICATION_CODE_PATTERNS,
SUBMIT_VERIFICATION_CODE_NODE_ID,
SUBMIT_VERIFICATION_CODE_RULE_ID,
createGrokMailRules,
};
});
+98
View File
@@ -0,0 +1,98 @@
(function attachMultiPageGrokWorkflow(root, factory) {
root.MultiPageGrokWorkflow = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createMultiPageGrokWorkflow() {
function freezeDeep(entry) {
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
return entry;
}
Object.getOwnPropertyNames(entry).forEach((key) => {
freezeDeep(entry[key]);
});
return Object.freeze(entry);
}
const STEP_VARIANTS = freezeDeep({
default: [
{
id: 1,
order: 10,
key: 'grok-open-signup-page',
title: '打开 Grok 注册页',
sourceId: 'grok-register-page',
driverId: 'flows/grok/background/register-runner',
command: 'grok-open-signup-page',
flowId: 'grok',
},
{
id: 2,
order: 20,
key: 'grok-submit-email',
title: '获取邮箱并继续',
sourceId: 'grok-register-page',
driverId: 'flows/grok/background/register-runner',
command: 'grok-submit-email',
flowId: 'grok',
},
{
id: 3,
order: 30,
key: 'grok-submit-verification-code',
title: '获取验证码并继续',
sourceId: 'grok-register-page',
driverId: 'flows/grok/background/register-runner',
command: 'grok-submit-verification-code',
mailRuleId: 'grok-submit-verification-code',
flowId: 'grok',
},
{
id: 4,
order: 40,
key: 'grok-submit-profile',
title: '填写资料并继续',
sourceId: 'grok-register-page',
driverId: 'flows/grok/background/register-runner',
command: 'grok-submit-profile',
flowId: 'grok',
},
{
id: 5,
order: 50,
key: 'grok-extract-sso-cookie',
title: '提取 SSO Cookie',
sourceId: 'grok-register-page',
driverId: 'flows/grok/background/register-runner',
command: 'grok-extract-sso-cookie',
flowId: 'grok',
},
],
});
function getVariantStepDefinitions(variantKey = 'default') {
return Array.isArray(STEP_VARIANTS[variantKey]) ? STEP_VARIANTS[variantKey] : STEP_VARIANTS.default;
}
function getModeStepDefinitions() {
return getVariantStepDefinitions('default');
}
function getAllSteps() {
return getVariantStepDefinitions('default');
}
function getPlusPaymentStepTitle() {
return '';
}
function resolveStepTitle(step = {}) {
return step?.title || '';
}
return {
flowId: 'grok',
getAllSteps,
getModeStepDefinitions,
getPlusPaymentStepTitle,
getVariantStepDefinitions,
resolveStepTitle,
};
});
+10 -2
View File
@@ -12,6 +12,10 @@
id: 'kiro',
path: 'flows/kiro/',
},
grok: {
id: 'grok',
path: 'flows/grok/',
},
});
function normalizeFlowId(value = '') {
@@ -32,10 +36,14 @@
...baseEntry,
definition: normalized === 'openai'
? (rootScope.MultiPageOpenAiFlowDefinition || null)
: (rootScope.MultiPageKiroFlowDefinition || null),
: (normalized === 'kiro'
? (rootScope.MultiPageKiroFlowDefinition || null)
: (rootScope.MultiPageGrokFlowDefinition || null)),
workflow: normalized === 'openai'
? (rootScope.MultiPageOpenAiWorkflow || null)
: (rootScope.MultiPageKiroWorkflow || null),
: (normalized === 'kiro'
? (rootScope.MultiPageKiroWorkflow || null)
: (rootScope.MultiPageGrokWorkflow || null)),
};
}
+21 -207
View File
@@ -14,43 +14,6 @@
'https://app.kiro.dev/*',
'https://kiro.dev/*',
]);
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
@@ -375,32 +338,17 @@
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
completeNodeFromBackground,
ensureContentScriptReadyOnTab = null,
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getMailConfig = null,
getState = async () => ({}),
getTabId = async () => null,
HOTMAIL_PROVIDER = 'hotmail-api',
LUCKMAIL_PROVIDER = 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
YYDS_MAIL_PROVIDER = 'yyds-mail',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false,
maybeSubmitFlowContribution = async () => null,
KIRO_REGISTER_INJECT_FILES = null,
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
pollFlowVerificationCode = null,
registerTab = async () => {},
reuseOrCreateTab = async () => null,
sendToContentScriptResilient = null,
sendToMailContentScriptResilient = null,
setState = async () => {},
sleepWithStop = async (ms) => {
await new Promise((resolve) => setTimeout(resolve, ms));
@@ -658,44 +606,6 @@
return password;
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
if (!mail?.source) {
return;
}
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
if (Number.isInteger(tabId)) {
await activateTab(tabId);
}
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function collectKiroWebSessionTabs(currentState = {}) {
const runtimeState = readKiroRuntime(currentState);
const candidates = [];
@@ -901,131 +811,35 @@
throw new Error(`Kiro Web 登录态尚未建立。请在自动打开的 Kiro 账号页登录后,从步骤 7 继续。${detail}`);
}
function buildDesktopOtpPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
const runtimeState = readKiroRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
const targetEmailHints = targetEmail ? [targetEmail] : [];
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 3
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 15000
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
return {
flowId: 'kiro',
step,
targetEmail,
targetEmailHints,
filterAfterTimestamp,
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
mail2925MatchTargetEmail: isMail2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
maxAttempts,
intervalMs,
};
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + 25000);
}
async function pollDesktopOtpCode(step, state = {}, nodeId = '') {
if (typeof getMailConfig !== 'function') {
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱配置能力,无法继续执行。');
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
if (typeof pollFlowVerificationCode !== 'function') {
throw new Error('Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。');
}
const runtimeState = readKiroRuntime(state);
const requestedAt = Math.max(0, Number(runtimeState.desktopAuth?.otpRequestedAt) || Date.now());
const filterAfterTimestamp = mail.provider === '2925'
const mailProvider = cleanString(state?.mailProvider).toLowerCase();
const filterAfterTimestamp = mailProvider === '2925'
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: requestedAt;
const pollPayload = buildDesktopOtpPollPayload(step, state, mail, filterAfterTimestamp);
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
await ensureIcloudMailSession({
state,
step,
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
});
}
if (mail.provider === HOTMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollHotmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'LuckMail'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollLuckmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloudflare Temp Email'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollCloudflareTempEmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUD_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloud Mail'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollCloudMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === YYDS_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'YYDS Mail'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollYydsMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`, 'info', nodeId);
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
});
} else {
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', nodeId);
await focusOrOpenMailTab(mail);
}
if (typeof sendToMailContentScriptResilient !== 'function') {
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
}
const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: pollPayload,
return pollFlowVerificationCode({
actionLabel: '桌面授权验证码',
filterAfterTimestamp,
flowId: 'kiro',
logStep: step,
logStepKey: 'kiro-complete-desktop-authorize',
missingCapabilityMessage: 'Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。',
nodeId: 'kiro-complete-desktop-authorize',
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`,
state: {
...state,
activeFlowId: 'kiro',
flowId: 'kiro',
visibleStep: step,
},
{
timeoutMs: responseTimeoutMs,
responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: step,
logStepKey: 'kiro-complete-desktop-authorize',
}
);
if (result?.error) {
throw new Error(result.error);
}
if (!result?.code) {
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`);
}
return result;
step,
});
}
async function executeKiroStartDesktopAuthorize(state = {}) {
+21 -210
View File
@@ -32,42 +32,6 @@
'https://profile.aws.amazon.com',
]);
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_REGISTER_PAGE_STATES = Object.freeze([
'kiro_signin_page',
'email_entry',
@@ -387,32 +351,17 @@
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
completeNodeFromBackground,
ensureContentScriptReadyOnTab = null,
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
generatePassword = null,
generateRandomName = null,
getMailConfig = null,
getState = async () => ({}),
getTabId = async () => null,
HOTMAIL_PROVIDER = 'hotmail-api',
LUCKMAIL_PROVIDER = 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
YYDS_MAIL_PROVIDER = 'yyds-mail',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false,
KIRO_REGISTER_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
pollFlowVerificationCode = null,
registerTab = async () => {},
resolveSignupEmailForFlow = null,
reuseOrCreateTab = async () => null,
sendToContentScriptResilient = null,
sendToMailContentScriptResilient = null,
setPasswordState = async () => {},
setState = async () => {},
sleepWithStop = async (ms) => {
@@ -836,174 +785,36 @@
};
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
if (!mail?.source) {
return;
}
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
if (Number.isInteger(tabId)) {
await activateTab(tabId);
}
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
function buildKiroVerificationPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
const runtimeState = readKiroRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
const targetEmailHints = targetEmail ? [targetEmail] : [];
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 3
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 15000
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
return {
flowId: 'kiro',
step,
targetEmail,
targetEmailHints,
filterAfterTimestamp,
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
mail2925MatchTargetEmail: isMail2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
maxAttempts,
intervalMs,
};
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + 25000);
}
async function pollKiroVerificationCode(step, state = {}, nodeId = '') {
if (typeof getMailConfig !== 'function') {
throw new Error('Kiro 验证码步骤缺少邮箱配置能力,无法继续执行。');
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
if (typeof pollFlowVerificationCode !== 'function') {
throw new Error('Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。');
}
const runtimeState = readKiroRuntime(state);
const recordedRequestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || 0);
const requestedAt = recordedRequestedAt || Math.max(0, Date.now() - MAIL_2925_FILTER_LOOKBACK_MS);
const filterAfterTimestamp = mail.provider === '2925'
const mailProvider = cleanString(state?.mailProvider).toLowerCase();
const filterAfterTimestamp = mailProvider === '2925'
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: requestedAt;
const pollPayload = buildKiroVerificationPollPayload(step, state, mail, filterAfterTimestamp);
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
await ensureIcloudMailSession({
state,
step,
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
});
}
throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询验证码...`, 'info', nodeId);
return pollHotmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'LuckMail'} 轮询验证码...`, 'info', nodeId);
return pollLuckmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloudflare Temp Email'} 轮询验证码...`, 'info', nodeId);
return pollCloudflareTempEmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUD_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloud Mail'} 轮询验证码...`, 'info', nodeId);
return pollCloudMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === YYDS_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'YYDS Mail'} 轮询验证码...`, 'info', nodeId);
return pollYydsMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`, 'info', nodeId);
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
});
} else {
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', nodeId);
await focusOrOpenMailTab(mail);
}
if (typeof sendToMailContentScriptResilient !== 'function') {
throw new Error('Kiro 验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
}
const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: pollPayload,
return pollFlowVerificationCode({
actionLabel: 'Kiro 验证码',
filterAfterTimestamp,
flowId: 'kiro',
logStep: step,
logStepKey: 'kiro-submit-verification-code',
missingCapabilityMessage: 'Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。',
nodeId: 'kiro-submit-verification-code',
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到 Kiro 验证码。`,
state: {
...state,
activeFlowId: 'kiro',
flowId: 'kiro',
visibleStep: step,
},
{
timeoutMs: responseTimeoutMs,
responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: step,
logStepKey: 'kiro-submit-verification-code',
}
);
if (result?.error) {
throw new Error(result.error);
}
if (!result?.code) {
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
}
return result;
step,
});
}
async function executeKiroOpenRegisterPage(state = {}) {
+162
View File
@@ -0,0 +1,162 @@
(function attachKiroMailRules(root, factory) {
root.MultiPageKiroMailRules = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createKiroMailRulesModule(root) {
const kiroStateApi = root.MultiPageBackgroundKiroState || null;
const SUBMIT_VERIFICATION_CODE_RULE_ID = 'kiro-submit-verification-code';
const DESKTOP_AUTHORIZE_CODE_RULE_ID = 'kiro-complete-desktop-authorize';
const SUBMIT_VERIFICATION_CODE_NODE_ID = 'kiro-submit-verification-code';
const DESKTOP_AUTHORIZE_NODE_ID = 'kiro-complete-desktop-authorize';
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
function cleanString(value = '') {
return String(value ?? '').trim();
}
function readKiroRuntime(state = {}) {
if (typeof kiroStateApi?.ensureRuntimeState === 'function') {
return kiroStateApi.ensureRuntimeState(state);
}
return state?.runtimeState?.flowState?.kiro || state?.flowState?.kiro || {};
}
function buildTargetEmailHints(targetEmail = '') {
const normalizedTarget = cleanString(targetEmail).toLowerCase();
return normalizedTarget ? [normalizedTarget] : [];
}
function resolveNodeId(input) {
const directNodeId = cleanString(input?.nodeId || input);
if (directNodeId === DESKTOP_AUTHORIZE_NODE_ID) {
return DESKTOP_AUTHORIZE_NODE_ID;
}
return SUBMIT_VERIFICATION_CODE_NODE_ID;
}
function getVisibleStepForNode(nodeId, state = {}) {
const explicitStep = Number(state?.visibleStep || state?.step);
if (Number.isInteger(explicitStep) && explicitStep > 0) {
return explicitStep;
}
return nodeId === DESKTOP_AUTHORIZE_NODE_ID ? 8 : 4;
}
function isMail2925Provider(state = {}) {
return cleanString(state?.mailProvider).toLowerCase() === '2925';
}
function shouldMatchMail2925TargetEmail(state = {}) {
return isMail2925Provider(state)
&& cleanString(state?.mail2925Mode).toLowerCase() === 'receive';
}
function createKiroMailRules(deps = {}) {
const {
LUCKMAIL_PROVIDER = 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
} = deps;
function getRuleDefinition(input, state = {}) {
const nodeId = resolveNodeId(input);
const normalizedStep = getVisibleStepForNode(nodeId, state);
const runtimeState = readKiroRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
const normalizedProvider = cleanString(state?.mailProvider).toLowerCase();
const mail2925Provider = isMail2925Provider(state);
const luckmailProvider = normalizedProvider === cleanString(LUCKMAIL_PROVIDER).toLowerCase();
return {
flowId: 'kiro',
ruleId: nodeId === DESKTOP_AUTHORIZE_NODE_ID
? DESKTOP_AUTHORIZE_CODE_RULE_ID
: SUBMIT_VERIFICATION_CODE_RULE_ID,
nodeId,
step: normalizedStep,
artifactType: 'code',
codePatterns: KIRO_AWS_VERIFICATION_CODE_PATTERNS,
filterAfterTimestamp: 0,
requiredKeywords: KIRO_AWS_REQUIRED_KEYWORDS,
senderFilters: KIRO_AWS_SENDER_FILTERS,
subjectFilters: KIRO_AWS_SUBJECT_FILTERS,
targetEmail,
targetEmailHints: buildTargetEmailHints(targetEmail),
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
maxAttempts: luckmailProvider
? 3
: (mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5),
intervalMs: luckmailProvider
? 15000
: (mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000),
};
}
function getRuleDefinitionForNode(nodeId, state = {}) {
return getRuleDefinition({ nodeId }, state);
}
function buildVerificationPollPayload(input, state = {}, overrides = {}) {
return {
...getRuleDefinition(input, state),
...(overrides || {}),
};
}
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
return buildVerificationPollPayload({ nodeId }, state, overrides);
}
return {
buildVerificationPollPayload,
buildVerificationPollPayloadForNode,
getRuleDefinition,
getRuleDefinitionForNode,
};
}
return {
DESKTOP_AUTHORIZE_CODE_RULE_ID,
DESKTOP_AUTHORIZE_NODE_ID,
KIRO_AWS_REQUIRED_KEYWORDS,
KIRO_AWS_SENDER_FILTERS,
KIRO_AWS_SUBJECT_FILTERS,
KIRO_AWS_VERIFICATION_CODE_PATTERNS,
SUBMIT_VERIFICATION_CODE_NODE_ID,
SUBMIT_VERIFICATION_CODE_RULE_ID,
createKiroMailRules,
};
});
+5
View File
@@ -51,6 +51,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
@@ -72,6 +73,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
@@ -93,6 +95,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
@@ -111,6 +114,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
@@ -128,6 +132,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
+32 -2
View File
@@ -191,15 +191,44 @@
return Boolean(normalizedAdapterId) && getContributionAdapterIds(flowId).includes(normalizedAdapterId);
}
function assertPublishedFlowsHaveContributionAdapters(flowIds = undefined) {
function hasPublicationTargets(flowId = DEFAULT_FLOW_ID) {
if (typeof flowRegistryApi.getPublicationTargetDefinitions !== 'function') {
return false;
}
const definitions = flowRegistryApi.getPublicationTargetDefinitions(flowId) || {};
return Object.keys(definitions).length > 0;
}
function expectsContributionAdapter(flowId = DEFAULT_FLOW_ID) {
const normalizedFlowId = normalizeFlowId(flowId, DEFAULT_FLOW_ID);
const capabilities = typeof flowRegistryApi.getFlowCapabilities === 'function'
? (flowRegistryApi.getFlowCapabilities(normalizedFlowId) || {})
: {};
return Boolean(
hasPublicationTargets(normalizedFlowId)
|| capabilities.supportsAccountContribution
|| capabilities.supportsContributionMode
|| getContributionAdapterIds(normalizedFlowId).length
);
}
function getPublishedContributionFlowIds(flowIds = undefined) {
const ids = Array.isArray(flowIds)
? flowIds
: (typeof flowRegistryApi.getRegisteredFlowIds === 'function'
? flowRegistryApi.getRegisteredFlowIds()
: Object.keys(FLOW_ADAPTER_IDS));
const missing = ids
return ids
.map((flowId) => normalizeString(flowId).toLowerCase())
.filter(Boolean)
.filter((flowId) => expectsContributionAdapter(flowId));
}
function assertPublishedFlowsHaveContributionAdapters(flowIds = undefined) {
const ids = Array.isArray(flowIds)
? flowIds.map((flowId) => normalizeString(flowId).toLowerCase()).filter(Boolean)
: getPublishedContributionFlowIds();
const missing = ids
.filter((flowId) => getContributionAdapterIds(flowId).length === 0);
if (missing.length) {
throw new Error(`缺少账号贡献适配器:${missing.join(', ')}`);
@@ -217,6 +246,7 @@
getContributionAdapterIds,
getContributionAdapters,
getDefaultContributionAdapterId,
getPublishedContributionFlowIds,
hasContributionAdapter,
normalizeAdapterId,
normalizeFlowId,
+20
View File
@@ -126,6 +126,7 @@
<select id="select-flow" class="data-select" aria-label="Flow 选择">
<option value="openai" selected>Codex / OpenAI</option>
<option value="kiro">Kiro</option>
<option value="grok">Grok</option>
</select>
</div>
<div class="data-row">
@@ -290,6 +291,23 @@
<span class="data-label">上传状态</span>
<span id="display-kiro-upload-status" class="data-value mono">未开始</span>
</div>
<div class="data-row" id="row-grok-register-status" style="display:none;">
<span class="data-label">Grok 注册</span>
<span id="display-grok-register-status" class="data-value mono">未开始</span>
</div>
<div class="data-row" id="row-grok-sso-status" style="display:none;">
<span class="data-label">SSO 状态</span>
<span id="display-grok-sso-status" class="data-value mono">未提取</span>
</div>
<div class="data-row" id="row-grok-sso-settings" style="display:none;">
<span class="data-label">SSO Cookie</span>
<div class="data-inline">
<span id="display-grok-sso-cookie" class="data-value mono">未提取</span>
<button id="btn-copy-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">复制</button>
<button id="btn-export-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">导出</button>
<button id="btn-clear-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button>
</div>
</div>
<div class="data-row module-divider-start" id="row-custom-password">
<span class="data-label">账户密码</span>
<div class="input-with-icon">
@@ -1881,6 +1899,8 @@
<script src="../flows/openai/workflow.js"></script>
<script src="../flows/kiro/index.js"></script>
<script src="../flows/kiro/workflow.js"></script>
<script src="../flows/grok/index.js"></script>
<script src="../flows/grok/workflow.js"></script>
<script src="../flows/index.js"></script>
<script src="../core/flow-kernel/flow-registry.js"></script>
<script src="../shared/contribution-registry.js"></script>
+171 -1
View File
@@ -192,6 +192,15 @@ const rowKiroLoginUrl = document.getElementById('row-kiro-login-url');
const displayKiroLoginUrl = document.getElementById('display-kiro-login-url');
const rowKiroUploadStatus = document.getElementById('row-kiro-upload-status');
const displayKiroUploadStatus = document.getElementById('display-kiro-upload-status');
const rowGrokRegisterStatus = document.getElementById('row-grok-register-status');
const displayGrokRegisterStatus = document.getElementById('display-grok-register-status');
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 displayGrokSsoCookie = document.getElementById('display-grok-sso-cookie');
const btnCopyGrokSso = document.getElementById('btn-copy-grok-sso');
const btnExportGrokSso = document.getElementById('btn-export-grok-sso');
const btnClearGrokSso = document.getElementById('btn-clear-grok-sso');
const rowCustomPassword = document.getElementById('row-custom-password');
const rowPlusMode = document.getElementById('row-plus-mode');
const inputPlusModeEnabled = document.getElementById('input-plus-mode-enabled');
@@ -2661,6 +2670,95 @@ function getKiroRuntimeState(state = {}) {
return {};
}
function getGrokRuntimeState(state = {}) {
const runtimeState = state?.runtimeState?.flowState?.grok;
if (runtimeState && typeof runtimeState === 'object' && !Array.isArray(runtimeState)) {
return runtimeState;
}
const flowState = state?.flowState?.grok;
if (flowState && typeof flowState === 'object' && !Array.isArray(flowState)) {
return flowState;
}
return {};
}
function normalizeGrokSsoCookies(state = latestState) {
const runtimeState = getGrokRuntimeState(state);
const rawCookies = Array.isArray(runtimeState?.sso?.cookies)
? runtimeState.sso.cookies
: (Array.isArray(state?.grokSsoCookies) ? state.grokSsoCookies : []);
const currentCookie = String(runtimeState?.sso?.currentCookie || state?.grokSsoCookie || '').trim();
return Array.from(new Set([
currentCookie,
...rawCookies,
].map((entry) => String(entry || '').trim()).filter(Boolean)));
}
function getGrokRegisterStatusLabel(value = '') {
const normalized = String(value || '').trim().toLowerCase();
switch (normalized) {
case 'signup_page_opened':
return '注册页已打开';
case 'email_submitting':
return '正在提交邮箱';
case 'verification_requested':
return '等待验证码';
case 'verified':
return '验证码已提交';
case 'profile_submitting':
return '正在提交资料';
case 'profile_submitted':
return '资料已提交';
case 'completed':
return '已完成';
case 'error':
return '失败';
default:
return String(value || '').trim() || '未开始';
}
}
function renderGrokRuntimeState(state = latestState) {
const runtimeState = getGrokRuntimeState(state);
const registerStatus = String(
runtimeState?.register?.status
|| state?.grokRegisterStatus
|| ''
).trim();
const cookies = normalizeGrokSsoCookies(state);
const currentCookie = String(
runtimeState?.sso?.currentCookie
|| state?.grokSsoCookie
|| cookies[0]
|| ''
).trim();
const extractedAt = Number(
runtimeState?.sso?.extractedAt
|| state?.grokSsoExtractedAt
|| 0
) || 0;
if (displayGrokRegisterStatus) {
displayGrokRegisterStatus.textContent = getGrokRegisterStatusLabel(registerStatus);
}
if (displayGrokSsoStatus) {
displayGrokSsoStatus.textContent = currentCookie
? `已提取 ${cookies.length || 1}${extractedAt ? `${new Date(extractedAt).toLocaleString()}` : ''}`
: '未提取';
}
if (displayGrokSsoCookie) {
displayGrokSsoCookie.textContent = currentCookie
? `${currentCookie.slice(0, 8)}...${currentCookie.slice(-6)}`
: '未提取';
displayGrokSsoCookie.title = currentCookie ? '已隐藏完整 SSO Cookie,可使用复制或导出' : '';
}
[btnCopyGrokSso, btnExportGrokSso, btnClearGrokSso].forEach((button) => {
if (button) {
button.disabled = cookies.length === 0;
}
});
}
function setKiroRsConnectionTestStatus(message = '') {
const nextText = String(message || '').trim() || '未测试';
kiroRsConnectionTestStatusText = nextText;
@@ -11069,6 +11167,7 @@ function applySettingsState(state) {
).trim();
displayKiroUploadStatus.textContent = getKiroUploadStatusLabel(kiroUploadStatus);
}
renderGrokRuntimeState(state);
const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService);
const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function'
? normalizeIpProxyServiceProfiles(state?.ipProxyServiceProfiles || {}, state || {})
@@ -13944,6 +14043,64 @@ async function copyTextToClipboard(text) {
await navigator.clipboard.writeText(value);
}
btnCopyGrokSso?.addEventListener('click', async () => {
try {
const cookies = normalizeGrokSsoCookies(latestState);
await copyTextToClipboard(cookies.join('\n'));
showToast('Grok SSO Cookie 已复制。', 'success');
} catch (error) {
showToast(error?.message || '复制 Grok SSO Cookie 失败。', 'error');
}
});
btnExportGrokSso?.addEventListener('click', () => {
try {
const cookies = normalizeGrokSsoCookies(latestState);
if (!cookies.length) {
throw new Error('没有可导出的 Grok SSO Cookie。');
}
downloadTextFile(
`${cookies.join('\n')}\n`,
`grok-sso-cookies-${new Date().toISOString().replace(/[:.]/g, '-')}.txt`,
'text/plain;charset=utf-8'
);
showToast('Grok SSO Cookie 已导出。', 'success');
} catch (error) {
showToast(error?.message || '导出 Grok SSO Cookie 失败。', 'error');
}
});
btnClearGrokSso?.addEventListener('click', async () => {
try {
const cookies = normalizeGrokSsoCookies(latestState);
if (!cookies.length) {
showToast('当前没有 Grok SSO Cookie。', 'info');
return;
}
const response = await chrome.runtime.sendMessage({
type: 'CLEAR_GROK_SSO_COOKIES',
source: 'sidepanel',
payload: {},
});
if (response?.error) {
throw new Error(response.error);
}
if (response?.state) {
syncLatestState(response.state);
} else {
syncLatestState({
grokSsoCookie: '',
grokSsoCookies: [],
grokSsoExtractedAt: 0,
});
}
renderGrokRuntimeState(latestState);
showToast('Grok SSO Cookie 已清空。', 'success');
} catch (error) {
showToast(error?.message || '清空 Grok SSO Cookie 失败。', 'error');
}
});
const hotmailManager = window.SidepanelHotmailManager?.createHotmailManager({
state: {
getLatestState: () => latestState,
@@ -14590,7 +14747,9 @@ stepsList?.addEventListener('click', async (event) => {
if (step === gpcCreateStep && !(await ensureGpcApiKeyReadyForStart())) {
return;
}
const shouldPersistSharedPassword = nodeId === 'fill-password' || nodeId === 'kiro-submit-password';
const shouldPersistSharedPassword = nodeId === 'fill-password'
|| nodeId === 'kiro-submit-password'
|| nodeId === 'grok-submit-profile';
if (shouldPersistSharedPassword && inputPassword.value !== (latestState?.customPassword || '')) {
await chrome.runtime.sendMessage({
type: 'SAVE_SETTING',
@@ -17331,6 +17490,17 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
) {
syncPasswordField(latestState || {});
}
if (
message.payload.grokRegisterStatus !== undefined
|| message.payload.grokCompletedAt !== undefined
|| message.payload.grokSsoCookie !== undefined
|| message.payload.grokSsoCookies !== undefined
|| message.payload.grokSsoExtractedAt !== undefined
|| message.payload.runtimeState !== undefined
|| message.payload.flowState !== undefined
) {
renderGrokRuntimeState(latestState);
}
if (message.payload.localCpaStep9Mode !== undefined) {
setLocalCpaStep9Mode(message.payload.localCpaStep9Mode);
}
@@ -0,0 +1,148 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadFlowMailPollingApi() {
const source = fs.readFileSync('background/flow-mail-polling.js', 'utf8');
const globalScope = {};
new Function('self', `${source}; return self;`)(globalScope);
return globalScope.MultiPageBackgroundFlowMailPolling;
}
test('flow mail polling service dispatches API mail providers through shared helper', async () => {
const api = loadFlowMailPollingApi();
const logs = [];
let buildCall = null;
let hotmailCall = null;
const service = api.createFlowMailPollingService({
addLog: async (message, level, options) => {
logs.push({ message, level, options });
},
buildVerificationPollPayloadForNode: (nodeId, state, overrides) => {
buildCall = { nodeId, state, overrides };
return {
flowId: state.activeFlowId,
nodeId,
step: 4,
targetEmail: 'user@example.com',
maxAttempts: 2,
intervalMs: 100,
...overrides,
};
},
getMailConfig: () => ({ provider: 'hotmail-api', label: 'Hotmail' }),
pollHotmailVerificationCode: async (step, state, payload) => {
hotmailCall = { step, state, payload };
return { code: '123456', emailTimestamp: 456 };
},
});
const result = await service.pollFlowVerificationCode({
flowId: 'kiro',
nodeId: 'kiro-submit-verification-code',
state: { activeFlowId: 'kiro', email: 'user@example.com' },
step: 4,
filterAfterTimestamp: 123,
logStepKey: 'kiro-submit-verification-code',
});
assert.equal(result.code, '123456');
assert.equal(buildCall.nodeId, 'kiro-submit-verification-code');
assert.equal(buildCall.overrides.filterAfterTimestamp, 123);
assert.equal(hotmailCall.step, 4);
assert.equal(hotmailCall.payload.filterAfterTimestamp, 123);
assert.equal(logs.some((entry) => entry.message.includes('Hotmail')), true);
});
test('flow mail polling service prepares browser mail provider sessions and payload timeouts', async () => {
const api = loadFlowMailPollingApi();
let ensured2925 = null;
let mailMessage = null;
let mailOptions = null;
const service = api.createFlowMailPollingService({
addLog: async () => {},
buildVerificationPollPayloadForNode: () => ({
flowId: 'kiro',
nodeId: 'kiro-submit-verification-code',
step: 4,
targetEmail: 'user@example.com',
maxAttempts: 2,
intervalMs: 100,
}),
ensureMail2925MailboxSession: async (options) => {
ensured2925 = options;
},
getMailConfig: () => ({
provider: '2925',
source: 'mail-2925',
label: '2925 邮箱',
}),
sendToMailContentScriptResilient: async (_mail, message, options) => {
mailMessage = message;
mailOptions = options;
return { code: '654321', emailTimestamp: 789 };
},
});
const result = await service.pollFlowVerificationCode({
flowId: 'kiro',
nodeId: 'kiro-submit-verification-code',
state: {
activeFlowId: 'kiro',
currentMail2925AccountId: 'acct-1',
mail2925UseAccountPool: true,
mail2925Accounts: [
{ id: 'acct-1', email: 'pool@example.com' },
],
},
step: 4,
logStepKey: 'kiro-submit-verification-code',
});
assert.equal(result.code, '654321');
assert.equal(ensured2925.accountId, 'acct-1');
assert.equal(ensured2925.expectedMailboxEmail, 'pool@example.com');
assert.equal(mailMessage.type, 'POLL_EMAIL');
assert.equal(mailMessage.payload.targetEmail, 'user@example.com');
assert.equal(mailOptions.logStepKey, 'kiro-submit-verification-code');
assert.equal(mailOptions.responseTimeoutMs, 45000);
});
test('flow mail polling service lets 2925 limit errors flow through shared recovery', async () => {
const api = loadFlowMailPollingApi();
const limitError = new Error('MAIL2925_LIMIT_REACHED::子邮箱已达上限');
const recoveredError = new Error('switched-account');
const service = api.createFlowMailPollingService({
addLog: async () => {},
buildVerificationPollPayloadForNode: () => ({
step: 4,
maxAttempts: 1,
intervalMs: 100,
}),
getMailConfig: () => ({
provider: '2925',
source: 'mail-2925',
label: '2925 邮箱',
}),
ensureMail2925MailboxSession: async () => {},
isMail2925LimitReachedError: (error) => error === limitError,
handleMail2925LimitReachedError: async (step, error) => {
assert.equal(step, 4);
assert.equal(error, limitError);
return recoveredError;
},
sendToMailContentScriptResilient: async () => {
throw limitError;
},
});
await assert.rejects(
() => service.pollFlowVerificationCode({
flowId: 'kiro',
nodeId: 'kiro-submit-verification-code',
state: { activeFlowId: 'kiro' },
step: 4,
}),
/switched-account/
);
});
+158
View File
@@ -0,0 +1,158 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadGrokStateApi() {
const source = fs.readFileSync('flows/grok/background/state.js', 'utf8');
const globalScope = {};
return new Function('self', `${source}; return self.MultiPageBackgroundGrokState;`)(globalScope);
}
test('grok state view projects canonical runtime state into legacy flat read fields', () => {
const api = loadGrokStateApi();
const view = api.buildStateView({
runtimeState: {
flowState: {
openai: {
preserved: true,
},
grok: {
session: {
registerTabId: 42,
pageState: 'profile_entry',
pageUrl: 'https://accounts.x.ai/sign-up',
},
register: {
email: 'USER@EXAMPLE.COM',
firstName: 'Ada',
lastName: 'Lovelace',
password: 'Secret123!',
verificationRequestedAt: 1000,
verificationCode: 'ABC123',
status: 'verified',
completedAt: 2000,
},
sso: {
currentCookie: 'cookie-a',
cookies: ['cookie-a', 'cookie-b', 'cookie-a'],
extractedAt: 3000,
},
},
},
},
});
assert.equal(view.grokRegisterTabId, 42);
assert.equal(view.grokPageState, 'profile_entry');
assert.equal(view.grokEmail, 'user@example.com');
assert.equal(view.grokFirstName, 'Ada');
assert.equal(view.grokLastName, 'Lovelace');
assert.equal(view.grokPassword, 'Secret123!');
assert.equal(view.grokVerificationRequestedAt, 1000);
assert.equal(view.grokVerificationCode, 'ABC123');
assert.equal(view.grokRegisterStatus, 'verified');
assert.equal(view.grokCompletedAt, 2000);
assert.equal(view.grokSsoCookie, 'cookie-a');
assert.deepEqual(view.grokSsoCookies, ['cookie-a', 'cookie-b']);
assert.equal(view.grokSsoExtractedAt, 3000);
assert.equal(view.runtimeState.flowState.openai.preserved, true);
assert.equal(view.runtimeState.flowState.grok.register.email, 'user@example.com');
assert.equal(view.flowState.grok.sso.currentCookie, 'cookie-a');
assert.equal(view.flows.grok.sso.cookies.length, 2);
});
test('grok completion payloads update canonical runtime state and flat compatibility fields', () => {
const api = loadGrokStateApi();
const patch = api.applyNodeCompletionPayload({}, {
grokEmail: 'GROK@EXAMPLE.COM',
grokVerificationRequestedAt: 123,
grokSsoCookie: 'cookie-z',
grokSsoCookies: ['cookie-z', 'cookie-z', 'cookie-y'],
grokCompletedAt: 456,
});
assert.equal(patch.grokEmail, 'grok@example.com');
assert.equal(patch.grokVerificationRequestedAt, 123);
assert.equal(patch.grokSsoCookie, 'cookie-z');
assert.deepEqual(patch.grokSsoCookies, ['cookie-z', 'cookie-y']);
assert.equal(patch.grokCompletedAt, 456);
assert.equal(patch.grokSsoExtractedAt, 456);
assert.equal(patch.runtimeState.flowState.grok.register.email, 'grok@example.com');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-z');
});
test('grok fresh keep-state reset preserves SSO cookies but clears registration runtime', () => {
const api = loadGrokStateApi();
const patch = api.buildFreshKeepState({
runtimeState: {
flowState: {
grok: {
session: {
registerTabId: 42,
pageState: 'profile_entry',
},
register: {
email: 'grok@example.com',
status: 'completed',
completedAt: 1000,
},
sso: {
currentCookie: 'cookie-a',
cookies: ['cookie-a', 'cookie-b'],
extractedAt: 2000,
},
},
},
},
});
assert.equal(patch.grokRegisterTabId, null);
assert.equal(patch.grokPageState, '');
assert.equal(patch.grokEmail, '');
assert.equal(patch.grokRegisterStatus, '');
assert.equal(patch.grokCompletedAt, 0);
assert.equal(patch.grokSsoCookie, 'cookie-a');
assert.deepEqual(patch.grokSsoCookies, ['cookie-a', 'cookie-b']);
assert.equal(patch.grokSsoExtractedAt, 2000);
assert.equal(patch.runtimeState.flowState.grok.register.email, '');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-a');
});
test('grok downstream reset clears only the state owned by the restarted tail', () => {
const api = loadGrokStateApi();
const currentState = {
runtimeState: {
flowState: {
grok: {
session: {
registerTabId: 7,
pageState: 'signed_in',
pageUrl: 'https://grok.com/',
lastError: 'old-error',
},
register: {
email: 'grok@example.com',
status: 'completed',
completedAt: 1000,
},
sso: {
currentCookie: 'cookie-a',
cookies: ['cookie-a'],
extractedAt: 2000,
},
},
},
},
};
const profilePatch = api.buildDownstreamResetPatch('grok-submit-profile', currentState);
assert.equal(profilePatch.grokEmail, 'grok@example.com');
assert.equal(profilePatch.grokRegisterStatus, 'completed');
assert.equal(profilePatch.grokSsoCookie, '');
assert.deepEqual(profilePatch.grokSsoCookies, []);
const emailPatch = api.buildDownstreamResetPatch('grok-submit-email', currentState);
assert.equal(emailPatch.grokEmail, '');
assert.equal(emailPatch.grokRegisterStatus, '');
assert.equal(emailPatch.grokRegisterTabId, 7);
});
@@ -91,6 +91,18 @@ test('kiro desktop authorize runner uses a shared 3-minute page-load timeout bud
assert.match(source, /finalizeDesktopAuthorizeCallback/);
});
test('kiro desktop authorize runner delegates OTP mail polling to the shared flow mail service', () => {
const source = fs.readFileSync('flows/kiro/background/desktop-authorize-runner.js', 'utf8');
assert.match(source, /pollFlowVerificationCode/);
assert.doesNotMatch(source, /buildDesktopOtpPollPayload/);
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
});
test('background wires the Kiro register injector into desktop authorization runner', () => {
const source = fs.readFileSync('background.js', 'utf8');
const start = source.indexOf('const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({');
@@ -37,6 +37,18 @@ test('kiro register runner uses a shared 3-minute page-load timeout budget', ()
assert.match(source, /onRetryableError: buildKiroRetryRecovery\(tabId, \{\s*\.\.\.options,\s*timeoutBudget,/);
});
test('kiro register runner delegates verification mail polling to the shared flow mail service', () => {
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
assert.match(source, /pollFlowVerificationCode/);
assert.doesNotMatch(source, /buildKiroVerificationPollPayload/);
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
});
test('kiro register consent step treats Kiro Web signed-in page as completion', () => {
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
assert.match(source, /readKiroRegisterPageState\(tabId, \{/);
@@ -2,10 +2,13 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('background imports mail rule registry and OpenAI mail rules modules', () => {
test('background imports mail rule registry and flow mail rules modules', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /background\/mail-rule-registry\.js/);
assert.match(source, /flows\/openai\/mail-rules\.js/);
assert.match(source, /flows\/kiro\/mail-rules\.js/);
assert.match(source, /flows\/grok\/mail-rules\.js/);
assert.match(source, /background\/flow-mail-polling\.js/);
});
test('mail rule registry exposes canonical OpenAI verification poll payloads', () => {
@@ -133,3 +136,185 @@ test('mail rule registry rejects unknown active flow ids instead of silently usi
/未找到 flow=site-a 的邮件轮询规则构造器/
);
});
test('mail rule registry exposes Kiro AWS verification poll payloads by node', () => {
const stateSource = fs.readFileSync('flows/kiro/background/state.js', 'utf8');
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
const kiroSource = fs.readFileSync('flows/kiro/mail-rules.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${registrySource}; ${kiroSource}; return self;`)(globalScope);
const kiroMailRules = globalScope.MultiPageKiroMailRules.createKiroMailRules({
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
});
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
defaultFlowId: 'openai',
flowBuilders: {
kiro: kiroMailRules,
},
});
const baseState = {
activeFlowId: 'kiro',
mailProvider: '2925',
mail2925Mode: 'receive',
runtimeState: {
flowState: {
kiro: {
register: {
email: 'kiro-user@example.com',
},
},
},
},
};
assert.deepEqual(
registry.buildVerificationPollPayloadForNode(
'kiro-submit-verification-code',
baseState,
{ filterAfterTimestamp: 12345 }
),
{
flowId: 'kiro',
ruleId: 'kiro-submit-verification-code',
nodeId: 'kiro-submit-verification-code',
step: 4,
artifactType: 'code',
codePatterns: [
{
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
},
{
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
},
{
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
},
],
filterAfterTimestamp: 12345,
requiredKeywords: ['verification', '验证码', 'code', 'aws'],
senderFilters: [
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
],
subjectFilters: ['aws builder id', 'verification', '验证码', 'code', 'aws'],
targetEmail: 'kiro-user@example.com',
targetEmailHints: ['kiro-user@example.com'],
mail2925MatchTargetEmail: true,
maxAttempts: 17,
intervalMs: 16000,
}
);
const desktopPayload = registry.buildVerificationPollPayloadForNode(
'kiro-complete-desktop-authorize',
{
...baseState,
mailProvider: 'luckmail-api',
mail2925Mode: 'provide',
}
);
assert.equal(desktopPayload.ruleId, 'kiro-complete-desktop-authorize');
assert.equal(desktopPayload.step, 8);
assert.equal(desktopPayload.mail2925MatchTargetEmail, false);
assert.equal(desktopPayload.maxAttempts, 3);
assert.equal(desktopPayload.intervalMs, 15000);
});
test('mail rule registry exposes Grok xAI verification poll payloads by node', () => {
const stateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8');
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
const grokSource = fs.readFileSync('flows/grok/mail-rules.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${registrySource}; ${grokSource}; return self;`)(globalScope);
const grokMailRules = globalScope.MultiPageGrokMailRules.createGrokMailRules({
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
});
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
defaultFlowId: 'openai',
flowBuilders: {
grok: grokMailRules,
},
});
assert.deepEqual(
registry.buildVerificationPollPayloadForNode(
'grok-submit-verification-code',
{
activeFlowId: 'grok',
mailProvider: '2925',
mail2925Mode: 'receive',
runtimeState: {
flowState: {
grok: {
register: {
email: 'grok-user@example.com',
},
},
},
},
},
{ filterAfterTimestamp: 12345 }
),
{
flowId: 'grok',
ruleId: 'grok-submit-verification-code',
nodeId: 'grok-submit-verification-code',
step: 3,
artifactType: 'code',
codePatterns: [
{
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
flags: 'gi',
},
{
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[:\\s]*(\\d{6})',
flags: 'gi',
},
{
source: '(?:验证码|代码|确认码)[:\\s为]+(\\d{6})',
flags: 'gi',
},
{
source: '(?<!#)\\b(\\d{6})\\b',
flags: 'g',
},
],
filterAfterTimestamp: 12345,
requiredKeywords: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
senderFilters: ['x.ai', 'xai', 'grok'],
subjectFilters: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
targetEmail: 'grok-user@example.com',
targetEmailHints: ['grok-user@example.com'],
mail2925MatchTargetEmail: true,
maxAttempts: 17,
intervalMs: 16000,
}
);
const luckmailPayload = registry.buildVerificationPollPayloadForNode(
'grok-submit-verification-code',
{
activeFlowId: 'grok',
mailProvider: 'luckmail-api',
mail2925Mode: 'provide',
grokEmail: 'fallback@example.com',
}
);
assert.equal(luckmailPayload.mail2925MatchTargetEmail, false);
assert.equal(luckmailPayload.maxAttempts, 3);
assert.equal(luckmailPayload.intervalMs, 15000);
});
@@ -625,6 +625,47 @@ test('SAVE_SETTING mirrors activeFlowId into flowId when switching to kiro flow'
});
});
test('CLEAR_GROK_SSO_COOKIES delegates canonical runtime cleanup to background', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
let called = false;
const router = api.createMessageRouter({
clearGrokSsoCookies: async () => {
called = true;
return {
ok: true,
state: {
grokSsoCookie: '',
grokSsoCookies: [],
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: '',
cookies: [],
extractedAt: 0,
},
},
},
},
},
};
},
});
const response = await router.handleMessage({
type: 'CLEAR_GROK_SSO_COOKIES',
source: 'sidepanel',
payload: {},
});
assert.equal(called, true);
assert.equal(response.ok, true);
assert.deepStrictEqual(response.state.grokSsoCookies, []);
assert.equal(response.state.runtimeState.flowState.grok.sso.currentCookie, '');
});
test('SAVE_SETTING syncs canonical kiro settingsState back into session state', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
+12
View File
@@ -17,11 +17,14 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
assert.match(source, /flows\/kiro\/background\/desktop-client\.js/);
assert.match(source, /flows\/kiro\/background\/desktop-authorize-runner\.js/);
assert.match(source, /flows\/kiro\/background\/publisher-kiro-rs\.js/);
assert.match(source, /flows\/grok\/background\/state\.js/);
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
assert.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/);
assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/);
assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/);
assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/);
assert.match(source, /const grokRegisterRunner = self\.MultiPageBackgroundGrokRegisterRunner\?\.createGrokRegisterRunner\(/);
assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/);
assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/);
@@ -32,11 +35,20 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
assert.match(source, /'kiro-start-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroStartDesktopAuthorize\(state\)/);
assert.match(source, /'kiro-complete-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroCompleteDesktopAuthorize\(state\)/);
assert.match(source, /'kiro-upload-credential': \(state\) => kiroPublisher\.executeKiroUploadCredential\(state\)/);
assert.match(source, /'grok-open-signup-page': \(state\) => grokRegisterRunner\.executeGrokOpenSignupPage\(state\)/);
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, /'grok-extract-sso-cookie': \(state\) => grokRegisterRunner\.executeGrokExtractSsoCookie\(state\)/);
assert.match(
source,
/'kiro-open-register-page',[\s\S]*'kiro-submit-email',[\s\S]*'kiro-submit-name',[\s\S]*'kiro-submit-verification-code',[\s\S]*'kiro-submit-password',[\s\S]*'kiro-complete-register-consent',[\s\S]*'kiro-start-desktop-authorize',[\s\S]*'kiro-complete-desktop-authorize',[\s\S]*'kiro-upload-credential'/
);
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'/
);
});
test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
+14
View File
@@ -69,9 +69,23 @@ test('contribution registry resolves the combined tutorial entry per flow', () =
);
});
test('contribution registry keeps Grok SSO export flow out of published contribution checks', () => {
const api = loadApi();
assert.equal(api.getContributionTutorialEntry('grok'), null);
assert.deepEqual(api.getContributionAdapterIds('grok'), []);
assert.deepEqual(api.getPublishedContributionFlowIds(['openai', 'kiro', 'grok']), ['openai', 'kiro']);
assert.throws(
() => api.assertPublishedFlowsHaveContributionAdapters(['grok']),
/缺少账号贡献适配器:grok/
);
});
test('contribution registry fails fast when a published flow has no adapter', () => {
const api = loadApi();
assert.deepEqual(api.getPublishedContributionFlowIds(), ['openai', 'kiro']);
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(), true);
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true);
assert.throws(
() => api.assertPublishedFlowsHaveContributionAdapters(['openai', 'missing-flow']),
+30
View File
@@ -98,6 +98,36 @@ test('flow capability registry exposes Kiro as an independent flow with its own
);
});
test('flow capability registry exposes Grok as an independent SSO flow without OpenAI-only modes', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry();
const capabilityState = registry.resolveSidepanelCapabilities({
state: {
activeFlowId: 'grok',
targetId: 'webchat2api',
signupMethod: 'phone',
plusModeEnabled: true,
phoneVerificationEnabled: true,
accountContributionEnabled: true,
},
});
assert.equal(capabilityState.activeFlowId, 'grok');
assert.equal(capabilityState.canShowPhoneSettings, false);
assert.equal(capabilityState.canShowPlusSettings, false);
assert.equal(capabilityState.canShowContributionMode, false);
assert.equal(capabilityState.canShowLuckmail, false);
assert.equal(capabilityState.effectiveSignupMethod, 'email');
assert.equal(capabilityState.effectiveTargetId, 'webchat2api');
assert.deepEqual(capabilityState.supportedTargetIds, ['webchat2api']);
assert.deepEqual(capabilityState.flowCapabilities.contributionAdapterIds, []);
assert.deepEqual(
capabilityState.visibleGroupIds,
['grok-runtime-status', 'grok-target-webchat2api', 'service-account', 'service-email', 'service-proxy']
);
});
test('flow capability registry exposes shared auto-run validation for phone locks and target support', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry({
+122 -1
View File
@@ -16,12 +16,14 @@ function loadApis() {
test('flow registry exposes canonical flow and target metadata', () => {
const { flowRegistry } = loadApis();
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro']);
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
assert.equal(flowRegistry.normalizeFlowId('kiro'), 'kiro');
assert.equal(flowRegistry.normalizeFlowId('grok'), 'grok');
assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai');
assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI');
assert.equal(flowRegistry.normalizeTargetId('openai', 'sub2api'), 'sub2api');
assert.equal(flowRegistry.normalizeTargetId('kiro', 'anything-else'), 'kiro-rs');
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']
@@ -30,10 +32,18 @@ test('flow registry exposes canonical flow and target metadata', () => {
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
);
assert.deepEqual(
flowRegistry.getVisibleGroupIds('grok', 'webchat2api'),
['grok-runtime-status', 'grok-target-webchat2api', 'service-account', 'service-email', 'service-proxy']
);
assert.deepEqual(
flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
['cpa', 'sub2api', 'codex2api']
);
assert.deepEqual(
flowRegistry.getTargetOptions('grok').map((entry) => entry.id),
['webchat2api']
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method']
@@ -41,6 +51,8 @@ test('flow registry exposes canonical flow and target metadata', () => {
assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true);
assert.equal(flowRegistry.getFlowCapabilities('kiro').supportsAccountContribution, true);
assert.equal(flowRegistry.getFlowCapabilities('grok').supportsAccountContribution, false);
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').supportedTargetIds, ['webchat2api']);
assert.deepEqual(
flowRegistry.getFlowCapabilities('openai').contributionAdapterIds,
['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']
@@ -49,6 +61,7 @@ test('flow registry exposes canonical flow and target metadata', () => {
flowRegistry.getFlowCapabilities('kiro').contributionAdapterIds,
['kiro-builder-id']
);
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').contributionAdapterIds, []);
});
test('settings schema normalizes view input into canonical nested namespaces', () => {
@@ -68,6 +81,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
stepExecutionRangeByFlow: {
openai: { enabled: true, fromStep: 2, toStep: 9 },
kiro: { enabled: true, fromStep: 1, toStep: 9 },
grok: { enabled: true, fromStep: 2, toStep: 4 },
},
});
@@ -78,6 +92,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
assert.equal(normalized.flows.openai.selectedTargetId, 'cpa');
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(normalized.flows.kiro.selectedTargetId, 'kiro-rs');
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.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
@@ -85,6 +100,11 @@ test('settings schema normalizes view input into canonical nested namespaces', (
fromStep: 1,
toStep: 9,
});
assert.deepEqual(normalized.flows.grok.autoRun.stepExecutionRange, {
enabled: true,
fromStep: 2,
toStep: 4,
});
});
test('settings schema lets explicit flat step range override stale canonical range', () => {
@@ -130,6 +150,11 @@ test('settings schema can project canonical state into a read view without legac
assert.equal(view.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(view.settingsSchemaVersion, 5);
assert.equal(view.settingsState.activeFlowId, 'kiro');
assert.deepEqual(view.stepExecutionRangeByFlow.grok, {
enabled: false,
fromStep: 1,
toStep: 5,
});
});
test('settings schema preserves CPA session strategy in canonical state and read view', () => {
@@ -143,3 +168,99 @@ test('settings schema preserves CPA session strategy in canonical state and read
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'cpa_codex_session');
assert.equal(view.plusAccountAccessStrategy, 'cpa_codex_session');
});
test('settings schema preserves registered custom flow settings without openai/kiro hardcoding', () => {
const { settingsSchema } = loadApis();
const customFlowRegistry = {
DEFAULT_FLOW_ID: 'openai',
getRegisteredFlowIds: () => ['openai', 'kiro', 'sample'],
getDefaultTargetId(flowId) {
return flowId === 'sample' ? 'sample-target' : (flowId === 'kiro' ? 'kiro-rs' : 'cpa');
},
getFlowDefinition(flowId) {
if (flowId !== 'sample') {
return null;
}
return {
id: 'sample',
defaultTargetId: 'sample-target',
settingsDefaults: {
targets: {
'sample-target': {
endpoint: 'https://sample.example.com',
},
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 3 },
},
},
};
},
getTargetDefinitions(flowId) {
if (flowId === 'sample') {
return {
'sample-target': { id: 'sample-target', label: 'Sample Target' },
};
}
if (flowId === 'kiro') {
return {
'kiro-rs': { id: 'kiro-rs', label: 'kiro.rs' },
};
}
return {
cpa: { id: 'cpa', label: 'CPA' },
sub2api: { id: 'sub2api', label: 'SUB2API' },
codex2api: { id: 'codex2api', label: 'Codex2API' },
};
},
normalizeFlowId(value = '', fallback = 'openai') {
const normalized = String(value || '').trim().toLowerCase();
return ['openai', 'kiro', 'sample'].includes(normalized)
? normalized
: (['openai', 'kiro', 'sample'].includes(fallback) ? fallback : 'openai');
},
normalizeTargetId(flowId, targetId = '', fallback = '') {
const targets = Object.keys(customFlowRegistry.getTargetDefinitions(flowId));
const normalized = String(targetId || '').trim().toLowerCase();
if (targets.includes(normalized)) {
return normalized;
}
if (targets.includes(fallback)) {
return fallback;
}
return customFlowRegistry.getDefaultTargetId(flowId);
},
};
const schema = settingsSchema.createSettingsSchema({ flowRegistry: customFlowRegistry });
const normalized = schema.normalizeSettingsState({
activeFlowId: 'sample',
targetId: 'sample-target',
settingsState: {
flows: {
sample: {
selectedTargetId: 'sample-target',
targets: {
'sample-target': {
endpoint: 'https://custom.example.com',
},
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 2, toStep: 3 },
},
},
},
},
});
const view = schema.buildSettingsView(normalized);
assert.equal(normalized.activeFlowId, 'sample');
assert.equal(normalized.flows.sample.selectedTargetId, 'sample-target');
assert.equal(normalized.flows.sample.targets['sample-target'].endpoint, 'https://custom.example.com');
assert.deepEqual(view.stepExecutionRangeByFlow.sample, {
enabled: true,
fromStep: 2,
toStep: 3,
});
assert.equal(schema.getSelectedTargetId(normalized, 'sample'), 'sample-target');
});
+185
View File
@@ -0,0 +1,185 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadGrokRunnerApi() {
const source = fs.readFileSync('flows/grok/background/register-runner.js', 'utf8');
const globalScope = {};
return new Function('self', `${source}; return self.MultiPageBackgroundGrokRegisterRunner;`)(globalScope);
}
function getGrokRuntime(state = {}) {
return state?.runtimeState?.flowState?.grok || {};
}
test('grok runner delegates verification polling to the shared flow mail service', () => {
const source = fs.readFileSync('flows/grok/background/register-runner.js', 'utf8');
assert.match(source, /pollFlowVerificationCode/);
assert.doesNotMatch(source, /buildGrokVerificationPollPayload/);
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
});
test('grok content script does not patch global MouseEvent prototypes', () => {
const source = fs.readFileSync('flows/grok/content/register-page.js', 'utf8');
assert.doesNotMatch(source, /MouseEvent\.prototype/);
assert.doesNotMatch(source, /Object\.defineProperty\(MouseEvent/);
assert.match(source, /screenX:/);
assert.match(source, /screenY:/);
});
test('grok verification runner polls by flow node and submits normalized code', async () => {
const api = loadGrokRunnerApi();
const calls = [];
let completedPayload = null;
let currentState = {
activeFlowId: 'grok',
mailProvider: '2925',
grokRegisterTabId: 101,
grokEmail: 'grok-user@example.com',
grokVerificationRequestedAt: 1000000,
runtimeState: {
flowState: {
grok: {
session: {
registerTabId: 101,
},
register: {
email: 'grok-user@example.com',
verificationRequestedAt: 1000000,
},
},
},
},
};
const runner = api.createGrokRegisterRunner({
addLog: async () => {},
chrome: {
tabs: {
get: async (tabId) => ({ id: tabId }),
update: async () => {},
},
},
completeNodeFromBackground: async (_nodeId, payload) => {
completedPayload = payload;
},
ensureContentScriptReadyOnTab: async () => {},
getState: async () => currentState,
getTabId: async () => 101,
isTabAlive: async () => true,
pollFlowVerificationCode: async (options) => {
calls.push({ type: 'poll', options });
return { code: 'ABC-123', messageId: 'mail-001' };
},
registerTab: async () => {},
sendToContentScriptResilient: async (sourceId, message) => {
calls.push({ type: 'send', sourceId, message });
return { submitted: true, state: 'profile_entry', url: 'https://accounts.x.ai/profile' };
},
setState: async (patch) => {
currentState = { ...currentState, ...patch };
},
waitForTabStableComplete: async () => {},
});
await runner.executeGrokSubmitVerificationCode({ nodeId: 'grok-submit-verification-code', ...currentState });
const pollCall = calls.find((entry) => entry.type === 'poll');
assert.equal(pollCall.options.flowId, 'grok');
assert.equal(pollCall.options.nodeId, 'grok-submit-verification-code');
assert.equal(pollCall.options.step, 3);
assert.equal(pollCall.options.logStep, 3);
assert.equal(pollCall.options.filterAfterTimestamp, 400000);
assert.equal(pollCall.options.state.activeFlowId, 'grok');
assert.equal(pollCall.options.state.visibleStep, 3);
const sendCall = calls.find((entry) => entry.type === 'send');
assert.equal(sendCall.sourceId, 'grok-register-page');
assert.equal(sendCall.message.type, 'EXECUTE_NODE');
assert.equal(sendCall.message.nodeId, 'grok-submit-verification-code');
assert.deepEqual(sendCall.message.payload, { code: 'ABC123' });
assert.equal(completedPayload.grokVerificationCode, 'ABC123');
assert.equal(completedPayload.grokVerificationRawCode, 'ABC-123');
assert.equal(completedPayload.grokVerificationMessageId, 'mail-001');
assert.equal(getGrokRuntime(completedPayload).register.verificationCode, 'ABC123');
assert.equal(getGrokRuntime(completedPayload).register.status, 'verified');
});
test('grok SSO extraction accumulates unique cookies without logging the secret value', async () => {
const api = loadGrokRunnerApi();
const logs = [];
let completedPayload = null;
let markUsedPayload = null;
let currentState = {
activeFlowId: 'grok',
grokRegisterTabId: 202,
grokSsoCookies: ['existing-cookie', 'new-cookie'],
runtimeState: {
flowState: {
grok: {
session: {
registerTabId: 202,
},
sso: {
cookies: ['existing-cookie', 'new-cookie'],
},
},
},
},
};
const runner = api.createGrokRegisterRunner({
addLog: async (message, level) => {
logs.push({ message, level });
},
chrome: {
cookies: {
get: async () => ({ value: 'new-cookie' }),
},
tabs: {
get: async (tabId) => ({ id: tabId }),
update: async () => {},
},
},
completeNodeFromBackground: async (_nodeId, payload) => {
completedPayload = payload;
},
ensureContentScriptReadyOnTab: async () => {},
getState: async () => currentState,
getTabId: async () => 202,
isTabAlive: async () => true,
markCurrentRegistrationAccountUsed: async (state) => {
markUsedPayload = state;
},
registerTab: async () => {},
sendToContentScriptResilient: async () => {
throw new Error('content fallback should not be used when chrome.cookies finds sso');
},
setState: async (patch) => {
currentState = { ...currentState, ...patch };
},
sleepWithStop: async () => {},
waitForTabStableComplete: async () => {},
});
await runner.executeGrokExtractSsoCookie({ nodeId: 'grok-extract-sso-cookie', ...currentState });
assert.equal(completedPayload.grokSsoCookie, 'new-cookie');
assert.deepEqual(completedPayload.grokSsoCookies, ['existing-cookie', 'new-cookie']);
assert.equal(getGrokRuntime(completedPayload).sso.currentCookie, 'new-cookie');
assert.deepEqual(getGrokRuntime(completedPayload).sso.cookies, ['existing-cookie', 'new-cookie']);
assert.equal(markUsedPayload.grokSsoCookie, 'new-cookie');
assert.equal(logs.some(({ message }) => message.includes('new-cookie')), false);
});
test('grok register runner requires background node completion dependency', () => {
const api = loadGrokRunnerApi();
assert.throws(
() => api.createGrokRegisterRunner({}),
/requires completeNodeFromBackground/
);
});
+2
View File
@@ -3,12 +3,14 @@ const fs = require('node:fs');
const FLOW_DEFINITION_FILES = Object.freeze([
'flows/openai/index.js',
'flows/kiro/index.js',
'flows/grok/index.js',
'flows/index.js',
]);
const FLOW_WORKFLOW_FILES = Object.freeze([
'flows/openai/workflow.js',
'flows/kiro/workflow.js',
'flows/grok/workflow.js',
]);
function readBundle(files = []) {
@@ -36,6 +36,7 @@ function extractFunction(source, name) {
test('sidepanel html exposes flow selector and kiro source fields', () => {
[
'id="select-flow"',
'<option value="grok">Grok</option>',
'id="label-source-selector"',
'id="row-step6-cookie-settings"',
'id="row-kiro-rs-url"',
@@ -46,9 +47,90 @@ test('sidepanel html exposes flow selector and kiro source fields', () => {
'id="row-kiro-web-status"',
'id="row-kiro-login-url"',
'id="row-kiro-upload-status"',
'id="row-grok-register-status"',
'id="row-grok-sso-status"',
'id="row-grok-sso-settings"',
'id="btn-copy-grok-sso"',
'id="btn-export-grok-sso"',
'id="btn-clear-grok-sso"',
'<script src="../flows/grok/index.js"></script>',
'<script src="../flows/grok/workflow.js"></script>',
].forEach((snippet) => {
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});
assert.ok(
sidepanelHtml.indexOf('<script src="../flows/kiro/workflow.js"></script>')
< sidepanelHtml.indexOf('<script src="../flows/grok/index.js"></script>')
);
assert.ok(
sidepanelHtml.indexOf('<script src="../flows/grok/workflow.js"></script>')
< sidepanelHtml.indexOf('<script src="../flows/index.js"></script>')
);
});
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);
const nextManagerIndex = sidepanelSource.indexOf('const hotmailManager', clearButtonIndex);
assert.notEqual(nextManagerIndex, -1);
const block = sidepanelSource.slice(clearButtonIndex, nextManagerIndex);
assert.match(block, /type:\s*'CLEAR_GROK_SSO_COOKIES'/);
assert.match(block, /chrome\.runtime\.sendMessage/);
assert.doesNotMatch(block, /chrome\.storage/);
assert.doesNotMatch(block, /storage\.local\.set/);
});
test('sidepanel renders Grok SSO status from canonical runtime state', () => {
const bundle = [
extractFunction(sidepanelSource, 'getGrokRuntimeState'),
extractFunction(sidepanelSource, 'normalizeGrokSsoCookies'),
extractFunction(sidepanelSource, 'getGrokRegisterStatusLabel'),
extractFunction(sidepanelSource, 'renderGrokRuntimeState'),
].join('\n');
const api = new Function(`
let latestState = {};
const displayGrokRegisterStatus = { textContent: '' };
const displayGrokSsoStatus = { textContent: '' };
const displayGrokSsoCookie = { textContent: '', title: '' };
const buttons = [];
const btnCopyGrokSso = { disabled: false };
const btnExportGrokSso = { disabled: false };
const btnClearGrokSso = { disabled: false };
${bundle}
return {
displayGrokRegisterStatus,
displayGrokSsoStatus,
displayGrokSsoCookie,
btnCopyGrokSso,
btnExportGrokSso,
btnClearGrokSso,
renderGrokRuntimeState,
};
`)();
api.renderGrokRuntimeState({
runtimeState: {
flowState: {
grok: {
register: { status: 'completed' },
sso: {
currentCookie: '1234567890abcdef',
cookies: ['1234567890abcdef', 'second-cookie'],
extractedAt: 0,
},
},
},
},
});
assert.equal(api.displayGrokRegisterStatus.textContent, '已完成');
assert.match(api.displayGrokSsoStatus.textContent, /^已提取 2 条/);
assert.equal(api.displayGrokSsoCookie.textContent, '12345678...abcdef');
assert.equal(api.btnCopyGrokSso.disabled, false);
assert.equal(api.btnExportGrokSso.disabled, false);
assert.equal(api.btnClearGrokSso.disabled, false);
});
test('sidepanel Kiro GitHub button opens the configured fork', () => {
+1
View File
@@ -525,6 +525,7 @@ function updateAccountRunHistorySettingsUI() {}
function updatePhoneVerificationSettingsUI() {}
function updatePanelModeUI() {}
function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); }
function renderGrokRuntimeState() {}
function renderSub2ApiGroupOptions() {}
function isLuckmailProvider() { return false; }
function updateButtonStates() {}
+62 -2
View File
@@ -15,6 +15,11 @@ test('background imports shared source registry module', () => {
assert.match(source, /core\/flow-kernel\/settings-schema\.js/);
assert.match(source, /core\/flow-kernel\/source-registry\.js/);
assert.match(source, /shared\/kiro-timeouts\.js/);
assert.match(source, /flows\/grok\/index\.js/);
assert.match(source, /flows\/grok\/workflow\.js/);
assert.match(source, /flows\/grok\/background\/state\.js/);
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
assert.match(source, /flows\/grok\/mail-rules\.js/);
});
test('manifest loads shared source registry before content utils in static bundles', () => {
@@ -41,15 +46,38 @@ test('manifest no longer ships a static Kiro content bundle', () => {
assert.equal(hasStaticKiroBundle, false);
});
test('manifest loads Grok flow definition in static bundles but not Grok content runtime', () => {
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
for (const entry of manifest.content_scripts || []) {
const scripts = Array.isArray(entry.js) ? entry.js : [];
if (!scripts.includes('flows/index.js')) continue;
assert.ok(scripts.includes('flows/kiro/index.js'));
assert.ok(scripts.includes('flows/grok/index.js'));
assert.ok(
scripts.indexOf('flows/kiro/index.js') < scripts.indexOf('flows/grok/index.js'),
'Kiro definition should load before Grok definition'
);
assert.ok(
scripts.indexOf('flows/grok/index.js') < scripts.indexOf('flows/index.js'),
'Grok definition must load before flows/index.js'
);
assert.equal(scripts.includes('flows/grok/content/register-page.js'), false);
}
});
test('background injects shared Kiro timeout module before Kiro content scripts', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(
source,
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
);
assert.match(
source,
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
);
assert.match(
source,
/const GROK_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'content\/utils\.js', 'flows\/grok\/content\/register-page\.js'\];/
);
});
@@ -83,6 +111,20 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
}),
'kiro-desktop-authorize'
);
assert.equal(
registry.detectSourceFromLocation({
url: 'https://accounts.x.ai/sign-up',
hostname: 'accounts.x.ai',
}),
'grok-register-page'
);
assert.equal(
registry.detectSourceFromLocation({
url: 'https://grok.com/',
hostname: 'grok.com',
}),
'grok-register-page'
);
assert.equal(
registry.detectSourceFromLocation({
url: 'https://example.com/',
@@ -115,6 +157,22 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
),
true
);
assert.equal(
registry.matchesSourceUrlFamily(
'grok-register-page',
'https://accounts.x.ai/sign-up',
'https://grok.com/'
),
true
);
assert.equal(
registry.matchesSourceUrlFamily(
'grok-register-page',
'https://grok.com/',
'https://accounts.x.ai/sign-up'
),
true
);
assert.equal(
registry.matchesSourceUrlFamily(
'kiro-desktop-authorize',
@@ -137,4 +195,6 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/register-runner', 'kiro-open-register-page'), true);
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/desktop-authorize-runner', 'kiro-start-desktop-authorize'), true);
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);
});
+33 -1
View File
@@ -23,6 +23,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
const kiroSteps = api.getSteps({ activeFlowId: 'kiro' });
const grokSteps = api.getSteps({ activeFlowId: 'grok' });
assert.equal(Array.isArray(steps), true);
assert.equal(steps.length, 11);
@@ -162,8 +163,9 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19);
assert.equal(api.hasFlow('openai'), true);
assert.equal(api.hasFlow('kiro'), true);
assert.equal(api.hasFlow('grok'), true);
assert.equal(api.hasFlow('site-a'), false);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro']);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
assert.deepStrictEqual(
@@ -210,6 +212,36 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
[],
]
);
assert.deepStrictEqual(
grokSteps.map((step) => step.key),
[
'grok-open-signup-page',
'grok-submit-email',
'grok-submit-verification-code',
'grok-submit-profile',
'grok-extract-sso-cookie',
]
);
assert.equal(grokSteps.every((step) => step.flowId === 'grok'), true);
assert.equal(grokSteps[0].driverId, 'flows/grok/background/register-runner');
assert.equal(grokSteps[0].sourceId, 'grok-register-page');
assert.equal(grokSteps[2].mailRuleId, 'grok-submit-verification-code');
assert.deepStrictEqual(
grokSteps.map((step) => step.title),
['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie']
);
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5]);
assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 5);
assert.deepStrictEqual(
api.getNodes({ activeFlowId: 'grok' }).map((node) => node.next),
[
['grok-submit-email'],
['grok-submit-verification-code'],
['grok-submit-profile'],
['grok-extract-sso-cookie'],
[],
]
);
assert.equal(plusSteps[6].title, '创建 Plus Checkout');
assert.equal(plusSteps[8].title, 'PayPal 登录与授权');
+40 -10
View File
@@ -10,10 +10,11 @@
## 1. 项目目标
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册条主 flow
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册条主 flow
- `openai`OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路
- `kiro`AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路
- `grok`Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 本地导出链路
它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路:
@@ -28,7 +29,7 @@
- 自动确认 OAuth 同意页
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。
同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro / Grok 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。
## 2. 核心运行参与者
@@ -43,8 +44,8 @@
- 向后台发送命令
- 接收后台广播并更新 UI
- 动态渲染步骤列表
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target`kiro` flow 使用独立`flows.kiro.targetId`
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态不显示 OpenAI 接码 / Plus / 平台绑定配置
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target`kiro` / `grok` flow 使用各自`flows.<flowId>.targetId`
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态Grok flow 只显示 `webchat2api` SSO 导出状态、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置
- 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
@@ -150,13 +151,13 @@
- `runtimeState`:当前运行中的 node、nodeStatuses、activeRunId 等核心执行态
- `sharedState`automation window、tab registry、最近来源 URL 这类跨 flow 共享运行态
- `serviceState`:代理、邮箱等共享服务运行态
- `flowState`OpenAI / Kiro 各自的 flow 私有运行态
- `flowState`OpenAI / Kiro / Grok 各自的 flow 私有运行态
这两个模块的作用,是把“当前该跑哪些节点”“当前 state 应该怎么看”“前后台恢复时如何合并 patch”从 `background.js` 主入口里抽离出来,避免再次退回到旧的平铺 step 状态机。
### 3.4 步骤注册
[data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import``cpa-session-import``kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow。
[data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import``cpa-session-import``kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow`grok` flow 输出固定的 5 节点注册与 SSO Cookie 提取 workflow。
[core/flow-kernel/step-registry.js](./core/flow-kernel/step-registry.js) 负责把“workflow node 元数据”映射到“步骤执行器”,同时保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据。
这意味着:
@@ -168,7 +169,7 @@
### 3.4.1 验证码规则注册
[background/mail-rule-registry.js](./background/mail-rule-registry.js) 负责按当前 `activeFlowId` 选择验证码规则构造器。[flows/openai/mail-rules.js](./flows/openai/mail-rules.js) 提供 OpenAI flow 当前使用的注册验证码 / 登录验证码规则。
[background/mail-rule-registry.js](./background/mail-rule-registry.js) 负责按当前 `activeFlowId` 选择验证码规则构造器。[flows/openai/mail-rules.js](./flows/openai/mail-rules.js) 提供 OpenAI flow 当前使用的注册验证码 / 登录验证码规则[flows/kiro/mail-rules.js](./flows/kiro/mail-rules.js) 提供 Kiro / AWS Builder ID 注册验证码与桌面授权验证码规则,[flows/grok/mail-rules.js](./flows/grok/mail-rules.js) 提供 xAI / Grok 注册验证码规则
这层统一产出:
@@ -180,7 +181,8 @@
它的职责边界是:
- `background/verification-flow.js` 只消费统一规则 payload,不再在每个 provider 或内容脚本里各自硬写一套 OpenAI 验证码过滤条件。
- “邮件从哪里读”由 provider 决定,“哪封邮件算命中验证码”优先由规则注册层决定
- `background/flow-mail-polling.js` 负责共享的 provider 分派、iCloud / 2925 会话准备、网页邮箱标签页打开与内容脚本轮询;flow runner 只提供 `flowId / nodeId / step / filterAfterTimestamp` 这类业务上下文
- “邮件从哪里读”由公共轮询调度层和 provider 决定,“哪封邮件算命中验证码”优先由规则注册层决定。
- 新 flow 如需接入邮箱验证码,应优先新增自己的 mail rules builder,而不是继续改写 OpenAI 规则。
### 3.5 日志步骤号链路
@@ -214,6 +216,7 @@
- `contributionLastPollAt`
- OAuth 链接
- Kiro 运行态:使用 `kiroRuntime.session / register / webAuth / desktopAuth / upload` 命名空间,保存 Kiro 官方登录入口、Builder ID 注册页状态、Kiro Web 登录态摘要、桌面授权 PKCE 凭据、上传状态和当前 Kiro 会话进度
- Grok 运行态:权威状态位于 `runtimeState.flowState.grok`,包含 `session / register / sso`;顶层 `grokRegisterTabId / grokPageState / grokEmail / grokPassword / grokSsoCookie / grokSsoCookies` 只作为兼容投影,不应当再形成第二套状态机
- 当前轮冻结后的注册方式 `resolvedSignupMethod`
- 当前统一账号标识 `accountIdentifierType / accountIdentifier`
- 当前邮箱 / 密码
@@ -762,9 +765,32 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
- Kiro 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
- Kiro 自动运行走通用 linear node runner,但执行器、运行态、页面驱动和上传器全部独立在 `background/kiro/*``content/kiro/*`
- Kiro 运行态统一落在 `kiroRuntime` 命名空间,不再散落为多个平铺字段
- Kiro 注册验证码与桌面授权验证码只在 runner 内计算业务时间窗,具体 AWS 邮件匹配规则由 `flows/kiro/mail-rules.js` 提供,provider 分派统一走 `background/flow-mail-polling.js`
- `kiro.rs` 上传固定发送 BuilderId profileArn `arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX`machineId 固定按 `sha256("KotlinNativeAPI/" + refreshToken)` 生成
- Kiro 注册页覆盖 `app.kiro.dev` 与 AWS Builder ID 页面;桌面授权页只绑定 AWS authorize 页面,二者由后台按 source 动态注入,避免同域 AWS 页面被错误归到另一条 Kiro 子链路
## 6.3 Grok Flow 链路
Grok flow 的目标是完成 Grok / xAI 注册页自动化,并把 `sso` Cookie 作为 `webchat2api` 可用的本地导出产物。它不是 OpenAI 目标来源,也不是公开贡献 / 发布 flow。
链路如下:
1. sidepanel 把当前 flow 切到 `grok`target 固定为 `webchat2api`
2. 步骤 1 打开 Grok / xAI 注册入口,并清理本轮注册页运行态
3. 步骤 2 通过共享邮箱服务生成或复用注册邮箱,并提交到 xAI 登录/注册页
4. 步骤 3 调用 `background/flow-mail-polling.js`,由 `flows/grok/mail-rules.js` 构造 xAI / Grok 验证码规则并轮询邮箱验证码,再交给 Grok 内容脚本提交
5. 步骤 4 填写资料和密码,推进注册完成页状态
6. 步骤 5 先通过 `chrome.cookies.get``x.ai / grok.com / accounts.x.ai` 查找 `sso` Cookie,必要时 fallback 到 Grok 内容脚本读取 `document.cookie`;提取到的值保存到 `runtimeState.flowState.grok.sso` 并投影到 `grokSsoCookie / grokSsoCookies`
7. sidepanel 只展示掩码后的 SSO 值,复制和导出才使用完整值;清空动作必须发 `CLEAR_GROK_SSO_COOKIES` 给后台,由 `flows/grok/background/state.js` 构造 patch,不能在 sidepanel 直接写 `chrome.storage`
这条链路的关键边界是:
- Grok 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
- Grok 自动运行走通用 linear node runner,执行器、运行态、页面驱动和邮件规则全部放在 `flows/grok/*`
- Grok 内容脚本不能全局 patch 浏览器原型;点击事件需要的坐标只能在局部事件参数里补齐
- Grok SSO Cookie 是本地导出产物,`flows/grok/index.js``publicationTargets` 保持为空,`supportsAccountContribution` 保持为 `false`
- `shared/contribution-registry.js` 的默认发布贡献校验只覆盖声明了发布目标或支持贡献的 flow;Grok 不应被自动纳入贡献 adapter 缺失检查。如果未来要把 Grok 改成正式发布 flow,必须先新增明确的 publication target 与 contribution adapter,再打开对应能力
## 7. 邮箱与 provider 链路
### 7.1 共享邮箱生成补充
@@ -840,8 +866,11 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
- `background/registration-email-state.js`
统一维护当前注册邮箱、上一份比较基线邮箱、来源与更新时间,并提供 Duck 生成前解析比较基线、Step 8 清空当前邮箱但保留历史基线,以及 Step 8 `add-email` 写入邮箱时保留手机号身份的共享工具。
- `background/mail-rule-registry.js` + `flows/openai/mail-rules.js`
统一构造 Step 4 / Step 8 的验证码规则 payload,区分注册验证码与登录/绑定验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到同一层。
- `background/mail-rule-registry.js` + `flows/*/mail-rules.js`
统一构造各 flow 的验证码规则 payload,区分 OpenAI 注册/登录验证码、Kiro 注册/桌面授权验证码与 Grok 注册验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到 flow mail rules 层。
- `background/flow-mail-polling.js`
统一执行 flow 邮箱验证码轮询调度:读取当前邮箱配置,调用 `mailRuleRegistry.buildVerificationPollPayloadForNode`,分派 Hotmail / LuckMail / Cloudflare Temp Email / Cloud Mail / YYDS Mail 这类 API provider,并为 iCloud、2925 和普通网页邮箱准备会话与内容脚本轮询。Kiro 与后续新 flow 不应在各自 runner 内复制 provider 分派逻辑。
- `background/generated-email-helpers.js`
在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入;Duck 生成前会按“侧栏当前可见邮箱 -> `registrationEmailState.current` -> `registrationEmailState.previous`”的顺序解析比较基线。
@@ -1131,6 +1160,7 @@ Hide My Email 获取与管理链路:
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
- fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口
- Grok fresh-attempt reset 会清理注册页 runtime,但保留已提取的 `grokSsoCookies`,避免用户导出前被下一轮准备动作清空
5. 执行 `runAutoSequenceFromStep`
- 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog
- 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点
+3
View File
@@ -171,6 +171,7 @@
- `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod`
- 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod`
- flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。
- 本地导出型 flow、发布型 flow、贡献型 flow 必须显式区分:只有声明了 `publicationTargets` 或明确支持贡献能力的 flow 才能进入发布/贡献 adapter 校验;例如 Grok / `webchat2api` SSO Cookie 导出属于本地导出 flow,不能因为有 target 就默认接入贡献模式。
### 1.7 日志步骤号原则
@@ -285,6 +286,7 @@
- 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js`
- 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js`
- 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败
- 如果新增 flow 只是本地导出产物,例如 Cookie、JSON、会话文件、手动复制值,必须保持 `publicationTargets` 为空并关闭 `supportsAccountContribution`;如果后续产品决定发布到远端服务,必须先设计 publication target、贡献 adapter、敏感字段脱敏和测试,再打开能力开关。
### 3.4.1 iCloud Hide My Email 维护补充
@@ -495,6 +497,7 @@ npm test
15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交?
16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态?
17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId`
18. 如果新增或调整 flow target,我有没有确认它到底是本地导出、正式发布还是贡献 flow,并同步检查 `publicationTargets / supportsAccountContribution / contributionAdapterIds / shared/contribution-registry.js`
## 9. 完成标准
+17 -4
View File
@@ -21,7 +21,7 @@
- `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/``.github/``_metadata/``.vscode/` 等目录。
- `LICENSE`:项目许可证文件。
- `README.md`:面向使用者的精简说明文档,主要介绍项目用途、功能范围、快速开始与文档入口;不再承载过多技术实现细节。
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled``stepExecutionRangeByFlow``kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路。
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled``stepExecutionRangeByFlow``kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路,`grok` flow 走独立的“注册页 1-4 步 -> SSO Cookie 提取 5 步”链路。
- `cloudmail-utils.js`Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
- `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
- `gopay-utils.js`GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。
@@ -59,11 +59,14 @@
- `flows/kiro/background/desktop-client.js`:Kiro 桌面授权协议层,负责桌面 OIDC client 注册、PKCE 参数生成、授权地址组装、callback 参数校验和授权码换 token。
- `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 提取、去重保存与账号记录收尾。
- `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` 页面区分开,避免自动运行误停机。
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `background/mail-rule-registry.js`flow-aware 邮件规则注册表,负责按 `activeFlowId` 选择验证码规则构造器,并统一输出注册/登录验证码轮询 payload。
- `background/flow-mail-polling.js`flow-aware 邮件轮询调度层,负责读取共享邮箱配置、调用当前 flow 的 mail rules、分派 Hotmail / LuckMail / Cloudflare Temp Email / Cloud Mail / YYDS Mail API provider、准备 iCloud / 2925 / 网页邮箱会话,并统一通过邮箱内容脚本轮询验证码。
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;手动改邮箱、手动取邮箱等入口也统一经这里落到 `setEmailState`,同步维护注册邮箱运行态;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline;手动 `EXECUTE_NODE` 会先按 `stepExecutionRangeByFlow` 校验当前节点是否允许执行。
- `background/registration-email-state.js`:注册邮箱运行态共享模块,负责维护 `registrationEmailState = { current, previous, source, updatedAt }`,并提供“当前邮箱清空时保留上一比较基线”“Duck 生成前解析比较基线”“Step 8 `add-email` 写入邮箱时按需保留手机号身份”的统一工具。
- `core/flow-kernel/runtime-state.js`:运行态视图与 patch 构造模块,负责把会话字段归一到 `runtimeState / sharedState / serviceState / flowState` 结构,并维护 flow-aware 的 session patch、节点状态默认值与兼容视图输出。
@@ -152,6 +155,11 @@
## `flows/`
- `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/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 操作、页面状态读取与局部点击事件坐标补齐,不修改全局浏览器原型。
## `icons/`
@@ -178,9 +186,10 @@
## `shared/`
- `core/flow-kernel/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界、步骤范围 UI 作用域,以及 Plus 模式下 `账号接入策略` 的可选项、可编辑性与最终生效值。
- `core/flow-kernel/flow-registry.js`flow/source/settings group 总注册表,定义 `openai / kiro` 套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。
- `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 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。
- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*``settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro 的 `flows.kiro.targetId / targets``stepExecutionRangeByFlow`
- `shared/contribution-registry.js`:贡献 adapter 与教程入口注册表,负责 OpenAI / Kiro 贡献 adapter、教程入口和发布贡献 flow 校验;默认发布校验只覆盖声明了 publication target 或支持贡献的 flowGrok 这类本地 SSO 导出 flow 不自动纳入贡献 adapter 缺失检查
- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*``settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro/Grok 的 `flows.<flowId>.targetId / targets``stepExecutionRangeByFlow`
- `core/flow-kernel/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。
## `sidepanel/`
@@ -270,7 +279,9 @@
- `tests/background-mail2925-relogin-wait.test.js`:测试 2925 强制重登入口 URL、长登录超时与前后固定等待窗口。
- `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。
- `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI mail rules 模块接入,并覆盖验证码轮询 payload 构造
- `tests/background-flow-mail-polling-module.test.js`:测试 flow-aware 邮件轮询调度层对 API provider、网页邮箱 provider、2925 会话准备和 2925 上限恢复的共享分派行为
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI / Kiro mail rules 模块接入,并覆盖各 flow 验证码轮询 payload 构造。
- `tests/background-grok-state-module.test.js`:测试 Grok 运行态 helper 的默认值、兼容字段投影、节点完成回写、下游重置、fresh-attempt keep-state 与 SSO Cookie 保留规则。
- `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
- `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。
@@ -358,6 +369,7 @@
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
- `tests/contribution-registry.test.js`:测试贡献 adapter 注册、教程入口解析、默认发布贡献 flow 校验,以及 Grok 本地 SSO 导出 flow 不被自动纳入发布贡献检查。
- `tests/flow-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。
- `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。
- `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。
@@ -365,6 +377,7 @@
- `tests/sidepanel-contribution-mode-flow-scope.test.js`:测试贡献模式只在 `openai` flow 内生效,不会污染 Kiro flow 的来源与面板作用域。
- `tests/sidepanel-contribution-update-hint.test.js`:测试顶部贡献更新轻提示在公告、教程和托管 auto-run 提醒并存时的文案合成。
- `tests/sidepanel-flow-source-registry.test.js`:测试 sidepanel 的 flow/source 选择器、Kiro 来源切换与显隐分组接线。
- `tests/grok-runner.test.js`:测试 Grok 注册页执行器对邮箱提交、验证码轮询、资料提交、SSO Cookie 提取、账号记录收尾和完成状态持久化的关键链路。
- `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先冻结当前轮数并刷新贡献内容更新摘要,且刷新失败或启动前状态回灌不会阻塞或改写自动流程启动目标。
- `tests/sidepanel-auto-run-risk-warning.test.js`:测试自动运行在高轮数且未配置节点轮询时会弹出风险提醒。
- `tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js`:测试 Cloudflare Temp Email 独立区域、随机子域名开关、域名列表恢复与提示文案联动。