合并 Grok SSO 多 flow 重构
吸收 Grok 注册与 webchat2api SSO Cookie 导出流程,并把 Kiro/Grok 验证码轮询统一到共享 flow 邮件轮询服务。保留 master 上 Kiro 页面恢复、active verify-otp 接管和重注入修复,补齐对应测试适配。
This commit is contained in:
+162
-67
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3826,20 +3837,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;
|
||||
@@ -3958,6 +3969,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: {
|
||||
@@ -3973,28 +4003,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);
|
||||
@@ -4038,6 +4047,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;
|
||||
}
|
||||
@@ -4346,6 +4358,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',
|
||||
@@ -9628,6 +9662,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,
|
||||
@@ -10822,6 +10864,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;
|
||||
@@ -10879,6 +10931,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',
|
||||
@@ -13390,8 +13447,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,
|
||||
@@ -13449,12 +13507,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,
|
||||
@@ -13823,33 +13919,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,
|
||||
@@ -13857,6 +13938,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,
|
||||
@@ -13888,31 +13992,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,
|
||||
@@ -14021,6 +14110,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,
|
||||
@@ -14037,6 +14131,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
|
||||
clearAutoRunTimerAlarm,
|
||||
clearFreeReusablePhoneActivation,
|
||||
clearGrokSsoCookies,
|
||||
clearLuckmailRuntimeState,
|
||||
clearYydsMailRuntimeState,
|
||||
clearStopRequest,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
deleteAccountRunHistoryRecords,
|
||||
clearAutoRunTimerAlarm,
|
||||
clearFreeReusablePhoneActivation,
|
||||
clearGrokSsoCookies,
|
||||
clearLuckmailRuntimeState,
|
||||
clearYydsMailRuntimeState,
|
||||
clearStopRequest,
|
||||
@@ -1148,6 +1149,13 @@
|
||||
return await clearFreeReusablePhoneActivation();
|
||||
}
|
||||
|
||||
case 'CLEAR_GROK_SSO_COOKIES': {
|
||||
if (typeof clearGrokSsoCookies !== 'function') {
|
||||
throw new Error('Grok SSO 清空能力未接入。');
|
||||
}
|
||||
return await clearGrokSsoCookies();
|
||||
}
|
||||
|
||||
case 'SET_FREE_REUSABLE_PHONE': {
|
||||
if (typeof setFreeReusablePhoneActivation !== 'function') {
|
||||
throw new Error('白嫖复用手机号记录能力未接入。');
|
||||
|
||||
+441
-335
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
(function attachGrokMailRules(root, factory) {
|
||||
root.MultiPageGrokMailRules = factory(root);
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createGrokMailRulesModule(root) {
|
||||
const grokStateApi = root.MultiPageBackgroundGrokState || null;
|
||||
const SUBMIT_VERIFICATION_CODE_RULE_ID = 'grok-submit-verification-code';
|
||||
const SUBMIT_VERIFICATION_CODE_NODE_ID = 'grok-submit-verification-code';
|
||||
const GROK_VERIFICATION_CODE_PATTERNS = Object.freeze([
|
||||
Object.freeze({
|
||||
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
|
||||
flags: 'gi',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[::\\s]*(\\d{6})',
|
||||
flags: 'gi',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '(?:验证码|代码|确认码)[::\\s为]+(\\d{6})',
|
||||
flags: 'gi',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '(?<!#)\\b(\\d{6})\\b',
|
||||
flags: 'g',
|
||||
}),
|
||||
]);
|
||||
const GROK_SENDER_FILTERS = Object.freeze([
|
||||
'x.ai',
|
||||
'xai',
|
||||
'grok',
|
||||
]);
|
||||
const GROK_SUBJECT_FILTERS = Object.freeze([
|
||||
'xai',
|
||||
'x.ai',
|
||||
'grok',
|
||||
'verification',
|
||||
'confirmation',
|
||||
'code',
|
||||
'验证码',
|
||||
'确认码',
|
||||
]);
|
||||
const GROK_REQUIRED_KEYWORDS = Object.freeze([
|
||||
'xai',
|
||||
'x.ai',
|
||||
'grok',
|
||||
'verification',
|
||||
'confirmation',
|
||||
'code',
|
||||
'验证码',
|
||||
'确认码',
|
||||
]);
|
||||
|
||||
function cleanString(value = '') {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function readGrokRuntime(state = {}) {
|
||||
if (typeof grokStateApi?.ensureRuntimeState === 'function') {
|
||||
return grokStateApi.ensureRuntimeState(state);
|
||||
}
|
||||
return state?.runtimeState?.flowState?.grok || state?.flowState?.grok || {};
|
||||
}
|
||||
|
||||
function buildTargetEmailHints(targetEmail = '') {
|
||||
const normalizedTarget = cleanString(targetEmail).toLowerCase();
|
||||
return normalizedTarget ? [normalizedTarget] : [];
|
||||
}
|
||||
|
||||
function getVisibleStep(state = {}) {
|
||||
const explicitStep = Number(state?.visibleStep || state?.step);
|
||||
return Number.isInteger(explicitStep) && explicitStep > 0 ? explicitStep : 3;
|
||||
}
|
||||
|
||||
function isMail2925Provider(state = {}) {
|
||||
return cleanString(state?.mailProvider).toLowerCase() === '2925';
|
||||
}
|
||||
|
||||
function shouldMatchMail2925TargetEmail(state = {}) {
|
||||
return isMail2925Provider(state)
|
||||
&& cleanString(state?.mail2925Mode).toLowerCase() === 'receive';
|
||||
}
|
||||
|
||||
function createGrokMailRules(deps = {}) {
|
||||
const {
|
||||
LUCKMAIL_PROVIDER = 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
|
||||
} = deps;
|
||||
|
||||
function getRuleDefinition(_input, state = {}) {
|
||||
const runtimeState = readGrokRuntime(state);
|
||||
const targetEmail = cleanString(runtimeState.register?.email || state?.grokEmail || state?.email).toLowerCase();
|
||||
const normalizedProvider = cleanString(state?.mailProvider).toLowerCase();
|
||||
const mail2925Provider = isMail2925Provider(state);
|
||||
const luckmailProvider = normalizedProvider === cleanString(LUCKMAIL_PROVIDER).toLowerCase();
|
||||
|
||||
return {
|
||||
flowId: 'grok',
|
||||
ruleId: SUBMIT_VERIFICATION_CODE_RULE_ID,
|
||||
nodeId: SUBMIT_VERIFICATION_CODE_NODE_ID,
|
||||
step: getVisibleStep(state),
|
||||
artifactType: 'code',
|
||||
codePatterns: GROK_VERIFICATION_CODE_PATTERNS,
|
||||
filterAfterTimestamp: 0,
|
||||
requiredKeywords: GROK_REQUIRED_KEYWORDS,
|
||||
senderFilters: GROK_SENDER_FILTERS,
|
||||
subjectFilters: GROK_SUBJECT_FILTERS,
|
||||
targetEmail,
|
||||
targetEmailHints: buildTargetEmailHints(targetEmail),
|
||||
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
|
||||
maxAttempts: luckmailProvider
|
||||
? 3
|
||||
: (mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5),
|
||||
intervalMs: luckmailProvider
|
||||
? 15000
|
||||
: (mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 5000),
|
||||
};
|
||||
}
|
||||
|
||||
function getRuleDefinitionForNode(nodeId, state = {}) {
|
||||
const normalizedNodeId = cleanString(nodeId);
|
||||
if (normalizedNodeId && normalizedNodeId !== SUBMIT_VERIFICATION_CODE_NODE_ID) {
|
||||
throw new Error(`Grok 邮件规则不支持节点:${normalizedNodeId}`);
|
||||
}
|
||||
return getRuleDefinition({ nodeId: SUBMIT_VERIFICATION_CODE_NODE_ID }, state);
|
||||
}
|
||||
|
||||
function buildVerificationPollPayload(input, state = {}, overrides = {}) {
|
||||
return {
|
||||
...getRuleDefinition(input, state),
|
||||
...(overrides || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
|
||||
return {
|
||||
...getRuleDefinitionForNode(nodeId, state),
|
||||
...(overrides || {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buildVerificationPollPayload,
|
||||
buildVerificationPollPayloadForNode,
|
||||
getRuleDefinition,
|
||||
getRuleDefinitionForNode,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
GROK_REQUIRED_KEYWORDS,
|
||||
GROK_SENDER_FILTERS,
|
||||
GROK_SUBJECT_FILTERS,
|
||||
GROK_VERIFICATION_CODE_PATTERNS,
|
||||
SUBMIT_VERIFICATION_CODE_NODE_ID,
|
||||
SUBMIT_VERIFICATION_CODE_RULE_ID,
|
||||
createGrokMailRules,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
(function attachMultiPageGrokWorkflow(root, factory) {
|
||||
root.MultiPageGrokWorkflow = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createMultiPageGrokWorkflow() {
|
||||
function freezeDeep(entry) {
|
||||
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
|
||||
return entry;
|
||||
}
|
||||
Object.getOwnPropertyNames(entry).forEach((key) => {
|
||||
freezeDeep(entry[key]);
|
||||
});
|
||||
return Object.freeze(entry);
|
||||
}
|
||||
|
||||
const STEP_VARIANTS = freezeDeep({
|
||||
default: [
|
||||
{
|
||||
id: 1,
|
||||
order: 10,
|
||||
key: 'grok-open-signup-page',
|
||||
title: '打开 Grok 注册页',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-open-signup-page',
|
||||
flowId: 'grok',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order: 20,
|
||||
key: 'grok-submit-email',
|
||||
title: '获取邮箱并继续',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-submit-email',
|
||||
flowId: 'grok',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
order: 30,
|
||||
key: 'grok-submit-verification-code',
|
||||
title: '获取验证码并继续',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-submit-verification-code',
|
||||
mailRuleId: 'grok-submit-verification-code',
|
||||
flowId: 'grok',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
order: 40,
|
||||
key: 'grok-submit-profile',
|
||||
title: '填写资料并继续',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-submit-profile',
|
||||
flowId: 'grok',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
order: 50,
|
||||
key: 'grok-extract-sso-cookie',
|
||||
title: '提取 SSO Cookie',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-extract-sso-cookie',
|
||||
flowId: 'grok',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function getVariantStepDefinitions(variantKey = 'default') {
|
||||
return Array.isArray(STEP_VARIANTS[variantKey]) ? STEP_VARIANTS[variantKey] : STEP_VARIANTS.default;
|
||||
}
|
||||
|
||||
function getModeStepDefinitions() {
|
||||
return getVariantStepDefinitions('default');
|
||||
}
|
||||
|
||||
function getAllSteps() {
|
||||
return getVariantStepDefinitions('default');
|
||||
}
|
||||
|
||||
function getPlusPaymentStepTitle() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveStepTitle(step = {}) {
|
||||
return step?.title || '';
|
||||
}
|
||||
|
||||
return {
|
||||
flowId: 'grok',
|
||||
getAllSteps,
|
||||
getModeStepDefinitions,
|
||||
getPlusPaymentStepTitle,
|
||||
getVariantStepDefinitions,
|
||||
resolveStepTitle,
|
||||
};
|
||||
});
|
||||
+10
-2
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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) => {
|
||||
@@ -954,173 +903,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 targetEmail = resolveKiroRegisterEmail(state);
|
||||
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 = {}) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
<select id="select-flow" class="data-select" aria-label="Flow 选择">
|
||||
<option value="openai" selected>Codex / OpenAI</option>
|
||||
<option value="kiro">Kiro</option>
|
||||
<option value="grok">Grok</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
@@ -290,6 +291,23 @@
|
||||
<span class="data-label">上传状态</span>
|
||||
<span id="display-kiro-upload-status" class="data-value mono">未开始</span>
|
||||
</div>
|
||||
<div class="data-row" id="row-grok-register-status" style="display:none;">
|
||||
<span class="data-label">Grok 注册</span>
|
||||
<span id="display-grok-register-status" class="data-value mono">未开始</span>
|
||||
</div>
|
||||
<div class="data-row" id="row-grok-sso-status" style="display:none;">
|
||||
<span class="data-label">SSO 状态</span>
|
||||
<span id="display-grok-sso-status" class="data-value mono">未提取</span>
|
||||
</div>
|
||||
<div class="data-row" id="row-grok-sso-settings" style="display:none;">
|
||||
<span class="data-label">SSO Cookie</span>
|
||||
<div class="data-inline">
|
||||
<span id="display-grok-sso-cookie" class="data-value mono">未提取</span>
|
||||
<button id="btn-copy-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">复制</button>
|
||||
<button id="btn-export-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">导出</button>
|
||||
<button id="btn-clear-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-custom-password">
|
||||
<span class="data-label">账户密码</span>
|
||||
<div class="input-with-icon">
|
||||
@@ -1881,6 +1899,8 @@
|
||||
<script src="../flows/openai/workflow.js"></script>
|
||||
<script src="../flows/kiro/index.js"></script>
|
||||
<script src="../flows/kiro/workflow.js"></script>
|
||||
<script src="../flows/grok/index.js"></script>
|
||||
<script src="../flows/grok/workflow.js"></script>
|
||||
<script src="../flows/index.js"></script>
|
||||
<script src="../core/flow-kernel/flow-registry.js"></script>
|
||||
<script src="../shared/contribution-registry.js"></script>
|
||||
|
||||
+171
-1
@@ -192,6 +192,15 @@ const rowKiroLoginUrl = document.getElementById('row-kiro-login-url');
|
||||
const displayKiroLoginUrl = document.getElementById('display-kiro-login-url');
|
||||
const rowKiroUploadStatus = document.getElementById('row-kiro-upload-status');
|
||||
const displayKiroUploadStatus = document.getElementById('display-kiro-upload-status');
|
||||
const rowGrokRegisterStatus = document.getElementById('row-grok-register-status');
|
||||
const displayGrokRegisterStatus = document.getElementById('display-grok-register-status');
|
||||
const rowGrokSsoStatus = document.getElementById('row-grok-sso-status');
|
||||
const displayGrokSsoStatus = document.getElementById('display-grok-sso-status');
|
||||
const rowGrokSsoSettings = document.getElementById('row-grok-sso-settings');
|
||||
const displayGrokSsoCookie = document.getElementById('display-grok-sso-cookie');
|
||||
const btnCopyGrokSso = document.getElementById('btn-copy-grok-sso');
|
||||
const btnExportGrokSso = document.getElementById('btn-export-grok-sso');
|
||||
const btnClearGrokSso = document.getElementById('btn-clear-grok-sso');
|
||||
const rowCustomPassword = document.getElementById('row-custom-password');
|
||||
const rowPlusMode = document.getElementById('row-plus-mode');
|
||||
const inputPlusModeEnabled = document.getElementById('input-plus-mode-enabled');
|
||||
@@ -2661,6 +2670,95 @@ function getKiroRuntimeState(state = {}) {
|
||||
return {};
|
||||
}
|
||||
|
||||
function getGrokRuntimeState(state = {}) {
|
||||
const runtimeState = state?.runtimeState?.flowState?.grok;
|
||||
if (runtimeState && typeof runtimeState === 'object' && !Array.isArray(runtimeState)) {
|
||||
return runtimeState;
|
||||
}
|
||||
const flowState = state?.flowState?.grok;
|
||||
if (flowState && typeof flowState === 'object' && !Array.isArray(flowState)) {
|
||||
return flowState;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function normalizeGrokSsoCookies(state = latestState) {
|
||||
const runtimeState = getGrokRuntimeState(state);
|
||||
const rawCookies = Array.isArray(runtimeState?.sso?.cookies)
|
||||
? runtimeState.sso.cookies
|
||||
: (Array.isArray(state?.grokSsoCookies) ? state.grokSsoCookies : []);
|
||||
const currentCookie = String(runtimeState?.sso?.currentCookie || state?.grokSsoCookie || '').trim();
|
||||
return Array.from(new Set([
|
||||
currentCookie,
|
||||
...rawCookies,
|
||||
].map((entry) => String(entry || '').trim()).filter(Boolean)));
|
||||
}
|
||||
|
||||
function getGrokRegisterStatusLabel(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'signup_page_opened':
|
||||
return '注册页已打开';
|
||||
case 'email_submitting':
|
||||
return '正在提交邮箱';
|
||||
case 'verification_requested':
|
||||
return '等待验证码';
|
||||
case 'verified':
|
||||
return '验证码已提交';
|
||||
case 'profile_submitting':
|
||||
return '正在提交资料';
|
||||
case 'profile_submitted':
|
||||
return '资料已提交';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'error':
|
||||
return '失败';
|
||||
default:
|
||||
return String(value || '').trim() || '未开始';
|
||||
}
|
||||
}
|
||||
|
||||
function renderGrokRuntimeState(state = latestState) {
|
||||
const runtimeState = getGrokRuntimeState(state);
|
||||
const registerStatus = String(
|
||||
runtimeState?.register?.status
|
||||
|| state?.grokRegisterStatus
|
||||
|| ''
|
||||
).trim();
|
||||
const cookies = normalizeGrokSsoCookies(state);
|
||||
const currentCookie = String(
|
||||
runtimeState?.sso?.currentCookie
|
||||
|| state?.grokSsoCookie
|
||||
|| cookies[0]
|
||||
|| ''
|
||||
).trim();
|
||||
const extractedAt = Number(
|
||||
runtimeState?.sso?.extractedAt
|
||||
|| state?.grokSsoExtractedAt
|
||||
|| 0
|
||||
) || 0;
|
||||
|
||||
if (displayGrokRegisterStatus) {
|
||||
displayGrokRegisterStatus.textContent = getGrokRegisterStatusLabel(registerStatus);
|
||||
}
|
||||
if (displayGrokSsoStatus) {
|
||||
displayGrokSsoStatus.textContent = currentCookie
|
||||
? `已提取 ${cookies.length || 1} 条${extractedAt ? `,${new Date(extractedAt).toLocaleString()}` : ''}`
|
||||
: '未提取';
|
||||
}
|
||||
if (displayGrokSsoCookie) {
|
||||
displayGrokSsoCookie.textContent = currentCookie
|
||||
? `${currentCookie.slice(0, 8)}...${currentCookie.slice(-6)}`
|
||||
: '未提取';
|
||||
displayGrokSsoCookie.title = currentCookie ? '已隐藏完整 SSO Cookie,可使用复制或导出' : '';
|
||||
}
|
||||
[btnCopyGrokSso, btnExportGrokSso, btnClearGrokSso].forEach((button) => {
|
||||
if (button) {
|
||||
button.disabled = cookies.length === 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setKiroRsConnectionTestStatus(message = '') {
|
||||
const nextText = String(message || '').trim() || '未测试';
|
||||
kiroRsConnectionTestStatusText = nextText;
|
||||
@@ -11069,6 +11167,7 @@ function applySettingsState(state) {
|
||||
).trim();
|
||||
displayKiroUploadStatus.textContent = getKiroUploadStatusLabel(kiroUploadStatus);
|
||||
}
|
||||
renderGrokRuntimeState(state);
|
||||
const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService);
|
||||
const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function'
|
||||
? normalizeIpProxyServiceProfiles(state?.ipProxyServiceProfiles || {}, state || {})
|
||||
@@ -13944,6 +14043,64 @@ async function copyTextToClipboard(text) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
}
|
||||
|
||||
btnCopyGrokSso?.addEventListener('click', async () => {
|
||||
try {
|
||||
const cookies = normalizeGrokSsoCookies(latestState);
|
||||
await copyTextToClipboard(cookies.join('\n'));
|
||||
showToast('Grok SSO Cookie 已复制。', 'success');
|
||||
} catch (error) {
|
||||
showToast(error?.message || '复制 Grok SSO Cookie 失败。', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
btnExportGrokSso?.addEventListener('click', () => {
|
||||
try {
|
||||
const cookies = normalizeGrokSsoCookies(latestState);
|
||||
if (!cookies.length) {
|
||||
throw new Error('没有可导出的 Grok SSO Cookie。');
|
||||
}
|
||||
downloadTextFile(
|
||||
`${cookies.join('\n')}\n`,
|
||||
`grok-sso-cookies-${new Date().toISOString().replace(/[:.]/g, '-')}.txt`,
|
||||
'text/plain;charset=utf-8'
|
||||
);
|
||||
showToast('Grok SSO Cookie 已导出。', 'success');
|
||||
} catch (error) {
|
||||
showToast(error?.message || '导出 Grok SSO Cookie 失败。', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
btnClearGrokSso?.addEventListener('click', async () => {
|
||||
try {
|
||||
const cookies = normalizeGrokSsoCookies(latestState);
|
||||
if (!cookies.length) {
|
||||
showToast('当前没有 Grok SSO Cookie。', 'info');
|
||||
return;
|
||||
}
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'CLEAR_GROK_SSO_COOKIES',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (response?.state) {
|
||||
syncLatestState(response.state);
|
||||
} else {
|
||||
syncLatestState({
|
||||
grokSsoCookie: '',
|
||||
grokSsoCookies: [],
|
||||
grokSsoExtractedAt: 0,
|
||||
});
|
||||
}
|
||||
renderGrokRuntimeState(latestState);
|
||||
showToast('Grok SSO Cookie 已清空。', 'success');
|
||||
} catch (error) {
|
||||
showToast(error?.message || '清空 Grok SSO Cookie 失败。', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
const hotmailManager = window.SidepanelHotmailManager?.createHotmailManager({
|
||||
state: {
|
||||
getLatestState: () => latestState,
|
||||
@@ -14590,7 +14747,9 @@ stepsList?.addEventListener('click', async (event) => {
|
||||
if (step === gpcCreateStep && !(await ensureGpcApiKeyReadyForStart())) {
|
||||
return;
|
||||
}
|
||||
const shouldPersistSharedPassword = nodeId === 'fill-password' || nodeId === 'kiro-submit-password';
|
||||
const shouldPersistSharedPassword = nodeId === 'fill-password'
|
||||
|| nodeId === 'kiro-submit-password'
|
||||
|| nodeId === 'grok-submit-profile';
|
||||
if (shouldPersistSharedPassword && inputPassword.value !== (latestState?.customPassword || '')) {
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
@@ -17331,6 +17490,17 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
) {
|
||||
syncPasswordField(latestState || {});
|
||||
}
|
||||
if (
|
||||
message.payload.grokRegisterStatus !== undefined
|
||||
|| message.payload.grokCompletedAt !== undefined
|
||||
|| message.payload.grokSsoCookie !== undefined
|
||||
|| message.payload.grokSsoCookies !== undefined
|
||||
|| message.payload.grokSsoExtractedAt !== undefined
|
||||
|| message.payload.runtimeState !== undefined
|
||||
|| message.payload.flowState !== undefined
|
||||
) {
|
||||
renderGrokRuntimeState(latestState);
|
||||
}
|
||||
if (message.payload.localCpaStep9Mode !== undefined) {
|
||||
setLocalCpaStep9Mode(message.payload.localCpaStep9Mode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadFlowMailPollingApi() {
|
||||
const source = fs.readFileSync('background/flow-mail-polling.js', 'utf8');
|
||||
const globalScope = {};
|
||||
new Function('self', `${source}; return self;`)(globalScope);
|
||||
return globalScope.MultiPageBackgroundFlowMailPolling;
|
||||
}
|
||||
|
||||
test('flow mail polling service dispatches API mail providers through shared helper', async () => {
|
||||
const api = loadFlowMailPollingApi();
|
||||
const logs = [];
|
||||
let buildCall = null;
|
||||
let hotmailCall = null;
|
||||
const service = api.createFlowMailPollingService({
|
||||
addLog: async (message, level, options) => {
|
||||
logs.push({ message, level, options });
|
||||
},
|
||||
buildVerificationPollPayloadForNode: (nodeId, state, overrides) => {
|
||||
buildCall = { nodeId, state, overrides };
|
||||
return {
|
||||
flowId: state.activeFlowId,
|
||||
nodeId,
|
||||
step: 4,
|
||||
targetEmail: 'user@example.com',
|
||||
maxAttempts: 2,
|
||||
intervalMs: 100,
|
||||
...overrides,
|
||||
};
|
||||
},
|
||||
getMailConfig: () => ({ provider: 'hotmail-api', label: 'Hotmail' }),
|
||||
pollHotmailVerificationCode: async (step, state, payload) => {
|
||||
hotmailCall = { step, state, payload };
|
||||
return { code: '123456', emailTimestamp: 456 };
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.pollFlowVerificationCode({
|
||||
flowId: 'kiro',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
state: { activeFlowId: 'kiro', email: 'user@example.com' },
|
||||
step: 4,
|
||||
filterAfterTimestamp: 123,
|
||||
logStepKey: 'kiro-submit-verification-code',
|
||||
});
|
||||
|
||||
assert.equal(result.code, '123456');
|
||||
assert.equal(buildCall.nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(buildCall.overrides.filterAfterTimestamp, 123);
|
||||
assert.equal(hotmailCall.step, 4);
|
||||
assert.equal(hotmailCall.payload.filterAfterTimestamp, 123);
|
||||
assert.equal(logs.some((entry) => entry.message.includes('Hotmail')), true);
|
||||
});
|
||||
|
||||
test('flow mail polling service prepares browser mail provider sessions and payload timeouts', async () => {
|
||||
const api = loadFlowMailPollingApi();
|
||||
let ensured2925 = null;
|
||||
let mailMessage = null;
|
||||
let mailOptions = null;
|
||||
const service = api.createFlowMailPollingService({
|
||||
addLog: async () => {},
|
||||
buildVerificationPollPayloadForNode: () => ({
|
||||
flowId: 'kiro',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
step: 4,
|
||||
targetEmail: 'user@example.com',
|
||||
maxAttempts: 2,
|
||||
intervalMs: 100,
|
||||
}),
|
||||
ensureMail2925MailboxSession: async (options) => {
|
||||
ensured2925 = options;
|
||||
},
|
||||
getMailConfig: () => ({
|
||||
provider: '2925',
|
||||
source: 'mail-2925',
|
||||
label: '2925 邮箱',
|
||||
}),
|
||||
sendToMailContentScriptResilient: async (_mail, message, options) => {
|
||||
mailMessage = message;
|
||||
mailOptions = options;
|
||||
return { code: '654321', emailTimestamp: 789 };
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.pollFlowVerificationCode({
|
||||
flowId: 'kiro',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
state: {
|
||||
activeFlowId: 'kiro',
|
||||
currentMail2925AccountId: 'acct-1',
|
||||
mail2925UseAccountPool: true,
|
||||
mail2925Accounts: [
|
||||
{ id: 'acct-1', email: 'pool@example.com' },
|
||||
],
|
||||
},
|
||||
step: 4,
|
||||
logStepKey: 'kiro-submit-verification-code',
|
||||
});
|
||||
|
||||
assert.equal(result.code, '654321');
|
||||
assert.equal(ensured2925.accountId, 'acct-1');
|
||||
assert.equal(ensured2925.expectedMailboxEmail, 'pool@example.com');
|
||||
assert.equal(mailMessage.type, 'POLL_EMAIL');
|
||||
assert.equal(mailMessage.payload.targetEmail, 'user@example.com');
|
||||
assert.equal(mailOptions.logStepKey, 'kiro-submit-verification-code');
|
||||
assert.equal(mailOptions.responseTimeoutMs, 45000);
|
||||
});
|
||||
|
||||
test('flow mail polling service lets 2925 limit errors flow through shared recovery', async () => {
|
||||
const api = loadFlowMailPollingApi();
|
||||
const limitError = new Error('MAIL2925_LIMIT_REACHED::子邮箱已达上限');
|
||||
const recoveredError = new Error('switched-account');
|
||||
const service = api.createFlowMailPollingService({
|
||||
addLog: async () => {},
|
||||
buildVerificationPollPayloadForNode: () => ({
|
||||
step: 4,
|
||||
maxAttempts: 1,
|
||||
intervalMs: 100,
|
||||
}),
|
||||
getMailConfig: () => ({
|
||||
provider: '2925',
|
||||
source: 'mail-2925',
|
||||
label: '2925 邮箱',
|
||||
}),
|
||||
ensureMail2925MailboxSession: async () => {},
|
||||
isMail2925LimitReachedError: (error) => error === limitError,
|
||||
handleMail2925LimitReachedError: async (step, error) => {
|
||||
assert.equal(step, 4);
|
||||
assert.equal(error, limitError);
|
||||
return recoveredError;
|
||||
},
|
||||
sendToMailContentScriptResilient: async () => {
|
||||
throw limitError;
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => service.pollFlowVerificationCode({
|
||||
flowId: 'kiro',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
state: { activeFlowId: 'kiro' },
|
||||
step: 4,
|
||||
}),
|
||||
/switched-account/
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadGrokStateApi() {
|
||||
const source = fs.readFileSync('flows/grok/background/state.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundGrokState;`)(globalScope);
|
||||
}
|
||||
|
||||
test('grok state view projects canonical runtime state into legacy flat read fields', () => {
|
||||
const api = loadGrokStateApi();
|
||||
const view = api.buildStateView({
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
openai: {
|
||||
preserved: true,
|
||||
},
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 42,
|
||||
pageState: 'profile_entry',
|
||||
pageUrl: 'https://accounts.x.ai/sign-up',
|
||||
},
|
||||
register: {
|
||||
email: 'USER@EXAMPLE.COM',
|
||||
firstName: 'Ada',
|
||||
lastName: 'Lovelace',
|
||||
password: 'Secret123!',
|
||||
verificationRequestedAt: 1000,
|
||||
verificationCode: 'ABC123',
|
||||
status: 'verified',
|
||||
completedAt: 2000,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: 'cookie-a',
|
||||
cookies: ['cookie-a', 'cookie-b', 'cookie-a'],
|
||||
extractedAt: 3000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(view.grokRegisterTabId, 42);
|
||||
assert.equal(view.grokPageState, 'profile_entry');
|
||||
assert.equal(view.grokEmail, 'user@example.com');
|
||||
assert.equal(view.grokFirstName, 'Ada');
|
||||
assert.equal(view.grokLastName, 'Lovelace');
|
||||
assert.equal(view.grokPassword, 'Secret123!');
|
||||
assert.equal(view.grokVerificationRequestedAt, 1000);
|
||||
assert.equal(view.grokVerificationCode, 'ABC123');
|
||||
assert.equal(view.grokRegisterStatus, 'verified');
|
||||
assert.equal(view.grokCompletedAt, 2000);
|
||||
assert.equal(view.grokSsoCookie, 'cookie-a');
|
||||
assert.deepEqual(view.grokSsoCookies, ['cookie-a', 'cookie-b']);
|
||||
assert.equal(view.grokSsoExtractedAt, 3000);
|
||||
assert.equal(view.runtimeState.flowState.openai.preserved, true);
|
||||
assert.equal(view.runtimeState.flowState.grok.register.email, 'user@example.com');
|
||||
assert.equal(view.flowState.grok.sso.currentCookie, 'cookie-a');
|
||||
assert.equal(view.flows.grok.sso.cookies.length, 2);
|
||||
});
|
||||
|
||||
test('grok completion payloads update canonical runtime state and flat compatibility fields', () => {
|
||||
const api = loadGrokStateApi();
|
||||
const patch = api.applyNodeCompletionPayload({}, {
|
||||
grokEmail: 'GROK@EXAMPLE.COM',
|
||||
grokVerificationRequestedAt: 123,
|
||||
grokSsoCookie: 'cookie-z',
|
||||
grokSsoCookies: ['cookie-z', 'cookie-z', 'cookie-y'],
|
||||
grokCompletedAt: 456,
|
||||
});
|
||||
|
||||
assert.equal(patch.grokEmail, 'grok@example.com');
|
||||
assert.equal(patch.grokVerificationRequestedAt, 123);
|
||||
assert.equal(patch.grokSsoCookie, 'cookie-z');
|
||||
assert.deepEqual(patch.grokSsoCookies, ['cookie-z', 'cookie-y']);
|
||||
assert.equal(patch.grokCompletedAt, 456);
|
||||
assert.equal(patch.grokSsoExtractedAt, 456);
|
||||
assert.equal(patch.runtimeState.flowState.grok.register.email, 'grok@example.com');
|
||||
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-z');
|
||||
});
|
||||
|
||||
test('grok fresh keep-state reset preserves SSO cookies but clears registration runtime', () => {
|
||||
const api = loadGrokStateApi();
|
||||
const patch = api.buildFreshKeepState({
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 42,
|
||||
pageState: 'profile_entry',
|
||||
},
|
||||
register: {
|
||||
email: 'grok@example.com',
|
||||
status: 'completed',
|
||||
completedAt: 1000,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: 'cookie-a',
|
||||
cookies: ['cookie-a', 'cookie-b'],
|
||||
extractedAt: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(patch.grokRegisterTabId, null);
|
||||
assert.equal(patch.grokPageState, '');
|
||||
assert.equal(patch.grokEmail, '');
|
||||
assert.equal(patch.grokRegisterStatus, '');
|
||||
assert.equal(patch.grokCompletedAt, 0);
|
||||
assert.equal(patch.grokSsoCookie, 'cookie-a');
|
||||
assert.deepEqual(patch.grokSsoCookies, ['cookie-a', 'cookie-b']);
|
||||
assert.equal(patch.grokSsoExtractedAt, 2000);
|
||||
assert.equal(patch.runtimeState.flowState.grok.register.email, '');
|
||||
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-a');
|
||||
});
|
||||
|
||||
test('grok downstream reset clears only the state owned by the restarted tail', () => {
|
||||
const api = loadGrokStateApi();
|
||||
const currentState = {
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 7,
|
||||
pageState: 'signed_in',
|
||||
pageUrl: 'https://grok.com/',
|
||||
lastError: 'old-error',
|
||||
},
|
||||
register: {
|
||||
email: 'grok@example.com',
|
||||
status: 'completed',
|
||||
completedAt: 1000,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: 'cookie-a',
|
||||
cookies: ['cookie-a'],
|
||||
extractedAt: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const profilePatch = api.buildDownstreamResetPatch('grok-submit-profile', currentState);
|
||||
assert.equal(profilePatch.grokEmail, 'grok@example.com');
|
||||
assert.equal(profilePatch.grokRegisterStatus, 'completed');
|
||||
assert.equal(profilePatch.grokSsoCookie, '');
|
||||
assert.deepEqual(profilePatch.grokSsoCookies, []);
|
||||
|
||||
const emailPatch = api.buildDownstreamResetPatch('grok-submit-email', currentState);
|
||||
assert.equal(emailPatch.grokEmail, '');
|
||||
assert.equal(emailPatch.grokRegisterStatus, '');
|
||||
assert.equal(emailPatch.grokRegisterTabId, 7);
|
||||
});
|
||||
@@ -91,6 +91,18 @@ test('kiro desktop authorize runner uses a shared 3-minute page-load timeout bud
|
||||
assert.match(source, /finalizeDesktopAuthorizeCallback/);
|
||||
});
|
||||
|
||||
test('kiro desktop authorize runner delegates OTP mail polling to the shared flow mail service', () => {
|
||||
const source = fs.readFileSync('flows/kiro/background/desktop-authorize-runner.js', 'utf8');
|
||||
assert.match(source, /pollFlowVerificationCode/);
|
||||
assert.doesNotMatch(source, /buildDesktopOtpPollPayload/);
|
||||
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
|
||||
});
|
||||
|
||||
test('background wires the Kiro register injector into desktop authorization runner', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const start = source.indexOf('const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({');
|
||||
|
||||
@@ -37,6 +37,18 @@ test('kiro register runner uses a shared 3-minute page-load timeout budget', ()
|
||||
assert.match(source, /onRetryableError: buildKiroRetryRecovery\(tabId, \{\s*\.\.\.options,\s*timeoutBudget,/);
|
||||
});
|
||||
|
||||
test('kiro register runner delegates verification mail polling to the shared flow mail service', () => {
|
||||
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
|
||||
assert.match(source, /pollFlowVerificationCode/);
|
||||
assert.doesNotMatch(source, /buildKiroVerificationPollPayload/);
|
||||
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
|
||||
});
|
||||
|
||||
test('kiro register consent step treats Kiro Web signed-in page as completion', () => {
|
||||
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
|
||||
assert.match(source, /readKiroRegisterPageState\(tabId, \{/);
|
||||
@@ -231,8 +243,8 @@ test('kiro verification polling uses the registration email field instead of pag
|
||||
getState: async () => currentState,
|
||||
getTabId: async () => 103,
|
||||
isTabAlive: async () => true,
|
||||
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
|
||||
pollPayloads.push(payload);
|
||||
pollFlowVerificationCode: async (options) => {
|
||||
pollPayloads.push(options);
|
||||
return { code: '123456', emailTimestamp: 2000, mailId: 'mail-1' };
|
||||
},
|
||||
sendToContentScriptResilient: async (_sourceId, message) => {
|
||||
@@ -266,8 +278,11 @@ test('kiro verification polling uses the registration email field instead of pag
|
||||
});
|
||||
|
||||
assert.equal(pollPayloads.length, 1);
|
||||
assert.equal(pollPayloads[0].targetEmail, 'skater-twine-carve@duck.com');
|
||||
assert.deepEqual(pollPayloads[0].targetEmailHints, ['skater-twine-carve@duck.com']);
|
||||
assert.equal(pollPayloads[0].flowId, 'kiro');
|
||||
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(pollPayloads[0].filterAfterTimestamp, 1000);
|
||||
assert.equal(pollPayloads[0].state.email, 'skater-twine-carve@duck.com');
|
||||
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'skater-twine-carve@duck.com');
|
||||
assert.equal(sentMessages.some((message) => (
|
||||
message.type === 'EXECUTE_NODE'
|
||||
&& message.nodeId === 'kiro-submit-verification-code'
|
||||
@@ -315,8 +330,8 @@ test('kiro verification step can adopt the active AWS verify-otp page without st
|
||||
getState: async () => currentState,
|
||||
getTabId: async () => null,
|
||||
isTabAlive: async () => false,
|
||||
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
|
||||
pollPayloads.push(payload);
|
||||
pollFlowVerificationCode: async (options) => {
|
||||
pollPayloads.push(options);
|
||||
return { code: '248680', emailTimestamp: 2000, mailId: 'mail-active' };
|
||||
},
|
||||
registerTab: async (source, tabId) => {
|
||||
@@ -355,7 +370,10 @@ test('kiro verification step can adopt the active AWS verify-otp page without st
|
||||
|
||||
assert.deepEqual(registeredTabs, [{ source: 'kiro-register-page', tabId: 301 }]);
|
||||
assert.equal(getKiroRuntime(statePatches[0]).session?.registerTabId, 301);
|
||||
assert.equal(pollPayloads[0].targetEmail, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(pollPayloads[0].flowId, 'kiro');
|
||||
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(pollPayloads[0].state.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(sentMessages.some((message) => (
|
||||
message.type === 'EXECUTE_NODE'
|
||||
&& message.nodeId === 'kiro-submit-verification-code'
|
||||
@@ -419,8 +437,8 @@ test('kiro verification step reinjects the register driver when only the generic
|
||||
'content/utils.js',
|
||||
'flows/kiro/content/register-page.js',
|
||||
],
|
||||
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
|
||||
pollPayloads.push(payload);
|
||||
pollFlowVerificationCode: async (options) => {
|
||||
pollPayloads.push(options);
|
||||
return { code: '248680', emailTimestamp: 2000, mailId: 'mail-reinject' };
|
||||
},
|
||||
sendToContentScriptResilient: async (_sourceId, message) => {
|
||||
@@ -464,7 +482,10 @@ test('kiro verification step reinjects the register driver when only the generic
|
||||
'content/utils.js',
|
||||
'flows/kiro/content/register-page.js',
|
||||
]);
|
||||
assert.equal(pollPayloads[0].targetEmail, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(pollPayloads[0].flowId, 'kiro');
|
||||
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(pollPayloads[0].state.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(getKiroRuntime(completedPayload).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports mail rule registry and OpenAI mail rules modules', () => {
|
||||
test('background imports mail rule registry and flow mail rules modules', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/mail-rule-registry\.js/);
|
||||
assert.match(source, /flows\/openai\/mail-rules\.js/);
|
||||
assert.match(source, /flows\/kiro\/mail-rules\.js/);
|
||||
assert.match(source, /flows\/grok\/mail-rules\.js/);
|
||||
assert.match(source, /background\/flow-mail-polling\.js/);
|
||||
});
|
||||
|
||||
test('mail rule registry exposes canonical OpenAI verification poll payloads', () => {
|
||||
@@ -133,3 +136,185 @@ test('mail rule registry rejects unknown active flow ids instead of silently usi
|
||||
/未找到 flow=site-a 的邮件轮询规则构造器/
|
||||
);
|
||||
});
|
||||
|
||||
test('mail rule registry exposes Kiro AWS verification poll payloads by node', () => {
|
||||
const stateSource = fs.readFileSync('flows/kiro/background/state.js', 'utf8');
|
||||
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
|
||||
const kiroSource = fs.readFileSync('flows/kiro/mail-rules.js', 'utf8');
|
||||
const globalScope = {};
|
||||
new Function('self', `${stateSource}; ${registrySource}; ${kiroSource}; return self;`)(globalScope);
|
||||
|
||||
const kiroMailRules = globalScope.MultiPageKiroMailRules.createKiroMailRules({
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
|
||||
});
|
||||
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
|
||||
defaultFlowId: 'openai',
|
||||
flowBuilders: {
|
||||
kiro: kiroMailRules,
|
||||
},
|
||||
});
|
||||
const baseState = {
|
||||
activeFlowId: 'kiro',
|
||||
mailProvider: '2925',
|
||||
mail2925Mode: 'receive',
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
kiro: {
|
||||
register: {
|
||||
email: 'kiro-user@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
registry.buildVerificationPollPayloadForNode(
|
||||
'kiro-submit-verification-code',
|
||||
baseState,
|
||||
{ filterAfterTimestamp: 12345 }
|
||||
),
|
||||
{
|
||||
flowId: 'kiro',
|
||||
ruleId: 'kiro-submit-verification-code',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
step: 4,
|
||||
artifactType: 'code',
|
||||
codePatterns: [
|
||||
{
|
||||
source: '(?:verification\\s*code|验证码|Your code is|code is)[::\\s]*(\\d{6})',
|
||||
flags: 'gi',
|
||||
},
|
||||
{
|
||||
source: '^\\s*(\\d{6})\\s*$',
|
||||
flags: 'gm',
|
||||
},
|
||||
{
|
||||
source: '>\\s*(\\d{6})\\s*<',
|
||||
flags: 'g',
|
||||
},
|
||||
],
|
||||
filterAfterTimestamp: 12345,
|
||||
requiredKeywords: ['verification', '验证码', 'code', 'aws'],
|
||||
senderFilters: [
|
||||
'no-reply@signin.aws',
|
||||
'no-reply@login.awsapps.com',
|
||||
'noreply@amazon.com',
|
||||
'account-update@amazon.com',
|
||||
'no-reply@aws.amazon.com',
|
||||
'noreply@aws.amazon.com',
|
||||
'aws',
|
||||
],
|
||||
subjectFilters: ['aws builder id', 'verification', '验证码', 'code', 'aws'],
|
||||
targetEmail: 'kiro-user@example.com',
|
||||
targetEmailHints: ['kiro-user@example.com'],
|
||||
mail2925MatchTargetEmail: true,
|
||||
maxAttempts: 17,
|
||||
intervalMs: 16000,
|
||||
}
|
||||
);
|
||||
|
||||
const desktopPayload = registry.buildVerificationPollPayloadForNode(
|
||||
'kiro-complete-desktop-authorize',
|
||||
{
|
||||
...baseState,
|
||||
mailProvider: 'luckmail-api',
|
||||
mail2925Mode: 'provide',
|
||||
}
|
||||
);
|
||||
assert.equal(desktopPayload.ruleId, 'kiro-complete-desktop-authorize');
|
||||
assert.equal(desktopPayload.step, 8);
|
||||
assert.equal(desktopPayload.mail2925MatchTargetEmail, false);
|
||||
assert.equal(desktopPayload.maxAttempts, 3);
|
||||
assert.equal(desktopPayload.intervalMs, 15000);
|
||||
});
|
||||
|
||||
test('mail rule registry exposes Grok xAI verification poll payloads by node', () => {
|
||||
const stateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8');
|
||||
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
|
||||
const grokSource = fs.readFileSync('flows/grok/mail-rules.js', 'utf8');
|
||||
const globalScope = {};
|
||||
new Function('self', `${stateSource}; ${registrySource}; ${grokSource}; return self;`)(globalScope);
|
||||
|
||||
const grokMailRules = globalScope.MultiPageGrokMailRules.createGrokMailRules({
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
|
||||
});
|
||||
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
|
||||
defaultFlowId: 'openai',
|
||||
flowBuilders: {
|
||||
grok: grokMailRules,
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
registry.buildVerificationPollPayloadForNode(
|
||||
'grok-submit-verification-code',
|
||||
{
|
||||
activeFlowId: 'grok',
|
||||
mailProvider: '2925',
|
||||
mail2925Mode: 'receive',
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
register: {
|
||||
email: 'grok-user@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ filterAfterTimestamp: 12345 }
|
||||
),
|
||||
{
|
||||
flowId: 'grok',
|
||||
ruleId: 'grok-submit-verification-code',
|
||||
nodeId: 'grok-submit-verification-code',
|
||||
step: 3,
|
||||
artifactType: 'code',
|
||||
codePatterns: [
|
||||
{
|
||||
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
|
||||
flags: 'gi',
|
||||
},
|
||||
{
|
||||
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[::\\s]*(\\d{6})',
|
||||
flags: 'gi',
|
||||
},
|
||||
{
|
||||
source: '(?:验证码|代码|确认码)[::\\s为]+(\\d{6})',
|
||||
flags: 'gi',
|
||||
},
|
||||
{
|
||||
source: '(?<!#)\\b(\\d{6})\\b',
|
||||
flags: 'g',
|
||||
},
|
||||
],
|
||||
filterAfterTimestamp: 12345,
|
||||
requiredKeywords: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
|
||||
senderFilters: ['x.ai', 'xai', 'grok'],
|
||||
subjectFilters: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
|
||||
targetEmail: 'grok-user@example.com',
|
||||
targetEmailHints: ['grok-user@example.com'],
|
||||
mail2925MatchTargetEmail: true,
|
||||
maxAttempts: 17,
|
||||
intervalMs: 16000,
|
||||
}
|
||||
);
|
||||
|
||||
const luckmailPayload = registry.buildVerificationPollPayloadForNode(
|
||||
'grok-submit-verification-code',
|
||||
{
|
||||
activeFlowId: 'grok',
|
||||
mailProvider: 'luckmail-api',
|
||||
mail2925Mode: 'provide',
|
||||
grokEmail: 'fallback@example.com',
|
||||
}
|
||||
);
|
||||
assert.equal(luckmailPayload.mail2925MatchTargetEmail, false);
|
||||
assert.equal(luckmailPayload.maxAttempts, 3);
|
||||
assert.equal(luckmailPayload.intervalMs, 15000);
|
||||
});
|
||||
|
||||
@@ -625,6 +625,47 @@ test('SAVE_SETTING mirrors activeFlowId into flowId when switching to kiro flow'
|
||||
});
|
||||
});
|
||||
|
||||
test('CLEAR_GROK_SSO_COOKIES delegates canonical runtime cleanup to background', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = { console };
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
let called = false;
|
||||
const router = api.createMessageRouter({
|
||||
clearGrokSsoCookies: async () => {
|
||||
called = true;
|
||||
return {
|
||||
ok: true,
|
||||
state: {
|
||||
grokSsoCookie: '',
|
||||
grokSsoCookies: [],
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
sso: {
|
||||
currentCookie: '',
|
||||
cookies: [],
|
||||
extractedAt: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'CLEAR_GROK_SSO_COOKIES',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
assert.equal(called, true);
|
||||
assert.equal(response.ok, true);
|
||||
assert.deepStrictEqual(response.state.grokSsoCookies, []);
|
||||
assert.equal(response.state.runtimeState.flowState.grok.sso.currentCookie, '');
|
||||
});
|
||||
|
||||
test('SAVE_SETTING syncs canonical kiro settingsState back into session state', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = { console };
|
||||
|
||||
@@ -17,11 +17,14 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
|
||||
assert.match(source, /flows\/kiro\/background\/desktop-client\.js/);
|
||||
assert.match(source, /flows\/kiro\/background\/desktop-authorize-runner\.js/);
|
||||
assert.match(source, /flows\/kiro\/background\/publisher-kiro-rs\.js/);
|
||||
assert.match(source, /flows\/grok\/background\/state\.js/);
|
||||
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
|
||||
assert.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/);
|
||||
|
||||
assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/);
|
||||
assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/);
|
||||
assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/);
|
||||
assert.match(source, /const grokRegisterRunner = self\.MultiPageBackgroundGrokRegisterRunner\?\.createGrokRegisterRunner\(/);
|
||||
|
||||
assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/);
|
||||
assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/);
|
||||
@@ -32,11 +35,20 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
|
||||
assert.match(source, /'kiro-start-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroStartDesktopAuthorize\(state\)/);
|
||||
assert.match(source, /'kiro-complete-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroCompleteDesktopAuthorize\(state\)/);
|
||||
assert.match(source, /'kiro-upload-credential': \(state\) => kiroPublisher\.executeKiroUploadCredential\(state\)/);
|
||||
assert.match(source, /'grok-open-signup-page': \(state\) => grokRegisterRunner\.executeGrokOpenSignupPage\(state\)/);
|
||||
assert.match(source, /'grok-submit-email': \(state\) => grokRegisterRunner\.executeGrokSubmitEmail\(state\)/);
|
||||
assert.match(source, /'grok-submit-verification-code': \(state\) => grokRegisterRunner\.executeGrokSubmitVerificationCode\(state\)/);
|
||||
assert.match(source, /'grok-submit-profile': \(state\) => grokRegisterRunner\.executeGrokSubmitProfile\(state\)/);
|
||||
assert.match(source, /'grok-extract-sso-cookie': \(state\) => grokRegisterRunner\.executeGrokExtractSsoCookie\(state\)/);
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/'kiro-open-register-page',[\s\S]*'kiro-submit-email',[\s\S]*'kiro-submit-name',[\s\S]*'kiro-submit-verification-code',[\s\S]*'kiro-submit-password',[\s\S]*'kiro-complete-register-consent',[\s\S]*'kiro-start-desktop-authorize',[\s\S]*'kiro-complete-desktop-authorize',[\s\S]*'kiro-upload-credential'/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/'grok-open-signup-page',[\s\S]*'grok-submit-email',[\s\S]*'grok-submit-verification-code',[\s\S]*'grok-submit-profile',[\s\S]*'grok-extract-sso-cookie'/
|
||||
);
|
||||
});
|
||||
|
||||
test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
|
||||
|
||||
@@ -69,9 +69,23 @@ test('contribution registry resolves the combined tutorial entry per flow', () =
|
||||
);
|
||||
});
|
||||
|
||||
test('contribution registry keeps Grok SSO export flow out of published contribution checks', () => {
|
||||
const api = loadApi();
|
||||
|
||||
assert.equal(api.getContributionTutorialEntry('grok'), null);
|
||||
assert.deepEqual(api.getContributionAdapterIds('grok'), []);
|
||||
assert.deepEqual(api.getPublishedContributionFlowIds(['openai', 'kiro', 'grok']), ['openai', 'kiro']);
|
||||
assert.throws(
|
||||
() => api.assertPublishedFlowsHaveContributionAdapters(['grok']),
|
||||
/缺少账号贡献适配器:grok/
|
||||
);
|
||||
});
|
||||
|
||||
test('contribution registry fails fast when a published flow has no adapter', () => {
|
||||
const api = loadApi();
|
||||
|
||||
assert.deepEqual(api.getPublishedContributionFlowIds(), ['openai', 'kiro']);
|
||||
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(), true);
|
||||
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true);
|
||||
assert.throws(
|
||||
() => api.assertPublishedFlowsHaveContributionAdapters(['openai', 'missing-flow']),
|
||||
|
||||
@@ -98,6 +98,36 @@ test('flow capability registry exposes Kiro as an independent flow with its own
|
||||
);
|
||||
});
|
||||
|
||||
test('flow capability registry exposes Grok as an independent SSO flow without OpenAI-only modes', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
const capabilityState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'grok',
|
||||
targetId: 'webchat2api',
|
||||
signupMethod: 'phone',
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
accountContributionEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(capabilityState.activeFlowId, 'grok');
|
||||
assert.equal(capabilityState.canShowPhoneSettings, false);
|
||||
assert.equal(capabilityState.canShowPlusSettings, false);
|
||||
assert.equal(capabilityState.canShowContributionMode, false);
|
||||
assert.equal(capabilityState.canShowLuckmail, false);
|
||||
assert.equal(capabilityState.effectiveSignupMethod, 'email');
|
||||
assert.equal(capabilityState.effectiveTargetId, 'webchat2api');
|
||||
assert.deepEqual(capabilityState.supportedTargetIds, ['webchat2api']);
|
||||
assert.deepEqual(capabilityState.flowCapabilities.contributionAdapterIds, []);
|
||||
assert.deepEqual(
|
||||
capabilityState.visibleGroupIds,
|
||||
['grok-runtime-status', 'grok-target-webchat2api', 'service-account', 'service-email', 'service-proxy']
|
||||
);
|
||||
});
|
||||
|
||||
test('flow capability registry exposes shared auto-run validation for phone locks and target support', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry({
|
||||
|
||||
@@ -16,12 +16,14 @@ function loadApis() {
|
||||
test('flow registry exposes canonical flow and target metadata', () => {
|
||||
const { flowRegistry } = loadApis();
|
||||
|
||||
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro']);
|
||||
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
|
||||
assert.equal(flowRegistry.normalizeFlowId('kiro'), 'kiro');
|
||||
assert.equal(flowRegistry.normalizeFlowId('grok'), 'grok');
|
||||
assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai');
|
||||
assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI');
|
||||
assert.equal(flowRegistry.normalizeTargetId('openai', 'sub2api'), 'sub2api');
|
||||
assert.equal(flowRegistry.normalizeTargetId('kiro', 'anything-else'), 'kiro-rs');
|
||||
assert.equal(flowRegistry.normalizeTargetId('grok', 'anything-else'), 'webchat2api');
|
||||
assert.deepEqual(
|
||||
flowRegistry.getVisibleGroupIds('openai', 'cpa'),
|
||||
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy']
|
||||
@@ -30,10 +32,18 @@ test('flow registry exposes canonical flow and target metadata', () => {
|
||||
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
|
||||
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getVisibleGroupIds('grok', 'webchat2api'),
|
||||
['grok-runtime-status', 'grok-target-webchat2api', 'service-account', 'service-email', 'service-proxy']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
|
||||
['cpa', 'sub2api', 'codex2api']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getTargetOptions('grok').map((entry) => entry.id),
|
||||
['webchat2api']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
|
||||
['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method']
|
||||
@@ -41,6 +51,8 @@ test('flow registry exposes canonical flow and target metadata', () => {
|
||||
assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
|
||||
assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true);
|
||||
assert.equal(flowRegistry.getFlowCapabilities('kiro').supportsAccountContribution, true);
|
||||
assert.equal(flowRegistry.getFlowCapabilities('grok').supportsAccountContribution, false);
|
||||
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').supportedTargetIds, ['webchat2api']);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getFlowCapabilities('openai').contributionAdapterIds,
|
||||
['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']
|
||||
@@ -49,6 +61,7 @@ test('flow registry exposes canonical flow and target metadata', () => {
|
||||
flowRegistry.getFlowCapabilities('kiro').contributionAdapterIds,
|
||||
['kiro-builder-id']
|
||||
);
|
||||
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').contributionAdapterIds, []);
|
||||
});
|
||||
|
||||
test('settings schema normalizes view input into canonical nested namespaces', () => {
|
||||
@@ -68,6 +81,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
|
||||
stepExecutionRangeByFlow: {
|
||||
openai: { enabled: true, fromStep: 2, toStep: 9 },
|
||||
kiro: { enabled: true, fromStep: 1, toStep: 9 },
|
||||
grok: { enabled: true, fromStep: 2, toStep: 4 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,6 +92,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
|
||||
assert.equal(normalized.flows.openai.selectedTargetId, 'cpa');
|
||||
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(normalized.flows.kiro.selectedTargetId, 'kiro-rs');
|
||||
assert.equal(normalized.flows.grok.selectedTargetId, 'webchat2api');
|
||||
assert.equal(normalized.flows.kiro.targets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key');
|
||||
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
|
||||
@@ -85,6 +100,11 @@ test('settings schema normalizes view input into canonical nested namespaces', (
|
||||
fromStep: 1,
|
||||
toStep: 9,
|
||||
});
|
||||
assert.deepEqual(normalized.flows.grok.autoRun.stepExecutionRange, {
|
||||
enabled: true,
|
||||
fromStep: 2,
|
||||
toStep: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('settings schema lets explicit flat step range override stale canonical range', () => {
|
||||
@@ -130,6 +150,11 @@ test('settings schema can project canonical state into a read view without legac
|
||||
assert.equal(view.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(view.settingsSchemaVersion, 5);
|
||||
assert.equal(view.settingsState.activeFlowId, 'kiro');
|
||||
assert.deepEqual(view.stepExecutionRangeByFlow.grok, {
|
||||
enabled: false,
|
||||
fromStep: 1,
|
||||
toStep: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('settings schema preserves CPA session strategy in canonical state and read view', () => {
|
||||
@@ -143,3 +168,99 @@ test('settings schema preserves CPA session strategy in canonical state and read
|
||||
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(view.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
});
|
||||
|
||||
test('settings schema preserves registered custom flow settings without openai/kiro hardcoding', () => {
|
||||
const { settingsSchema } = loadApis();
|
||||
const customFlowRegistry = {
|
||||
DEFAULT_FLOW_ID: 'openai',
|
||||
getRegisteredFlowIds: () => ['openai', 'kiro', 'sample'],
|
||||
getDefaultTargetId(flowId) {
|
||||
return flowId === 'sample' ? 'sample-target' : (flowId === 'kiro' ? 'kiro-rs' : 'cpa');
|
||||
},
|
||||
getFlowDefinition(flowId) {
|
||||
if (flowId !== 'sample') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: 'sample',
|
||||
defaultTargetId: 'sample-target',
|
||||
settingsDefaults: {
|
||||
targets: {
|
||||
'sample-target': {
|
||||
endpoint: 'https://sample.example.com',
|
||||
},
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 3 },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getTargetDefinitions(flowId) {
|
||||
if (flowId === 'sample') {
|
||||
return {
|
||||
'sample-target': { id: 'sample-target', label: 'Sample Target' },
|
||||
};
|
||||
}
|
||||
if (flowId === 'kiro') {
|
||||
return {
|
||||
'kiro-rs': { id: 'kiro-rs', label: 'kiro.rs' },
|
||||
};
|
||||
}
|
||||
return {
|
||||
cpa: { id: 'cpa', label: 'CPA' },
|
||||
sub2api: { id: 'sub2api', label: 'SUB2API' },
|
||||
codex2api: { id: 'codex2api', label: 'Codex2API' },
|
||||
};
|
||||
},
|
||||
normalizeFlowId(value = '', fallback = 'openai') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return ['openai', 'kiro', 'sample'].includes(normalized)
|
||||
? normalized
|
||||
: (['openai', 'kiro', 'sample'].includes(fallback) ? fallback : 'openai');
|
||||
},
|
||||
normalizeTargetId(flowId, targetId = '', fallback = '') {
|
||||
const targets = Object.keys(customFlowRegistry.getTargetDefinitions(flowId));
|
||||
const normalized = String(targetId || '').trim().toLowerCase();
|
||||
if (targets.includes(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
if (targets.includes(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
return customFlowRegistry.getDefaultTargetId(flowId);
|
||||
},
|
||||
};
|
||||
const schema = settingsSchema.createSettingsSchema({ flowRegistry: customFlowRegistry });
|
||||
|
||||
const normalized = schema.normalizeSettingsState({
|
||||
activeFlowId: 'sample',
|
||||
targetId: 'sample-target',
|
||||
settingsState: {
|
||||
flows: {
|
||||
sample: {
|
||||
selectedTargetId: 'sample-target',
|
||||
targets: {
|
||||
'sample-target': {
|
||||
endpoint: 'https://custom.example.com',
|
||||
},
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: true, fromStep: 2, toStep: 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const view = schema.buildSettingsView(normalized);
|
||||
|
||||
assert.equal(normalized.activeFlowId, 'sample');
|
||||
assert.equal(normalized.flows.sample.selectedTargetId, 'sample-target');
|
||||
assert.equal(normalized.flows.sample.targets['sample-target'].endpoint, 'https://custom.example.com');
|
||||
assert.deepEqual(view.stepExecutionRangeByFlow.sample, {
|
||||
enabled: true,
|
||||
fromStep: 2,
|
||||
toStep: 3,
|
||||
});
|
||||
assert.equal(schema.getSelectedTargetId(normalized, 'sample'), 'sample-target');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadGrokRunnerApi() {
|
||||
const source = fs.readFileSync('flows/grok/background/register-runner.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundGrokRegisterRunner;`)(globalScope);
|
||||
}
|
||||
|
||||
function getGrokRuntime(state = {}) {
|
||||
return state?.runtimeState?.flowState?.grok || {};
|
||||
}
|
||||
|
||||
test('grok runner delegates verification polling to the shared flow mail service', () => {
|
||||
const source = fs.readFileSync('flows/grok/background/register-runner.js', 'utf8');
|
||||
assert.match(source, /pollFlowVerificationCode/);
|
||||
assert.doesNotMatch(source, /buildGrokVerificationPollPayload/);
|
||||
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
|
||||
});
|
||||
|
||||
test('grok content script does not patch global MouseEvent prototypes', () => {
|
||||
const source = fs.readFileSync('flows/grok/content/register-page.js', 'utf8');
|
||||
assert.doesNotMatch(source, /MouseEvent\.prototype/);
|
||||
assert.doesNotMatch(source, /Object\.defineProperty\(MouseEvent/);
|
||||
assert.match(source, /screenX:/);
|
||||
assert.match(source, /screenY:/);
|
||||
});
|
||||
|
||||
test('grok verification runner polls by flow node and submits normalized code', async () => {
|
||||
const api = loadGrokRunnerApi();
|
||||
const calls = [];
|
||||
let completedPayload = null;
|
||||
let currentState = {
|
||||
activeFlowId: 'grok',
|
||||
mailProvider: '2925',
|
||||
grokRegisterTabId: 101,
|
||||
grokEmail: 'grok-user@example.com',
|
||||
grokVerificationRequestedAt: 1000000,
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 101,
|
||||
},
|
||||
register: {
|
||||
email: 'grok-user@example.com',
|
||||
verificationRequestedAt: 1000000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const runner = api.createGrokRegisterRunner({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async (tabId) => ({ id: tabId }),
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeNodeFromBackground: async (_nodeId, payload) => {
|
||||
completedPayload = payload;
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getState: async () => currentState,
|
||||
getTabId: async () => 101,
|
||||
isTabAlive: async () => true,
|
||||
pollFlowVerificationCode: async (options) => {
|
||||
calls.push({ type: 'poll', options });
|
||||
return { code: 'ABC-123', messageId: 'mail-001' };
|
||||
},
|
||||
registerTab: async () => {},
|
||||
sendToContentScriptResilient: async (sourceId, message) => {
|
||||
calls.push({ type: 'send', sourceId, message });
|
||||
return { submitted: true, state: 'profile_entry', url: 'https://accounts.x.ai/profile' };
|
||||
},
|
||||
setState: async (patch) => {
|
||||
currentState = { ...currentState, ...patch };
|
||||
},
|
||||
waitForTabStableComplete: async () => {},
|
||||
});
|
||||
|
||||
await runner.executeGrokSubmitVerificationCode({ nodeId: 'grok-submit-verification-code', ...currentState });
|
||||
|
||||
const pollCall = calls.find((entry) => entry.type === 'poll');
|
||||
assert.equal(pollCall.options.flowId, 'grok');
|
||||
assert.equal(pollCall.options.nodeId, 'grok-submit-verification-code');
|
||||
assert.equal(pollCall.options.step, 3);
|
||||
assert.equal(pollCall.options.logStep, 3);
|
||||
assert.equal(pollCall.options.filterAfterTimestamp, 400000);
|
||||
assert.equal(pollCall.options.state.activeFlowId, 'grok');
|
||||
assert.equal(pollCall.options.state.visibleStep, 3);
|
||||
|
||||
const sendCall = calls.find((entry) => entry.type === 'send');
|
||||
assert.equal(sendCall.sourceId, 'grok-register-page');
|
||||
assert.equal(sendCall.message.type, 'EXECUTE_NODE');
|
||||
assert.equal(sendCall.message.nodeId, 'grok-submit-verification-code');
|
||||
assert.deepEqual(sendCall.message.payload, { code: 'ABC123' });
|
||||
|
||||
assert.equal(completedPayload.grokVerificationCode, 'ABC123');
|
||||
assert.equal(completedPayload.grokVerificationRawCode, 'ABC-123');
|
||||
assert.equal(completedPayload.grokVerificationMessageId, 'mail-001');
|
||||
assert.equal(getGrokRuntime(completedPayload).register.verificationCode, 'ABC123');
|
||||
assert.equal(getGrokRuntime(completedPayload).register.status, 'verified');
|
||||
});
|
||||
|
||||
test('grok SSO extraction accumulates unique cookies without logging the secret value', async () => {
|
||||
const api = loadGrokRunnerApi();
|
||||
const logs = [];
|
||||
let completedPayload = null;
|
||||
let markUsedPayload = null;
|
||||
let currentState = {
|
||||
activeFlowId: 'grok',
|
||||
grokRegisterTabId: 202,
|
||||
grokSsoCookies: ['existing-cookie', 'new-cookie'],
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 202,
|
||||
},
|
||||
sso: {
|
||||
cookies: ['existing-cookie', 'new-cookie'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const runner = api.createGrokRegisterRunner({
|
||||
addLog: async (message, level) => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {
|
||||
cookies: {
|
||||
get: async () => ({ value: 'new-cookie' }),
|
||||
},
|
||||
tabs: {
|
||||
get: async (tabId) => ({ id: tabId }),
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeNodeFromBackground: async (_nodeId, payload) => {
|
||||
completedPayload = payload;
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getState: async () => currentState,
|
||||
getTabId: async () => 202,
|
||||
isTabAlive: async () => true,
|
||||
markCurrentRegistrationAccountUsed: async (state) => {
|
||||
markUsedPayload = state;
|
||||
},
|
||||
registerTab: async () => {},
|
||||
sendToContentScriptResilient: async () => {
|
||||
throw new Error('content fallback should not be used when chrome.cookies finds sso');
|
||||
},
|
||||
setState: async (patch) => {
|
||||
currentState = { ...currentState, ...patch };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
waitForTabStableComplete: async () => {},
|
||||
});
|
||||
|
||||
await runner.executeGrokExtractSsoCookie({ nodeId: 'grok-extract-sso-cookie', ...currentState });
|
||||
|
||||
assert.equal(completedPayload.grokSsoCookie, 'new-cookie');
|
||||
assert.deepEqual(completedPayload.grokSsoCookies, ['existing-cookie', 'new-cookie']);
|
||||
assert.equal(getGrokRuntime(completedPayload).sso.currentCookie, 'new-cookie');
|
||||
assert.deepEqual(getGrokRuntime(completedPayload).sso.cookies, ['existing-cookie', 'new-cookie']);
|
||||
assert.equal(markUsedPayload.grokSsoCookie, 'new-cookie');
|
||||
assert.equal(logs.some(({ message }) => message.includes('new-cookie')), false);
|
||||
});
|
||||
|
||||
test('grok register runner requires background node completion dependency', () => {
|
||||
const api = loadGrokRunnerApi();
|
||||
assert.throws(
|
||||
() => api.createGrokRegisterRunner({}),
|
||||
/requires completeNodeFromBackground/
|
||||
);
|
||||
});
|
||||
@@ -3,12 +3,14 @@ const fs = require('node:fs');
|
||||
const FLOW_DEFINITION_FILES = Object.freeze([
|
||||
'flows/openai/index.js',
|
||||
'flows/kiro/index.js',
|
||||
'flows/grok/index.js',
|
||||
'flows/index.js',
|
||||
]);
|
||||
|
||||
const FLOW_WORKFLOW_FILES = Object.freeze([
|
||||
'flows/openai/workflow.js',
|
||||
'flows/kiro/workflow.js',
|
||||
'flows/grok/workflow.js',
|
||||
]);
|
||||
|
||||
function readBundle(files = []) {
|
||||
|
||||
@@ -36,6 +36,7 @@ function extractFunction(source, name) {
|
||||
test('sidepanel html exposes flow selector and kiro source fields', () => {
|
||||
[
|
||||
'id="select-flow"',
|
||||
'<option value="grok">Grok</option>',
|
||||
'id="label-source-selector"',
|
||||
'id="row-step6-cookie-settings"',
|
||||
'id="row-kiro-rs-url"',
|
||||
@@ -46,9 +47,90 @@ test('sidepanel html exposes flow selector and kiro source fields', () => {
|
||||
'id="row-kiro-web-status"',
|
||||
'id="row-kiro-login-url"',
|
||||
'id="row-kiro-upload-status"',
|
||||
'id="row-grok-register-status"',
|
||||
'id="row-grok-sso-status"',
|
||||
'id="row-grok-sso-settings"',
|
||||
'id="btn-copy-grok-sso"',
|
||||
'id="btn-export-grok-sso"',
|
||||
'id="btn-clear-grok-sso"',
|
||||
'<script src="../flows/grok/index.js"></script>',
|
||||
'<script src="../flows/grok/workflow.js"></script>',
|
||||
].forEach((snippet) => {
|
||||
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
});
|
||||
assert.ok(
|
||||
sidepanelHtml.indexOf('<script src="../flows/kiro/workflow.js"></script>')
|
||||
< sidepanelHtml.indexOf('<script src="../flows/grok/index.js"></script>')
|
||||
);
|
||||
assert.ok(
|
||||
sidepanelHtml.indexOf('<script src="../flows/grok/workflow.js"></script>')
|
||||
< sidepanelHtml.indexOf('<script src="../flows/index.js"></script>')
|
||||
);
|
||||
});
|
||||
|
||||
test('sidepanel Grok SSO clear action goes through background message instead of direct storage writes', () => {
|
||||
const clearButtonIndex = sidepanelSource.indexOf("btnClearGrokSso?.addEventListener('click'");
|
||||
assert.notEqual(clearButtonIndex, -1);
|
||||
const nextManagerIndex = sidepanelSource.indexOf('const hotmailManager', clearButtonIndex);
|
||||
assert.notEqual(nextManagerIndex, -1);
|
||||
const block = sidepanelSource.slice(clearButtonIndex, nextManagerIndex);
|
||||
|
||||
assert.match(block, /type:\s*'CLEAR_GROK_SSO_COOKIES'/);
|
||||
assert.match(block, /chrome\.runtime\.sendMessage/);
|
||||
assert.doesNotMatch(block, /chrome\.storage/);
|
||||
assert.doesNotMatch(block, /storage\.local\.set/);
|
||||
});
|
||||
|
||||
test('sidepanel renders Grok SSO status from canonical runtime state', () => {
|
||||
const bundle = [
|
||||
extractFunction(sidepanelSource, 'getGrokRuntimeState'),
|
||||
extractFunction(sidepanelSource, 'normalizeGrokSsoCookies'),
|
||||
extractFunction(sidepanelSource, 'getGrokRegisterStatusLabel'),
|
||||
extractFunction(sidepanelSource, 'renderGrokRuntimeState'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = {};
|
||||
const displayGrokRegisterStatus = { textContent: '' };
|
||||
const displayGrokSsoStatus = { textContent: '' };
|
||||
const displayGrokSsoCookie = { textContent: '', title: '' };
|
||||
const buttons = [];
|
||||
const btnCopyGrokSso = { disabled: false };
|
||||
const btnExportGrokSso = { disabled: false };
|
||||
const btnClearGrokSso = { disabled: false };
|
||||
${bundle}
|
||||
return {
|
||||
displayGrokRegisterStatus,
|
||||
displayGrokSsoStatus,
|
||||
displayGrokSsoCookie,
|
||||
btnCopyGrokSso,
|
||||
btnExportGrokSso,
|
||||
btnClearGrokSso,
|
||||
renderGrokRuntimeState,
|
||||
};
|
||||
`)();
|
||||
|
||||
api.renderGrokRuntimeState({
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
register: { status: 'completed' },
|
||||
sso: {
|
||||
currentCookie: '1234567890abcdef',
|
||||
cookies: ['1234567890abcdef', 'second-cookie'],
|
||||
extractedAt: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(api.displayGrokRegisterStatus.textContent, '已完成');
|
||||
assert.match(api.displayGrokSsoStatus.textContent, /^已提取 2 条/);
|
||||
assert.equal(api.displayGrokSsoCookie.textContent, '12345678...abcdef');
|
||||
assert.equal(api.btnCopyGrokSso.disabled, false);
|
||||
assert.equal(api.btnExportGrokSso.disabled, false);
|
||||
assert.equal(api.btnClearGrokSso.disabled, false);
|
||||
});
|
||||
|
||||
test('sidepanel Kiro GitHub button opens the configured fork', () => {
|
||||
|
||||
@@ -525,6 +525,7 @@ function updateAccountRunHistorySettingsUI() {}
|
||||
function updatePhoneVerificationSettingsUI() {}
|
||||
function updatePanelModeUI() {}
|
||||
function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); }
|
||||
function renderGrokRuntimeState() {}
|
||||
function renderSub2ApiGroupOptions() {}
|
||||
function isLuckmailProvider() { return false; }
|
||||
function updateButtonStates() {}
|
||||
|
||||
@@ -15,6 +15,11 @@ test('background imports shared source registry module', () => {
|
||||
assert.match(source, /core\/flow-kernel\/settings-schema\.js/);
|
||||
assert.match(source, /core\/flow-kernel\/source-registry\.js/);
|
||||
assert.match(source, /shared\/kiro-timeouts\.js/);
|
||||
assert.match(source, /flows\/grok\/index\.js/);
|
||||
assert.match(source, /flows\/grok\/workflow\.js/);
|
||||
assert.match(source, /flows\/grok\/background\/state\.js/);
|
||||
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
|
||||
assert.match(source, /flows\/grok\/mail-rules\.js/);
|
||||
});
|
||||
|
||||
test('manifest loads shared source registry before content utils in static bundles', () => {
|
||||
@@ -41,15 +46,38 @@ test('manifest no longer ships a static Kiro content bundle', () => {
|
||||
assert.equal(hasStaticKiroBundle, false);
|
||||
});
|
||||
|
||||
test('manifest loads Grok flow definition in static bundles but not Grok content runtime', () => {
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
for (const entry of manifest.content_scripts || []) {
|
||||
const scripts = Array.isArray(entry.js) ? entry.js : [];
|
||||
if (!scripts.includes('flows/index.js')) continue;
|
||||
assert.ok(scripts.includes('flows/kiro/index.js'));
|
||||
assert.ok(scripts.includes('flows/grok/index.js'));
|
||||
assert.ok(
|
||||
scripts.indexOf('flows/kiro/index.js') < scripts.indexOf('flows/grok/index.js'),
|
||||
'Kiro definition should load before Grok definition'
|
||||
);
|
||||
assert.ok(
|
||||
scripts.indexOf('flows/grok/index.js') < scripts.indexOf('flows/index.js'),
|
||||
'Grok definition must load before flows/index.js'
|
||||
);
|
||||
assert.equal(scripts.includes('flows/grok/content/register-page.js'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('background injects shared Kiro timeout module before Kiro content scripts', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(
|
||||
source,
|
||||
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
|
||||
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
|
||||
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const GROK_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'content\/utils\.js', 'flows\/grok\/content\/register-page\.js'\];/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -83,6 +111,20 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
|
||||
}),
|
||||
'kiro-desktop-authorize'
|
||||
);
|
||||
assert.equal(
|
||||
registry.detectSourceFromLocation({
|
||||
url: 'https://accounts.x.ai/sign-up',
|
||||
hostname: 'accounts.x.ai',
|
||||
}),
|
||||
'grok-register-page'
|
||||
);
|
||||
assert.equal(
|
||||
registry.detectSourceFromLocation({
|
||||
url: 'https://grok.com/',
|
||||
hostname: 'grok.com',
|
||||
}),
|
||||
'grok-register-page'
|
||||
);
|
||||
assert.equal(
|
||||
registry.detectSourceFromLocation({
|
||||
url: 'https://example.com/',
|
||||
@@ -115,6 +157,22 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'grok-register-page',
|
||||
'https://accounts.x.ai/sign-up',
|
||||
'https://grok.com/'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'grok-register-page',
|
||||
'https://grok.com/',
|
||||
'https://accounts.x.ai/sign-up'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'kiro-desktop-authorize',
|
||||
@@ -137,4 +195,6 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
|
||||
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/register-runner', 'kiro-open-register-page'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/desktop-authorize-runner', 'kiro-start-desktop-authorize'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/publisher-kiro-rs', 'kiro-upload-credential'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('flows/grok/content/register-page', 'grok-submit-profile'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('flows/grok/background/register-runner', 'grok-extract-sso-cookie'), true);
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
|
||||
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
|
||||
const kiroSteps = api.getSteps({ activeFlowId: 'kiro' });
|
||||
const grokSteps = api.getSteps({ activeFlowId: 'grok' });
|
||||
|
||||
assert.equal(Array.isArray(steps), true);
|
||||
assert.equal(steps.length, 11);
|
||||
@@ -162,8 +163,9 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19);
|
||||
assert.equal(api.hasFlow('openai'), true);
|
||||
assert.equal(api.hasFlow('kiro'), true);
|
||||
assert.equal(api.hasFlow('grok'), true);
|
||||
assert.equal(api.hasFlow('site-a'), false);
|
||||
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro']);
|
||||
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
|
||||
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
|
||||
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
|
||||
assert.deepStrictEqual(
|
||||
@@ -210,6 +212,36 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
[],
|
||||
]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
grokSteps.map((step) => step.key),
|
||||
[
|
||||
'grok-open-signup-page',
|
||||
'grok-submit-email',
|
||||
'grok-submit-verification-code',
|
||||
'grok-submit-profile',
|
||||
'grok-extract-sso-cookie',
|
||||
]
|
||||
);
|
||||
assert.equal(grokSteps.every((step) => step.flowId === 'grok'), true);
|
||||
assert.equal(grokSteps[0].driverId, 'flows/grok/background/register-runner');
|
||||
assert.equal(grokSteps[0].sourceId, 'grok-register-page');
|
||||
assert.equal(grokSteps[2].mailRuleId, 'grok-submit-verification-code');
|
||||
assert.deepStrictEqual(
|
||||
grokSteps.map((step) => step.title),
|
||||
['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie']
|
||||
);
|
||||
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5]);
|
||||
assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 5);
|
||||
assert.deepStrictEqual(
|
||||
api.getNodes({ activeFlowId: 'grok' }).map((node) => node.next),
|
||||
[
|
||||
['grok-submit-email'],
|
||||
['grok-submit-verification-code'],
|
||||
['grok-submit-profile'],
|
||||
['grok-extract-sso-cookie'],
|
||||
[],
|
||||
]
|
||||
);
|
||||
assert.equal(plusSteps[6].title, '创建 Plus Checkout');
|
||||
assert.equal(plusSteps[8].title, 'PayPal 登录与授权');
|
||||
|
||||
|
||||
+40
-10
@@ -10,10 +10,11 @@
|
||||
|
||||
## 1. 项目目标
|
||||
|
||||
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册两条主 flow:
|
||||
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册三条主 flow:
|
||||
|
||||
- `openai`:OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路
|
||||
- `kiro`:AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路
|
||||
- `grok`:Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 本地导出链路
|
||||
|
||||
它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路:
|
||||
|
||||
@@ -28,7 +29,7 @@
|
||||
- 自动确认 OAuth 同意页
|
||||
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
|
||||
|
||||
同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。
|
||||
同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro / Grok 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。
|
||||
|
||||
## 2. 核心运行参与者
|
||||
|
||||
@@ -43,8 +44,8 @@
|
||||
- 向后台发送命令
|
||||
- 接收后台广播并更新 UI
|
||||
- 动态渲染步骤列表
|
||||
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target,`kiro` flow 则使用独立的 `flows.kiro.targetId`
|
||||
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态,不显示 OpenAI 接码 / Plus / 平台绑定配置
|
||||
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target,`kiro` / `grok` flow 使用各自的 `flows.<flowId>.targetId`
|
||||
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态;Grok flow 只显示 `webchat2api` SSO 导出状态、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置
|
||||
- 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源
|
||||
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
|
||||
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
|
||||
@@ -150,13 +151,13 @@
|
||||
- `runtimeState`:当前运行中的 node、nodeStatuses、activeRunId 等核心执行态
|
||||
- `sharedState`:automation window、tab registry、最近来源 URL 这类跨 flow 共享运行态
|
||||
- `serviceState`:代理、邮箱等共享服务运行态
|
||||
- `flowState`:OpenAI / Kiro 各自的 flow 私有运行态
|
||||
- `flowState`:OpenAI / Kiro / Grok 各自的 flow 私有运行态
|
||||
|
||||
这两个模块的作用,是把“当前该跑哪些节点”“当前 state 应该怎么看”“前后台恢复时如何合并 patch”从 `background.js` 主入口里抽离出来,避免再次退回到旧的平铺 step 状态机。
|
||||
|
||||
### 3.4 步骤注册
|
||||
|
||||
[data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import` 或 `cpa-session-import`;`kiro` flow 则输出固定的 9 节点注册/桌面授权/上传 workflow。
|
||||
[data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import` 或 `cpa-session-import`;`kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow;`grok` flow 输出固定的 5 节点注册与 SSO Cookie 提取 workflow。
|
||||
[core/flow-kernel/step-registry.js](./core/flow-kernel/step-registry.js) 负责把“workflow node 元数据”映射到“步骤执行器”,同时保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据。
|
||||
|
||||
这意味着:
|
||||
@@ -168,7 +169,7 @@
|
||||
|
||||
### 3.4.1 验证码规则注册
|
||||
|
||||
[background/mail-rule-registry.js](./background/mail-rule-registry.js) 负责按当前 `activeFlowId` 选择验证码规则构造器。[flows/openai/mail-rules.js](./flows/openai/mail-rules.js) 则提供 OpenAI flow 当前使用的注册验证码 / 登录验证码规则。
|
||||
[background/mail-rule-registry.js](./background/mail-rule-registry.js) 负责按当前 `activeFlowId` 选择验证码规则构造器。[flows/openai/mail-rules.js](./flows/openai/mail-rules.js) 提供 OpenAI flow 当前使用的注册验证码 / 登录验证码规则,[flows/kiro/mail-rules.js](./flows/kiro/mail-rules.js) 提供 Kiro / AWS Builder ID 注册验证码与桌面授权验证码规则,[flows/grok/mail-rules.js](./flows/grok/mail-rules.js) 提供 xAI / Grok 注册验证码规则。
|
||||
|
||||
这层统一产出:
|
||||
|
||||
@@ -180,7 +181,8 @@
|
||||
它的职责边界是:
|
||||
|
||||
- `background/verification-flow.js` 只消费统一规则 payload,不再在每个 provider 或内容脚本里各自硬写一套 OpenAI 验证码过滤条件。
|
||||
- “邮件从哪里读”由 provider 决定,“哪封邮件算命中验证码”优先由规则注册层决定。
|
||||
- `background/flow-mail-polling.js` 负责共享的 provider 分派、iCloud / 2925 会话准备、网页邮箱标签页打开与内容脚本轮询;flow runner 只提供 `flowId / nodeId / step / filterAfterTimestamp` 这类业务上下文。
|
||||
- “邮件从哪里读”由公共轮询调度层和 provider 决定,“哪封邮件算命中验证码”优先由规则注册层决定。
|
||||
- 新 flow 如需接入邮箱验证码,应优先新增自己的 mail rules builder,而不是继续改写 OpenAI 规则。
|
||||
|
||||
### 3.5 日志步骤号链路
|
||||
@@ -214,6 +216,7 @@
|
||||
- `contributionLastPollAt`
|
||||
- OAuth 链接
|
||||
- Kiro 运行态:使用 `kiroRuntime.session / register / webAuth / desktopAuth / upload` 命名空间,保存 Kiro 官方登录入口、Builder ID 注册页状态、Kiro Web 登录态摘要、桌面授权 PKCE 凭据、上传状态和当前 Kiro 会话进度
|
||||
- Grok 运行态:权威状态位于 `runtimeState.flowState.grok`,包含 `session / register / sso`;顶层 `grokRegisterTabId / grokPageState / grokEmail / grokPassword / grokSsoCookie / grokSsoCookies` 只作为兼容投影,不应当再形成第二套状态机
|
||||
- 当前轮冻结后的注册方式 `resolvedSignupMethod`
|
||||
- 当前统一账号标识 `accountIdentifierType / accountIdentifier`
|
||||
- 当前邮箱 / 密码
|
||||
@@ -762,9 +765,32 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
|
||||
- Kiro 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
|
||||
- Kiro 自动运行走通用 linear node runner,但执行器、运行态、页面驱动和上传器全部独立在 `background/kiro/*` 与 `content/kiro/*`
|
||||
- Kiro 运行态统一落在 `kiroRuntime` 命名空间,不再散落为多个平铺字段
|
||||
- Kiro 注册验证码与桌面授权验证码只在 runner 内计算业务时间窗,具体 AWS 邮件匹配规则由 `flows/kiro/mail-rules.js` 提供,provider 分派统一走 `background/flow-mail-polling.js`
|
||||
- `kiro.rs` 上传固定发送 BuilderId profileArn `arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX`,machineId 固定按 `sha256("KotlinNativeAPI/" + refreshToken)` 生成
|
||||
- Kiro 注册页覆盖 `app.kiro.dev` 与 AWS Builder ID 页面;桌面授权页只绑定 AWS authorize 页面,二者由后台按 source 动态注入,避免同域 AWS 页面被错误归到另一条 Kiro 子链路
|
||||
|
||||
## 6.3 Grok Flow 链路
|
||||
|
||||
Grok flow 的目标是完成 Grok / xAI 注册页自动化,并把 `sso` Cookie 作为 `webchat2api` 可用的本地导出产物。它不是 OpenAI 目标来源,也不是公开贡献 / 发布 flow。
|
||||
|
||||
链路如下:
|
||||
|
||||
1. sidepanel 把当前 flow 切到 `grok`,target 固定为 `webchat2api`
|
||||
2. 步骤 1 打开 Grok / xAI 注册入口,并清理本轮注册页运行态
|
||||
3. 步骤 2 通过共享邮箱服务生成或复用注册邮箱,并提交到 xAI 登录/注册页
|
||||
4. 步骤 3 调用 `background/flow-mail-polling.js`,由 `flows/grok/mail-rules.js` 构造 xAI / Grok 验证码规则并轮询邮箱验证码,再交给 Grok 内容脚本提交
|
||||
5. 步骤 4 填写资料和密码,推进注册完成页状态
|
||||
6. 步骤 5 先通过 `chrome.cookies.get` 从 `x.ai / grok.com / accounts.x.ai` 查找 `sso` Cookie,必要时 fallback 到 Grok 内容脚本读取 `document.cookie`;提取到的值保存到 `runtimeState.flowState.grok.sso` 并投影到 `grokSsoCookie / grokSsoCookies`
|
||||
7. sidepanel 只展示掩码后的 SSO 值,复制和导出才使用完整值;清空动作必须发 `CLEAR_GROK_SSO_COOKIES` 给后台,由 `flows/grok/background/state.js` 构造 patch,不能在 sidepanel 直接写 `chrome.storage`
|
||||
|
||||
这条链路的关键边界是:
|
||||
|
||||
- Grok 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
|
||||
- Grok 自动运行走通用 linear node runner,执行器、运行态、页面驱动和邮件规则全部放在 `flows/grok/*`
|
||||
- Grok 内容脚本不能全局 patch 浏览器原型;点击事件需要的坐标只能在局部事件参数里补齐
|
||||
- Grok SSO Cookie 是本地导出产物,`flows/grok/index.js` 的 `publicationTargets` 保持为空,`supportsAccountContribution` 保持为 `false`
|
||||
- `shared/contribution-registry.js` 的默认发布贡献校验只覆盖声明了发布目标或支持贡献的 flow;Grok 不应被自动纳入贡献 adapter 缺失检查。如果未来要把 Grok 改成正式发布 flow,必须先新增明确的 publication target 与 contribution adapter,再打开对应能力
|
||||
|
||||
## 7. 邮箱与 provider 链路
|
||||
|
||||
### 7.1 共享邮箱生成补充
|
||||
@@ -840,8 +866,11 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
|
||||
- `background/registration-email-state.js`
|
||||
统一维护当前注册邮箱、上一份比较基线邮箱、来源与更新时间,并提供 Duck 生成前解析比较基线、Step 8 清空当前邮箱但保留历史基线,以及 Step 8 `add-email` 写入邮箱时保留手机号身份的共享工具。
|
||||
|
||||
- `background/mail-rule-registry.js` + `flows/openai/mail-rules.js`
|
||||
统一构造 Step 4 / Step 8 的验证码规则 payload,区分注册验证码与登录/绑定验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到同一层。
|
||||
- `background/mail-rule-registry.js` + `flows/*/mail-rules.js`
|
||||
统一构造各 flow 的验证码规则 payload,区分 OpenAI 注册/登录验证码、Kiro 注册/桌面授权验证码与 Grok 注册验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到 flow mail rules 层。
|
||||
|
||||
- `background/flow-mail-polling.js`
|
||||
统一执行 flow 邮箱验证码轮询调度:读取当前邮箱配置,调用 `mailRuleRegistry.buildVerificationPollPayloadForNode`,分派 Hotmail / LuckMail / Cloudflare Temp Email / Cloud Mail / YYDS Mail 这类 API provider,并为 iCloud、2925 和普通网页邮箱准备会话与内容脚本轮询。Kiro 与后续新 flow 不应在各自 runner 内复制 provider 分派逻辑。
|
||||
|
||||
- `background/generated-email-helpers.js`
|
||||
在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入;Duck 生成前会按“侧栏当前可见邮箱 -> `registrationEmailState.current` -> `registrationEmailState.previous`”的顺序解析比较基线。
|
||||
@@ -1131,6 +1160,7 @@ Hide My Email 获取与管理链路:
|
||||
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
|
||||
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
|
||||
- fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口
|
||||
- Grok fresh-attempt reset 会清理注册页 runtime,但保留已提取的 `grokSsoCookies`,避免用户导出前被下一轮准备动作清空
|
||||
5. 执行 `runAutoSequenceFromStep`
|
||||
- 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog
|
||||
- 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
- `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod`。
|
||||
- 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod`。
|
||||
- flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。
|
||||
- 本地导出型 flow、发布型 flow、贡献型 flow 必须显式区分:只有声明了 `publicationTargets` 或明确支持贡献能力的 flow 才能进入发布/贡献 adapter 校验;例如 Grok / `webchat2api` SSO Cookie 导出属于本地导出 flow,不能因为有 target 就默认接入贡献模式。
|
||||
|
||||
### 1.7 日志步骤号原则
|
||||
|
||||
@@ -285,6 +286,7 @@
|
||||
- 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js`
|
||||
- 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js`
|
||||
- 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败
|
||||
- 如果新增 flow 只是本地导出产物,例如 Cookie、JSON、会话文件、手动复制值,必须保持 `publicationTargets` 为空并关闭 `supportsAccountContribution`;如果后续产品决定发布到远端服务,必须先设计 publication target、贡献 adapter、敏感字段脱敏和测试,再打开能力开关。
|
||||
|
||||
### 3.4.1 iCloud Hide My Email 维护补充
|
||||
|
||||
@@ -495,6 +497,7 @@ npm test
|
||||
15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交?
|
||||
16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态?
|
||||
17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId`?
|
||||
18. 如果新增或调整 flow target,我有没有确认它到底是本地导出、正式发布还是贡献 flow,并同步检查 `publicationTargets / supportsAccountContribution / contributionAdapterIds / shared/contribution-registry.js`?
|
||||
|
||||
## 9. 完成标准
|
||||
|
||||
|
||||
+17
-4
@@ -21,7 +21,7 @@
|
||||
- `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/`、`.github/`、`_metadata/`、`.vscode/` 等目录。
|
||||
- `LICENSE`:项目许可证文件。
|
||||
- `README.md`:面向使用者的精简说明文档,主要介绍项目用途、功能范围、快速开始与文档入口;不再承载过多技术实现细节。
|
||||
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled` 与 `stepExecutionRangeByFlow`,`kiro` flow 则走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路。
|
||||
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled` 与 `stepExecutionRangeByFlow`,`kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路,`grok` flow 走独立的“注册页 1-4 步 -> SSO Cookie 提取 5 步”链路。
|
||||
- `cloudmail-utils.js`:Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
|
||||
- `cloudflare-temp-email-utils.js`:Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
|
||||
- `gopay-utils.js`:GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。
|
||||
@@ -59,11 +59,14 @@
|
||||
- `flows/kiro/background/desktop-client.js`:Kiro 桌面授权协议层,负责桌面 OIDC client 注册、PKCE 参数生成、授权地址组装、callback 参数校验和授权码换 token。
|
||||
- `flows/kiro/background/desktop-authorize-runner.js`:Kiro 桌面授权执行器,负责步骤 7~8 的标签页打开、授权页轮询、localhost callback 捕获,以及桌面授权完成后的凭据落库。
|
||||
- `flows/kiro/background/publisher-kiro-rs.js`:Kiro 发布器,负责 `kiro.rs` 地址归一化、连通性探测、BuilderId profileArn 固定映射、machineId 计算、上传 payload 构建与最终凭据上传。
|
||||
- `flows/grok/background/state.js`:Grok 独立运行态模型与状态工具,负责 `runtimeState.flowState.grok.session / register / sso` 的默认值、归一化、兼容字段投影、节点完成回写、下游重置和 fresh-attempt keep-state 构建。
|
||||
- `flows/grok/background/register-runner.js`:Grok 注册页执行器,负责步骤 1~5 的编排、Grok/xAI 注册标签页管理、邮箱提交、验证码轮询提交、资料/密码提交、SSO Cookie 提取、去重保存与账号记录收尾。
|
||||
- `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。
|
||||
- `background/ip-proxy-provider-711proxy.js`:711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。
|
||||
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。
|
||||
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
|
||||
- `background/mail-rule-registry.js`:flow-aware 邮件规则注册表,负责按 `activeFlowId` 选择验证码规则构造器,并统一输出注册/登录验证码轮询 payload。
|
||||
- `background/flow-mail-polling.js`:flow-aware 邮件轮询调度层,负责读取共享邮箱配置、调用当前 flow 的 mail rules、分派 Hotmail / LuckMail / Cloudflare Temp Email / Cloud Mail / YYDS Mail API provider、准备 iCloud / 2925 / 网页邮箱会话,并统一通过邮箱内容脚本轮询验证码。
|
||||
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;手动改邮箱、手动取邮箱等入口也统一经这里落到 `setEmailState`,同步维护注册邮箱运行态;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline;手动 `EXECUTE_NODE` 会先按 `stepExecutionRangeByFlow` 校验当前节点是否允许执行。
|
||||
- `background/registration-email-state.js`:注册邮箱运行态共享模块,负责维护 `registrationEmailState = { current, previous, source, updatedAt }`,并提供“当前邮箱清空时保留上一比较基线”“Duck 生成前解析比较基线”“Step 8 `add-email` 写入邮箱时按需保留手机号身份”的统一工具。
|
||||
- `core/flow-kernel/runtime-state.js`:运行态视图与 patch 构造模块,负责把会话字段归一到 `runtimeState / sharedState / serviceState / flowState` 结构,并维护 flow-aware 的 session patch、节点状态默认值与兼容视图输出。
|
||||
@@ -152,6 +155,11 @@
|
||||
## `flows/`
|
||||
|
||||
- `flows/openai/mail-rules.js`:OpenAI flow 的验证码邮件规则定义,负责按注册/登录节点输出 code pattern、关键词、目标邮箱提示、2925 receive 弱匹配与轮询参数。
|
||||
- `flows/kiro/mail-rules.js`:Kiro flow 的 AWS Builder ID 邮件规则定义,负责按注册验证码节点与桌面授权验证码节点输出 AWS 发件人、主题、关键词、code pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。
|
||||
- `flows/grok/index.js`:Grok flow 定义,声明 `webchat2api` target、Grok 注册页 runtime source、Grok 专属设置分组、能力边界与本地 SSO 导出属性;该 flow 当前没有 publication target,也不支持贡献 adapter。
|
||||
- `flows/grok/workflow.js`:Grok workflow 定义,固定输出打开注册页、提交邮箱、提交验证码、提交资料、提取 SSO Cookie 五个节点。
|
||||
- `flows/grok/mail-rules.js`:Grok flow 的 xAI / Grok 验证码邮件规则定义,负责输出发件人、主题、关键词、`ABC-123` 与 6 位码 pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。
|
||||
- `flows/grok/content/register-page.js`:Grok 注册页内容脚本,负责五个 Grok 节点的 DOM 操作、页面状态读取与局部点击事件坐标补齐,不修改全局浏览器原型。
|
||||
|
||||
## `icons/`
|
||||
|
||||
@@ -178,9 +186,10 @@
|
||||
## `shared/`
|
||||
|
||||
- `core/flow-kernel/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界、步骤范围 UI 作用域,以及 Plus 模式下 `账号接入策略` 的可选项、可编辑性与最终生效值。
|
||||
- `core/flow-kernel/flow-registry.js`:flow/source/settings group 总注册表,定义 `openai / kiro` 两套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。
|
||||
- `core/flow-kernel/flow-registry.js`:flow/source/settings group 总注册表,定义 `openai / kiro / grok` 三套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。
|
||||
- `shared/kiro-timeouts.js`:Kiro 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。
|
||||
- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*` 与 `settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro 的 `flows.kiro.targetId / targets` 与 `stepExecutionRangeByFlow`。
|
||||
- `shared/contribution-registry.js`:贡献 adapter 与教程入口注册表,负责 OpenAI / Kiro 贡献 adapter、教程入口和发布贡献 flow 校验;默认发布校验只覆盖声明了 publication target 或支持贡献的 flow,Grok 这类本地 SSO 导出 flow 不自动纳入贡献 adapter 缺失检查。
|
||||
- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*` 与 `settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro/Grok 的 `flows.<flowId>.targetId / targets` 与 `stepExecutionRangeByFlow`。
|
||||
- `core/flow-kernel/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。
|
||||
|
||||
## `sidepanel/`
|
||||
@@ -270,7 +279,9 @@
|
||||
- `tests/background-mail2925-relogin-wait.test.js`:测试 2925 强制重登入口 URL、长登录超时与前后固定等待窗口。
|
||||
- `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。
|
||||
- `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。
|
||||
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI mail rules 模块接入,并覆盖验证码轮询 payload 构造。
|
||||
- `tests/background-flow-mail-polling-module.test.js`:测试 flow-aware 邮件轮询调度层对 API provider、网页邮箱 provider、2925 会话准备和 2925 上限恢复的共享分派行为。
|
||||
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI / Kiro mail rules 模块接入,并覆盖各 flow 验证码轮询 payload 构造。
|
||||
- `tests/background-grok-state-module.test.js`:测试 Grok 运行态 helper 的默认值、兼容字段投影、节点完成回写、下游重置、fresh-attempt keep-state 与 SSO Cookie 保留规则。
|
||||
- `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
|
||||
- `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。
|
||||
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。
|
||||
@@ -358,6 +369,7 @@
|
||||
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
|
||||
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
|
||||
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
|
||||
- `tests/contribution-registry.test.js`:测试贡献 adapter 注册、教程入口解析、默认发布贡献 flow 校验,以及 Grok 本地 SSO 导出 flow 不被自动纳入发布贡献检查。
|
||||
- `tests/flow-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。
|
||||
- `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。
|
||||
- `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。
|
||||
@@ -365,6 +377,7 @@
|
||||
- `tests/sidepanel-contribution-mode-flow-scope.test.js`:测试贡献模式只在 `openai` flow 内生效,不会污染 Kiro flow 的来源与面板作用域。
|
||||
- `tests/sidepanel-contribution-update-hint.test.js`:测试顶部贡献更新轻提示在公告、教程和托管 auto-run 提醒并存时的文案合成。
|
||||
- `tests/sidepanel-flow-source-registry.test.js`:测试 sidepanel 的 flow/source 选择器、Kiro 来源切换与显隐分组接线。
|
||||
- `tests/grok-runner.test.js`:测试 Grok 注册页执行器对邮箱提交、验证码轮询、资料提交、SSO Cookie 提取、账号记录收尾和完成状态持久化的关键链路。
|
||||
- `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先冻结当前轮数并刷新贡献内容更新摘要,且刷新失败或启动前状态回灌不会阻塞或改写自动流程启动目标。
|
||||
- `tests/sidepanel-auto-run-risk-warning.test.js`:测试自动运行在高轮数且未配置节点轮询时会弹出风险提醒。
|
||||
- `tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js`:测试 Cloudflare Temp Email 独立区域、随机子域名开关、域名列表恢复与提示文案联动。
|
||||
|
||||
Reference in New Issue
Block a user