From 2ea9ad978e97fa184b5298dbf51383a9fd1b0a4b Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sat, 23 May 2026 04:29:37 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=A4=9A=20flow=20=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E8=BD=AE=E8=AF=A2=E5=B9=B6=E6=8E=A5=E5=85=A5=20Grok?= =?UTF-8?q?=20SSO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 229 ++++-- background/flow-mail-polling.js | 354 ++++++++ background/message-router.js | 8 + core/flow-kernel/settings-schema.js | 776 ++++++++++-------- flows/grok/background/register-runner.js | 691 ++++++++++++++++ flows/grok/background/state.js | 374 +++++++++ flows/grok/content/register-page.js | 317 +++++++ flows/grok/index.js | 141 ++++ flows/grok/mail-rules.js | 157 ++++ flows/grok/workflow.js | 98 +++ flows/index.js | 12 +- .../background/desktop-authorize-runner.js | 228 +---- flows/kiro/background/register-runner.js | 231 +----- flows/kiro/mail-rules.js | 162 ++++ manifest.json | 5 + shared/contribution-registry.js | 34 +- sidepanel/sidepanel.html | 20 + sidepanel/sidepanel.js | 172 +++- ...ackground-flow-mail-polling-module.test.js | 148 ++++ tests/background-grok-state-module.test.js | 158 ++++ ...ro-desktop-authorize-runner-module.test.js | 12 + ...ground-kiro-register-runner-module.test.js | 12 + ...ckground-mail-rule-registry-module.test.js | 187 ++++- .../background-message-router-module.test.js | 41 + tests/background-step-registry.test.js | 12 + tests/contribution-registry.test.js | 14 + tests/flow-capabilities-module.test.js | 30 + tests/flow-registry-settings-schema.test.js | 123 ++- tests/grok-runner.test.js | 185 +++++ tests/helpers/script-bundles.js | 2 + tests/sidepanel-flow-source-registry.test.js | 82 ++ tests/sidepanel-icloud-provider.test.js | 1 + tests/source-registry-module.test.js | 64 +- tests/step-definitions-module.test.js | 34 +- 项目完整链路说明.md | 50 +- 项目开发规范(AI协作).md | 3 + 项目文件结构说明.md | 21 +- 37 files changed, 4345 insertions(+), 843 deletions(-) create mode 100644 background/flow-mail-polling.js create mode 100644 flows/grok/background/register-runner.js create mode 100644 flows/grok/background/state.js create mode 100644 flows/grok/content/register-page.js create mode 100644 flows/grok/index.js create mode 100644 flows/grok/mail-rules.js create mode 100644 flows/grok/workflow.js create mode 100644 flows/kiro/mail-rules.js create mode 100644 tests/background-flow-mail-polling-module.test.js create mode 100644 tests/background-grok-state-module.test.js create mode 100644 tests/grok-runner.test.js diff --git a/background.js b/background.js index 7b71a1d..9ad771a 100644 --- a/background.js +++ b/background.js @@ -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, diff --git a/background/flow-mail-polling.js b/background/flow-mail-polling.js new file mode 100644 index 0000000..820115b --- /dev/null +++ b/background/flow-mail-polling.js @@ -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, + }; +}); diff --git a/background/message-router.js b/background/message-router.js index 5de76ed..93003c9 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -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('白嫖复用手机号记录能力未接入。'); diff --git a/core/flow-kernel/settings-schema.js b/core/flow-kernel/settings-schema.js index 8e8afad..8571706 100644 --- a/core/flow-kernel/settings-schema.js +++ b/core/flow-kernel/settings-schema.js @@ -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); diff --git a/flows/grok/background/register-runner.js b/flows/grok/background/register-runner.js new file mode 100644 index 0000000..f4bb501 --- /dev/null +++ b/flows/grok/background/register-runner.js @@ -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, + }; +}); diff --git a/flows/grok/background/state.js b/flows/grok/background/state.js new file mode 100644 index 0000000..8eb5507 --- /dev/null +++ b/flows/grok/background/state.js @@ -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, + }; +}); diff --git a/flows/grok/content/register-page.js b/flows/grok/content/register-page.js new file mode 100644 index 0000000..8d2ea5a --- /dev/null +++ b/flows/grok/content/register-page.js @@ -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, +}; diff --git a/flows/grok/index.js b/flows/grok/index.js new file mode 100644 index 0000000..a06eac8 --- /dev/null +++ b/flows/grok/index.js @@ -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; +}); diff --git a/flows/grok/mail-rules.js b/flows/grok/mail-rules.js new file mode 100644 index 0000000..aa2b501 --- /dev/null +++ b/flows/grok/mail-rules.js @@ -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: '(? 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, + }; +}); diff --git a/flows/grok/workflow.js b/flows/grok/workflow.js new file mode 100644 index 0000000..f872a75 --- /dev/null +++ b/flows/grok/workflow.js @@ -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, + }; +}); diff --git a/flows/index.js b/flows/index.js index 90ff613..3fa9b96 100644 --- a/flows/index.js +++ b/flows/index.js @@ -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)), }; } diff --git a/flows/kiro/background/desktop-authorize-runner.js b/flows/kiro/background/desktop-authorize-runner.js index 0f1f15b..3d7117b 100644 --- a/flows/kiro/background/desktop-authorize-runner.js +++ b/flows/kiro/background/desktop-authorize-runner.js @@ -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 = {}) { diff --git a/flows/kiro/background/register-runner.js b/flows/kiro/background/register-runner.js index 521f29c..43098c1 100644 --- a/flows/kiro/background/register-runner.js +++ b/flows/kiro/background/register-runner.js @@ -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 = {}) { diff --git a/flows/kiro/mail-rules.js b/flows/kiro/mail-rules.js new file mode 100644 index 0000000..5eac02b --- /dev/null +++ b/flows/kiro/mail-rules.js @@ -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, + }; +}); diff --git a/manifest.json b/manifest.json index 49d8706..f63369f 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/shared/contribution-registry.js b/shared/contribution-registry.js index 9ea6610..b36abbe 100644 --- a/shared/contribution-registry.js +++ b/shared/contribution-registry.js @@ -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, diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index ccc80e7..6b0c79e 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -126,6 +126,7 @@