重构多 flow 邮件轮询并接入 Grok SSO
This commit is contained in:
+162
-67
@@ -5,6 +5,8 @@ importScripts(
|
|||||||
'flows/openai/workflow.js',
|
'flows/openai/workflow.js',
|
||||||
'flows/kiro/index.js',
|
'flows/kiro/index.js',
|
||||||
'flows/kiro/workflow.js',
|
'flows/kiro/workflow.js',
|
||||||
|
'flows/grok/index.js',
|
||||||
|
'flows/grok/workflow.js',
|
||||||
'flows/index.js',
|
'flows/index.js',
|
||||||
'core/flow-kernel/flow-registry.js',
|
'core/flow-kernel/flow-registry.js',
|
||||||
'shared/contribution-registry.js',
|
'shared/contribution-registry.js',
|
||||||
@@ -34,9 +36,11 @@ importScripts(
|
|||||||
'core/flow-kernel/workflow-engine.js',
|
'core/flow-kernel/workflow-engine.js',
|
||||||
'core/flow-kernel/runtime-state.js',
|
'core/flow-kernel/runtime-state.js',
|
||||||
'flows/kiro/background/state.js',
|
'flows/kiro/background/state.js',
|
||||||
|
'flows/grok/background/state.js',
|
||||||
'flows/kiro/background/credential-artifact.js',
|
'flows/kiro/background/credential-artifact.js',
|
||||||
'background/contribution/adapters/kiro-builder-id.js',
|
'background/contribution/adapters/kiro-builder-id.js',
|
||||||
'flows/kiro/background/register-runner.js',
|
'flows/kiro/background/register-runner.js',
|
||||||
|
'flows/grok/background/register-runner.js',
|
||||||
'flows/kiro/background/desktop-client.js',
|
'flows/kiro/background/desktop-client.js',
|
||||||
'flows/kiro/background/desktop-authorize-runner.js',
|
'flows/kiro/background/desktop-authorize-runner.js',
|
||||||
'flows/kiro/background/publisher-kiro-rs.js',
|
'flows/kiro/background/publisher-kiro-rs.js',
|
||||||
@@ -44,6 +48,9 @@ importScripts(
|
|||||||
'background/signup-flow-helpers.js',
|
'background/signup-flow-helpers.js',
|
||||||
'background/mail-rule-registry.js',
|
'background/mail-rule-registry.js',
|
||||||
'flows/openai/mail-rules.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/message-router.js',
|
||||||
'background/verification-flow.js',
|
'background/verification-flow.js',
|
||||||
'background/auto-run-controller.js',
|
'background/auto-run-controller.js',
|
||||||
@@ -424,6 +431,7 @@ const runtimeStateHelpers = self.MultiPageBackgroundRuntimeState?.createRuntimeS
|
|||||||
defaultNodeStatuses: DEFAULT_NODE_STATUSES,
|
defaultNodeStatuses: DEFAULT_NODE_STATUSES,
|
||||||
}) || null;
|
}) || null;
|
||||||
const kiroStateHelpers = self.MultiPageBackgroundKiroState || null;
|
const kiroStateHelpers = self.MultiPageBackgroundKiroState || null;
|
||||||
|
const grokStateHelpers = self.MultiPageBackgroundGrokState || null;
|
||||||
const DEFAULT_REGISTRATION_EMAIL_STATE = registrationEmailStateHelpers?.DEFAULT_REGISTRATION_EMAIL_STATE || {
|
const DEFAULT_REGISTRATION_EMAIL_STATE = registrationEmailStateHelpers?.DEFAULT_REGISTRATION_EMAIL_STATE || {
|
||||||
current: '',
|
current: '',
|
||||||
previous: '',
|
previous: '',
|
||||||
@@ -496,6 +504,9 @@ function buildStateViewWithRuntimeState(state = {}) {
|
|||||||
if (kiroStateHelpers?.buildStateView) {
|
if (kiroStateHelpers?.buildStateView) {
|
||||||
nextState = kiroStateHelpers.buildStateView(nextState);
|
nextState = kiroStateHelpers.buildStateView(nextState);
|
||||||
}
|
}
|
||||||
|
if (grokStateHelpers?.buildStateView) {
|
||||||
|
nextState = grokStateHelpers.buildStateView(nextState);
|
||||||
|
}
|
||||||
return nextState;
|
return nextState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3809,20 +3820,20 @@ function buildSettingsStatePatchFromFlatUpdates(updates = {}) {
|
|||||||
assignIfUpdated('kiroRsKey', ['flows', 'kiro', 'targets', 'kiro-rs', 'apiKey']);
|
assignIfUpdated('kiroRsKey', ['flows', 'kiro', 'targets', 'kiro-rs', 'apiKey']);
|
||||||
|
|
||||||
if (hasUpdate('stepExecutionRangeByFlow') && isPlainObjectValue(updates.stepExecutionRangeByFlow)) {
|
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(
|
setSettingsStatePatchValue(
|
||||||
patch,
|
patch,
|
||||||
['flows', 'openai', 'autoRun', 'stepExecutionRange'],
|
['flows', flowId, 'autoRun', 'stepExecutionRange'],
|
||||||
updates.stepExecutionRangeByFlow.openai
|
range
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
if (isPlainObjectValue(updates.stepExecutionRangeByFlow.kiro)) {
|
|
||||||
setSettingsStatePatchValue(
|
|
||||||
patch,
|
|
||||||
['flows', 'kiro', 'autoRun', 'stepExecutionRange'],
|
|
||||||
updates.stepExecutionRangeByFlow.kiro
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return patch;
|
return patch;
|
||||||
@@ -3941,6 +3952,25 @@ function buildAutoRunFreshResetSettingsState(prevState = {}, activeFlowId = DEFA
|
|||||||
})
|
})
|
||||||
: currentSettingsState;
|
: currentSettingsState;
|
||||||
const normalizedStepExecutionRangeByFlow = normalizeStepExecutionRangeByFlow(prevState?.stepExecutionRangeByFlow || {});
|
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 = {
|
const nextSettingsStatePatch = {
|
||||||
activeFlowId,
|
activeFlowId,
|
||||||
services: {
|
services: {
|
||||||
@@ -3956,28 +3986,7 @@ function buildAutoRunFreshResetSettingsState(prevState = {}, activeFlowId = DEFA
|
|||||||
mode: prevState?.ipProxyMode,
|
mode: prevState?.ipProxyMode,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flows: {
|
flows: flowPatch,
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return mergeAutoRunKeepStateValue(currentSettingsState, nextSettingsStatePatch);
|
return mergeAutoRunKeepStateValue(currentSettingsState, nextSettingsStatePatch);
|
||||||
@@ -4021,6 +4030,9 @@ function buildFreshAutoRunKeepState(prevState = {}) {
|
|||||||
if (typeof kiroStateHelpers?.buildFreshKeepState === 'function') {
|
if (typeof kiroStateHelpers?.buildFreshKeepState === 'function') {
|
||||||
Object.assign(keepState, kiroStateHelpers.buildFreshKeepState(sourceState));
|
Object.assign(keepState, kiroStateHelpers.buildFreshKeepState(sourceState));
|
||||||
}
|
}
|
||||||
|
if (typeof grokStateHelpers?.buildFreshKeepState === 'function') {
|
||||||
|
Object.assign(keepState, grokStateHelpers.buildFreshKeepState(sourceState));
|
||||||
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(sourceState, 'settingsSchemaVersion')) {
|
if (Object.prototype.hasOwnProperty.call(sourceState, 'settingsSchemaVersion')) {
|
||||||
keepState.settingsSchemaVersion = Number(sourceState.settingsSchemaVersion) || 0;
|
keepState.settingsSchemaVersion = Number(sourceState.settingsSchemaVersion) || 0;
|
||||||
}
|
}
|
||||||
@@ -4329,6 +4341,28 @@ function broadcastDataUpdate(payload) {
|
|||||||
}).catch(() => { });
|
}).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 = {}) {
|
function broadcastIcloudAliasesChanged(payload = {}) {
|
||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
type: 'ICLOUD_ALIASES_CHANGED',
|
type: 'ICLOUD_ALIASES_CHANGED',
|
||||||
@@ -9607,6 +9641,14 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (String(stepKey || '').trim().toLowerCase().startsWith('grok-')) {
|
||||||
|
const grokResets = typeof grokStateHelpers?.buildDownstreamResetPatch === 'function'
|
||||||
|
? grokStateHelpers.buildDownstreamResetPatch(stepKey, state)
|
||||||
|
: {};
|
||||||
|
if (Object.keys(grokResets).length > 0) {
|
||||||
|
return grokResets;
|
||||||
|
}
|
||||||
|
}
|
||||||
const plusRuntimeResets = {
|
const plusRuntimeResets = {
|
||||||
plusCheckoutTabId: null,
|
plusCheckoutTabId: null,
|
||||||
plusCheckoutUrl: null,
|
plusCheckoutUrl: null,
|
||||||
@@ -10801,6 +10843,16 @@ async function handleNodeData(nodeId, payload) {
|
|||||||
}
|
}
|
||||||
return;
|
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);
|
const step = getStepIdByNodeIdForState(nodeId, state);
|
||||||
if (!Number.isInteger(step) || step <= 0) {
|
if (!Number.isInteger(step) || step <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -10858,6 +10910,11 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
|
|||||||
'kiro-start-desktop-authorize',
|
'kiro-start-desktop-authorize',
|
||||||
'kiro-complete-desktop-authorize',
|
'kiro-complete-desktop-authorize',
|
||||||
'kiro-upload-credential',
|
'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([
|
const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([
|
||||||
'fill-password',
|
'fill-password',
|
||||||
@@ -13369,8 +13426,9 @@ async function resumeAutoRun() {
|
|||||||
|
|
||||||
const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
|
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 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_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/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'];
|
||||||
|
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({
|
const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
|
||||||
chrome,
|
chrome,
|
||||||
addLog,
|
addLog,
|
||||||
@@ -13428,12 +13486,50 @@ const openAiMailRules = self.MultiPageOpenAiMailRules?.createOpenAiMailRules({
|
|||||||
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
||||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
|
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({
|
const mailRuleRegistry = self.MultiPageBackgroundMailRuleRegistry?.createMailRuleRegistry({
|
||||||
defaultFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
defaultFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||||
flowBuilders: {
|
flowBuilders: {
|
||||||
openai: openAiMailRules,
|
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({
|
const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({
|
||||||
addLog,
|
addLog,
|
||||||
buildVerificationPollPayload: mailRuleRegistry?.buildVerificationPollPayload,
|
buildVerificationPollPayload: mailRuleRegistry?.buildVerificationPollPayload,
|
||||||
@@ -13802,33 +13898,18 @@ const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKir
|
|||||||
chrome,
|
chrome,
|
||||||
ensureContentScriptReadyOnTab,
|
ensureContentScriptReadyOnTab,
|
||||||
completeNodeFromBackground,
|
completeNodeFromBackground,
|
||||||
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
|
|
||||||
ensureMail2925MailboxSession,
|
|
||||||
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||||
generatePassword,
|
generatePassword,
|
||||||
generateRandomName,
|
generateRandomName,
|
||||||
getMailConfig,
|
|
||||||
getTabId,
|
getTabId,
|
||||||
getState,
|
getState,
|
||||||
HOTMAIL_PROVIDER,
|
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
LUCKMAIL_PROVIDER,
|
|
||||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
|
||||||
CLOUD_MAIL_PROVIDER,
|
|
||||||
YYDS_MAIL_PROVIDER,
|
|
||||||
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
|
||||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
|
|
||||||
isRetryableContentScriptTransportError,
|
isRetryableContentScriptTransportError,
|
||||||
pollCloudflareTempEmailVerificationCode,
|
pollFlowVerificationCode: flowMailPollingService?.pollFlowVerificationCode,
|
||||||
pollCloudMailVerificationCode,
|
|
||||||
pollHotmailVerificationCode,
|
|
||||||
pollLuckmailVerificationCode,
|
|
||||||
pollYydsMailVerificationCode,
|
|
||||||
registerTab,
|
registerTab,
|
||||||
resolveSignupEmailForFlow,
|
resolveSignupEmailForFlow,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
sendToMailContentScriptResilient,
|
|
||||||
setPasswordState,
|
setPasswordState,
|
||||||
setState,
|
setState,
|
||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
@@ -13836,6 +13917,29 @@ const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKir
|
|||||||
waitForTabStableComplete,
|
waitForTabStableComplete,
|
||||||
KIRO_REGISTER_INJECT_FILES,
|
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?.({
|
const kiroBuilderIdContributionAdapter = self.MultiPageBackgroundKiroBuilderIdContributionAdapter?.createKiroBuilderIdContributionAdapter?.({
|
||||||
addLog,
|
addLog,
|
||||||
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||||
@@ -13867,31 +13971,16 @@ const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeR
|
|||||||
chrome,
|
chrome,
|
||||||
completeNodeFromBackground,
|
completeNodeFromBackground,
|
||||||
ensureContentScriptReadyOnTab,
|
ensureContentScriptReadyOnTab,
|
||||||
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
|
|
||||||
ensureMail2925MailboxSession,
|
|
||||||
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||||
getMailConfig,
|
|
||||||
getTabId,
|
getTabId,
|
||||||
getState,
|
getState,
|
||||||
HOTMAIL_PROVIDER,
|
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
KIRO_REGISTER_INJECT_FILES,
|
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,
|
maybeSubmitFlowContribution,
|
||||||
pollCloudflareTempEmailVerificationCode,
|
pollFlowVerificationCode: flowMailPollingService?.pollFlowVerificationCode,
|
||||||
pollCloudMailVerificationCode,
|
|
||||||
pollHotmailVerificationCode,
|
|
||||||
pollLuckmailVerificationCode,
|
|
||||||
pollYydsMailVerificationCode,
|
|
||||||
registerTab,
|
registerTab,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
sendToMailContentScriptResilient,
|
|
||||||
setState,
|
setState,
|
||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
throwIfStopped,
|
throwIfStopped,
|
||||||
@@ -14000,6 +14089,11 @@ const stepExecutorsByKey = {
|
|||||||
'kiro-start-desktop-authorize': (state) => kiroDesktopAuthorizeRunner.executeKiroStartDesktopAuthorize(state),
|
'kiro-start-desktop-authorize': (state) => kiroDesktopAuthorizeRunner.executeKiroStartDesktopAuthorize(state),
|
||||||
'kiro-complete-desktop-authorize': (state) => kiroDesktopAuthorizeRunner.executeKiroCompleteDesktopAuthorize(state),
|
'kiro-complete-desktop-authorize': (state) => kiroDesktopAuthorizeRunner.executeKiroCompleteDesktopAuthorize(state),
|
||||||
'kiro-upload-credential': (state) => kiroPublisher.executeKiroUploadCredential(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({
|
const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
|
||||||
addLog,
|
addLog,
|
||||||
@@ -14016,6 +14110,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
|||||||
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
|
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
|
||||||
clearAutoRunTimerAlarm,
|
clearAutoRunTimerAlarm,
|
||||||
clearFreeReusablePhoneActivation,
|
clearFreeReusablePhoneActivation,
|
||||||
|
clearGrokSsoCookies,
|
||||||
clearLuckmailRuntimeState,
|
clearLuckmailRuntimeState,
|
||||||
clearYydsMailRuntimeState,
|
clearYydsMailRuntimeState,
|
||||||
clearStopRequest,
|
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,
|
deleteAccountRunHistoryRecords,
|
||||||
clearAutoRunTimerAlarm,
|
clearAutoRunTimerAlarm,
|
||||||
clearFreeReusablePhoneActivation,
|
clearFreeReusablePhoneActivation,
|
||||||
|
clearGrokSsoCookies,
|
||||||
clearLuckmailRuntimeState,
|
clearLuckmailRuntimeState,
|
||||||
clearYydsMailRuntimeState,
|
clearYydsMailRuntimeState,
|
||||||
clearStopRequest,
|
clearStopRequest,
|
||||||
@@ -1165,6 +1166,13 @@
|
|||||||
return await clearFreeReusablePhoneActivation();
|
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': {
|
case 'SET_FREE_REUSABLE_PHONE': {
|
||||||
if (typeof setFreeReusablePhoneActivation !== 'function') {
|
if (typeof setFreeReusablePhoneActivation !== 'function') {
|
||||||
throw new Error('白嫖复用手机号记录能力未接入。');
|
throw new Error('白嫖复用手机号记录能力未接入。');
|
||||||
|
|||||||
+441
-335
@@ -17,6 +17,23 @@
|
|||||||
return value;
|
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 = {}) {
|
function normalizeStepExecutionRangeEntry(value = {}, fallback = {}) {
|
||||||
const source = isPlainObject(value) ? value : {};
|
const source = isPlainObject(value) ? value : {};
|
||||||
const fallbackSource = isPlainObject(fallback) ? fallback : {};
|
const fallbackSource = isPlainObject(fallback) ? fallback : {};
|
||||||
@@ -47,6 +64,19 @@
|
|||||||
const normalizeTargetId = typeof flowRegistry.normalizeTargetId === 'function'
|
const normalizeTargetId = typeof flowRegistry.normalizeTargetId === 'function'
|
||||||
? flowRegistry.normalizeTargetId
|
? flowRegistry.normalizeTargetId
|
||||||
: ((_flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase());
|
: ((_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 normalizePlusAccountAccessStrategy = (value = '') => {
|
||||||
const normalized = String(value || '').trim().toLowerCase();
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
if (normalized === 'sub2api_codex_session') {
|
if (normalized === 'sub2api_codex_session') {
|
||||||
@@ -58,6 +88,146 @@
|
|||||||
return 'oauth';
|
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() {
|
function buildDefaultSettingsState() {
|
||||||
return {
|
return {
|
||||||
schemaVersion: 5,
|
schemaVersion: 5,
|
||||||
@@ -75,67 +245,7 @@
|
|||||||
mode: 'account',
|
mode: 'account',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flows: {
|
flows: buildDefaultFlows(),
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +257,223 @@
|
|||||||
return cloneValue(resolvedValue);
|
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 = {}) {
|
function normalizeSettingsState(input = {}, options = {}) {
|
||||||
const defaults = buildDefaultSettingsState();
|
const defaults = buildDefaultSettingsState();
|
||||||
const nested = isPlainObject(input?.settingsState)
|
const nested = isPlainObject(input?.settingsState)
|
||||||
@@ -159,31 +486,7 @@
|
|||||||
?? defaults.activeFlowId,
|
?? defaults.activeFlowId,
|
||||||
defaults.activeFlowId
|
defaults.activeFlowId
|
||||||
);
|
);
|
||||||
const openaiSelectedTargetId = normalizeTargetId(
|
const normalized = {
|
||||||
'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 {
|
|
||||||
schemaVersion: Number(input?.settingsSchemaVersion || nested?.schemaVersion || defaults.schemaVersion) || defaults.schemaVersion,
|
schemaVersion: Number(input?.settingsSchemaVersion || nested?.schemaVersion || defaults.schemaVersion) || defaults.schemaVersion,
|
||||||
activeFlowId,
|
activeFlowId,
|
||||||
services: {
|
services: {
|
||||||
@@ -219,203 +522,22 @@
|
|||||||
).trim(),
|
).trim(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flows: {
|
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
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 = {}) {
|
function mergeSettingsState(baseValue = {}, patchValue = {}) {
|
||||||
@@ -425,24 +547,8 @@
|
|||||||
activeFlowId: patchValue?.activeFlowId ?? baseSettingsState.activeFlowId,
|
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({
|
return normalizeSettingsState({
|
||||||
settingsState: mergeRecursive(baseSettingsState, patchSettingsState),
|
settingsState: mergePlainObjects(baseSettingsState, patchSettingsState),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +565,7 @@
|
|||||||
return normalizeTargetId(
|
return normalizeTargetId(
|
||||||
normalizedFlowId,
|
normalizedFlowId,
|
||||||
flowSettings?.selectedTargetId,
|
flowSettings?.selectedTargetId,
|
||||||
normalizedFlowId === 'kiro' ? defaultKiroTargetId : defaultOpenAiTargetId
|
getDefaultTargetId(normalizedFlowId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,16 +582,16 @@
|
|||||||
|
|
||||||
function buildStepExecutionRangeByFlow(settingsState = {}) {
|
function buildStepExecutionRangeByFlow(settingsState = {}) {
|
||||||
const normalizedState = normalizeSettingsState(settingsState);
|
const normalizedState = normalizeSettingsState(settingsState);
|
||||||
return {
|
const defaults = buildDefaultSettingsState();
|
||||||
openai: normalizeStepExecutionRangeEntry(
|
return Object.fromEntries(
|
||||||
normalizedState?.flows?.openai?.autoRun?.stepExecutionRange,
|
Object.entries(normalizedState.flows || {}).map(([flowId, flowSettings]) => [
|
||||||
buildDefaultSettingsState().flows.openai.autoRun.stepExecutionRange
|
flowId,
|
||||||
),
|
normalizeStepExecutionRangeEntry(
|
||||||
kiro: normalizeStepExecutionRangeEntry(
|
flowSettings?.autoRun?.stepExecutionRange,
|
||||||
normalizedState?.flows?.kiro?.autoRun?.stepExecutionRange,
|
defaults.flows?.[flowId]?.autoRun?.stepExecutionRange || getDefaultStepExecutionRange(flowId)
|
||||||
buildDefaultSettingsState().flows.kiro.autoRun.stepExecutionRange
|
),
|
||||||
),
|
])
|
||||||
};
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSettingsView(settingsState = {}, baseInput = {}) {
|
function buildSettingsView(settingsState = {}, baseInput = {}) {
|
||||||
@@ -493,38 +599,38 @@
|
|||||||
const next = {
|
const next = {
|
||||||
...(isPlainObject(baseInput) ? cloneValue(baseInput) : {}),
|
...(isPlainObject(baseInput) ? cloneValue(baseInput) : {}),
|
||||||
};
|
};
|
||||||
const openaiState = normalizedState.flows.openai;
|
const openaiState = normalizedState.flows.openai || buildDefaultFlowSettings('openai');
|
||||||
const kiroState = normalizedState.flows.kiro;
|
const kiroState = normalizedState.flows.kiro || buildDefaultFlowSettings('kiro');
|
||||||
next.activeFlowId = normalizedState.activeFlowId;
|
next.activeFlowId = normalizedState.activeFlowId;
|
||||||
next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId);
|
next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId);
|
||||||
next.vpsUrl = openaiState.targets.cpa.vpsUrl;
|
next.vpsUrl = openaiState.targets.cpa?.vpsUrl || '';
|
||||||
next.vpsPassword = openaiState.targets.cpa.vpsPassword;
|
next.vpsPassword = openaiState.targets.cpa?.vpsPassword || '';
|
||||||
next.localCpaStep9Mode = openaiState.targets.cpa.localCpaStep9Mode;
|
next.localCpaStep9Mode = openaiState.targets.cpa?.localCpaStep9Mode || 'submit';
|
||||||
next.sub2apiUrl = openaiState.targets.sub2api.sub2apiUrl;
|
next.sub2apiUrl = openaiState.targets.sub2api?.sub2apiUrl || '';
|
||||||
next.sub2apiEmail = openaiState.targets.sub2api.sub2apiEmail;
|
next.sub2apiEmail = openaiState.targets.sub2api?.sub2apiEmail || '';
|
||||||
next.sub2apiPassword = openaiState.targets.sub2api.sub2apiPassword;
|
next.sub2apiPassword = openaiState.targets.sub2api?.sub2apiPassword || '';
|
||||||
next.sub2apiGroupName = openaiState.targets.sub2api.sub2apiGroupName;
|
next.sub2apiGroupName = openaiState.targets.sub2api?.sub2apiGroupName || 'codex';
|
||||||
next.sub2apiGroupNames = cloneValue(openaiState.targets.sub2api.sub2apiGroupNames);
|
next.sub2apiGroupNames = cloneValue(openaiState.targets.sub2api?.sub2apiGroupNames || ['codex', 'openai-plus']);
|
||||||
next.sub2apiAccountPriority = openaiState.targets.sub2api.sub2apiAccountPriority;
|
next.sub2apiAccountPriority = openaiState.targets.sub2api?.sub2apiAccountPriority || 1;
|
||||||
next.sub2apiDefaultProxyName = openaiState.targets.sub2api.sub2apiDefaultProxyName;
|
next.sub2apiDefaultProxyName = openaiState.targets.sub2api?.sub2apiDefaultProxyName || '';
|
||||||
next.codex2apiUrl = openaiState.targets.codex2api.codex2apiUrl;
|
next.codex2apiUrl = openaiState.targets.codex2api?.codex2apiUrl || '';
|
||||||
next.codex2apiAdminKey = openaiState.targets.codex2api.codex2apiAdminKey;
|
next.codex2apiAdminKey = openaiState.targets.codex2api?.codex2apiAdminKey || '';
|
||||||
next.customPassword = normalizedState.services.account.customPassword;
|
next.customPassword = normalizedState.services.account.customPassword;
|
||||||
next.signupMethod = openaiState.signup.signupMethod;
|
next.signupMethod = openaiState.signup?.signupMethod || 'email';
|
||||||
next.phoneVerificationEnabled = openaiState.signup.phoneVerificationEnabled;
|
next.phoneVerificationEnabled = Boolean(openaiState.signup?.phoneVerificationEnabled);
|
||||||
next.phoneSignupReloginAfterBindEmailEnabled = openaiState.signup.phoneSignupReloginAfterBindEmailEnabled;
|
next.phoneSignupReloginAfterBindEmailEnabled = Boolean(openaiState.signup?.phoneSignupReloginAfterBindEmailEnabled);
|
||||||
next.plusModeEnabled = openaiState.plus.plusModeEnabled;
|
next.plusModeEnabled = Boolean(openaiState.plus?.plusModeEnabled);
|
||||||
next.plusPaymentMethod = openaiState.plus.plusPaymentMethod;
|
next.plusPaymentMethod = openaiState.plus?.plusPaymentMethod || 'paypal-hosted';
|
||||||
next.plusAccountAccessStrategy = openaiState.plus.plusAccountAccessStrategy;
|
next.plusAccountAccessStrategy = openaiState.plus?.plusAccountAccessStrategy || 'oauth';
|
||||||
next.hostedCheckoutVerificationUrl = openaiState.plus.hostedCheckoutVerificationUrl;
|
next.hostedCheckoutVerificationUrl = openaiState.plus?.hostedCheckoutVerificationUrl || '';
|
||||||
next.hostedCheckoutPhoneNumber = openaiState.plus.hostedCheckoutPhoneNumber;
|
next.hostedCheckoutPhoneNumber = openaiState.plus?.hostedCheckoutPhoneNumber || '';
|
||||||
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus.plusHostedCheckoutOauthDelaySeconds;
|
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus?.plusHostedCheckoutOauthDelaySeconds ?? 3;
|
||||||
next.mailProvider = normalizedState.services.email.provider;
|
next.mailProvider = normalizedState.services.email.provider;
|
||||||
next.ipProxyEnabled = normalizedState.services.proxy.enabled;
|
next.ipProxyEnabled = normalizedState.services.proxy.enabled;
|
||||||
next.ipProxyService = normalizedState.services.proxy.provider;
|
next.ipProxyService = normalizedState.services.proxy.provider;
|
||||||
next.ipProxyMode = normalizedState.services.proxy.mode;
|
next.ipProxyMode = normalizedState.services.proxy.mode;
|
||||||
next.kiroRsUrl = kiroState.targets['kiro-rs'].baseUrl;
|
next.kiroRsUrl = kiroState.targets['kiro-rs']?.baseUrl || '';
|
||||||
next.kiroRsKey = kiroState.targets['kiro-rs'].apiKey;
|
next.kiroRsKey = kiroState.targets['kiro-rs']?.apiKey || '';
|
||||||
next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState);
|
next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState);
|
||||||
next.settingsSchemaVersion = normalizedState.schemaVersion;
|
next.settingsSchemaVersion = normalizedState.schemaVersion;
|
||||||
next.settingsState = cloneValue(normalizedState);
|
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',
|
id: 'kiro',
|
||||||
path: 'flows/kiro/',
|
path: 'flows/kiro/',
|
||||||
},
|
},
|
||||||
|
grok: {
|
||||||
|
id: 'grok',
|
||||||
|
path: 'flows/grok/',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function normalizeFlowId(value = '') {
|
function normalizeFlowId(value = '') {
|
||||||
@@ -32,10 +36,14 @@
|
|||||||
...baseEntry,
|
...baseEntry,
|
||||||
definition: normalized === 'openai'
|
definition: normalized === 'openai'
|
||||||
? (rootScope.MultiPageOpenAiFlowDefinition || null)
|
? (rootScope.MultiPageOpenAiFlowDefinition || null)
|
||||||
: (rootScope.MultiPageKiroFlowDefinition || null),
|
: (normalized === 'kiro'
|
||||||
|
? (rootScope.MultiPageKiroFlowDefinition || null)
|
||||||
|
: (rootScope.MultiPageGrokFlowDefinition || null)),
|
||||||
workflow: normalized === 'openai'
|
workflow: normalized === 'openai'
|
||||||
? (rootScope.MultiPageOpenAiWorkflow || null)
|
? (rootScope.MultiPageOpenAiWorkflow || null)
|
||||||
: (rootScope.MultiPageKiroWorkflow || null),
|
: (normalized === 'kiro'
|
||||||
|
? (rootScope.MultiPageKiroWorkflow || null)
|
||||||
|
: (rootScope.MultiPageGrokWorkflow || null)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,43 +14,6 @@
|
|||||||
'https://app.kiro.dev/*',
|
'https://app.kiro.dev/*',
|
||||||
'https://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) {
|
function isPlainObject(value) {
|
||||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
@@ -375,32 +338,17 @@
|
|||||||
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
|
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
|
||||||
completeNodeFromBackground,
|
completeNodeFromBackground,
|
||||||
ensureContentScriptReadyOnTab = null,
|
ensureContentScriptReadyOnTab = null,
|
||||||
ensureIcloudMailSession = null,
|
|
||||||
ensureMail2925MailboxSession = null,
|
|
||||||
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||||
getMailConfig = null,
|
|
||||||
getState = async () => ({}),
|
getState = async () => ({}),
|
||||||
getTabId = async () => null,
|
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,
|
isTabAlive = async () => false,
|
||||||
maybeSubmitFlowContribution = async () => null,
|
maybeSubmitFlowContribution = async () => null,
|
||||||
KIRO_REGISTER_INJECT_FILES = null,
|
KIRO_REGISTER_INJECT_FILES = null,
|
||||||
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
|
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
|
||||||
pollCloudflareTempEmailVerificationCode = null,
|
pollFlowVerificationCode = null,
|
||||||
pollCloudMailVerificationCode = null,
|
|
||||||
pollHotmailVerificationCode = null,
|
|
||||||
pollLuckmailVerificationCode = null,
|
|
||||||
pollYydsMailVerificationCode = null,
|
|
||||||
registerTab = async () => {},
|
registerTab = async () => {},
|
||||||
reuseOrCreateTab = async () => null,
|
reuseOrCreateTab = async () => null,
|
||||||
sendToContentScriptResilient = null,
|
sendToContentScriptResilient = null,
|
||||||
sendToMailContentScriptResilient = null,
|
|
||||||
setState = async () => {},
|
setState = async () => {},
|
||||||
sleepWithStop = async (ms) => {
|
sleepWithStop = async (ms) => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -658,44 +606,6 @@
|
|||||||
return password;
|
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 = {}) {
|
async function collectKiroWebSessionTabs(currentState = {}) {
|
||||||
const runtimeState = readKiroRuntime(currentState);
|
const runtimeState = readKiroRuntime(currentState);
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
@@ -901,131 +811,35 @@
|
|||||||
throw new Error(`Kiro Web 登录态尚未建立。请在自动打开的 Kiro 账号页登录后,从步骤 7 继续。${detail}`);
|
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 = '') {
|
async function pollDesktopOtpCode(step, state = {}, nodeId = '') {
|
||||||
if (typeof getMailConfig !== 'function') {
|
if (typeof pollFlowVerificationCode !== 'function') {
|
||||||
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱配置能力,无法继续执行。');
|
throw new Error('Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。');
|
||||||
}
|
|
||||||
const mail = getMailConfig(state);
|
|
||||||
if (mail?.error) {
|
|
||||||
throw new Error(mail.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtimeState = readKiroRuntime(state);
|
const runtimeState = readKiroRuntime(state);
|
||||||
const requestedAt = Math.max(0, Number(runtimeState.desktopAuth?.otpRequestedAt) || Date.now());
|
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)
|
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||||
: requestedAt;
|
: requestedAt;
|
||||||
const pollPayload = buildDesktopOtpPollPayload(step, state, mail, filterAfterTimestamp);
|
|
||||||
|
|
||||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
return pollFlowVerificationCode({
|
||||||
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
|
actionLabel: '桌面授权验证码',
|
||||||
await ensureIcloudMailSession({
|
filterAfterTimestamp,
|
||||||
state,
|
flowId: 'kiro',
|
||||||
step,
|
logStep: step,
|
||||||
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
|
logStepKey: 'kiro-complete-desktop-authorize',
|
||||||
});
|
missingCapabilityMessage: 'Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。',
|
||||||
}
|
nodeId: 'kiro-complete-desktop-authorize',
|
||||||
|
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`,
|
||||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
state: {
|
||||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询桌面授权验证码...`, 'info', nodeId);
|
...state,
|
||||||
return pollHotmailVerificationCode(step, state, pollPayload);
|
activeFlowId: 'kiro',
|
||||||
}
|
flowId: 'kiro',
|
||||||
if (mail.provider === LUCKMAIL_PROVIDER) {
|
visibleStep: step,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
{
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeKiroStartDesktopAuthorize(state = {}) {
|
async function executeKiroStartDesktopAuthorize(state = {}) {
|
||||||
|
|||||||
@@ -32,42 +32,6 @@
|
|||||||
'https://profile.aws.amazon.com',
|
'https://profile.aws.amazon.com',
|
||||||
]);
|
]);
|
||||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
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([
|
const KIRO_REGISTER_PAGE_STATES = Object.freeze([
|
||||||
'kiro_signin_page',
|
'kiro_signin_page',
|
||||||
'email_entry',
|
'email_entry',
|
||||||
@@ -387,32 +351,17 @@
|
|||||||
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
|
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
|
||||||
completeNodeFromBackground,
|
completeNodeFromBackground,
|
||||||
ensureContentScriptReadyOnTab = null,
|
ensureContentScriptReadyOnTab = null,
|
||||||
ensureIcloudMailSession = null,
|
|
||||||
ensureMail2925MailboxSession = null,
|
|
||||||
generatePassword = null,
|
generatePassword = null,
|
||||||
generateRandomName = null,
|
generateRandomName = null,
|
||||||
getMailConfig = null,
|
|
||||||
getState = async () => ({}),
|
getState = async () => ({}),
|
||||||
getTabId = async () => null,
|
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,
|
isTabAlive = async () => false,
|
||||||
KIRO_REGISTER_INJECT_FILES = null,
|
KIRO_REGISTER_INJECT_FILES = null,
|
||||||
pollCloudflareTempEmailVerificationCode = null,
|
pollFlowVerificationCode = null,
|
||||||
pollCloudMailVerificationCode = null,
|
|
||||||
pollHotmailVerificationCode = null,
|
|
||||||
pollLuckmailVerificationCode = null,
|
|
||||||
pollYydsMailVerificationCode = null,
|
|
||||||
registerTab = async () => {},
|
registerTab = async () => {},
|
||||||
resolveSignupEmailForFlow = null,
|
resolveSignupEmailForFlow = null,
|
||||||
reuseOrCreateTab = async () => null,
|
reuseOrCreateTab = async () => null,
|
||||||
sendToContentScriptResilient = null,
|
sendToContentScriptResilient = null,
|
||||||
sendToMailContentScriptResilient = null,
|
|
||||||
setPasswordState = async () => {},
|
setPasswordState = async () => {},
|
||||||
setState = async () => {},
|
setState = async () => {},
|
||||||
sleepWithStop = async (ms) => {
|
sleepWithStop = async (ms) => {
|
||||||
@@ -836,174 +785,36 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
|
||||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
|
||||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
|
||||||
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
|
|
||||||
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
|
|
||||||
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
|
|
||||||
if (accountEmail) {
|
|
||||||
return accountEmail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function focusOrOpenMailTab(mail) {
|
|
||||||
if (!mail?.source) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const alive = await isTabAlive(mail.source);
|
|
||||||
if (alive) {
|
|
||||||
if (mail.navigateOnReuse) {
|
|
||||||
await reuseOrCreateTab(mail.source, mail.url, {
|
|
||||||
inject: mail.inject,
|
|
||||||
injectSource: mail.injectSource,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabId = await getTabId(mail.source);
|
|
||||||
if (Number.isInteger(tabId)) {
|
|
||||||
await activateTab(tabId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await reuseOrCreateTab(mail.source, mail.url, {
|
|
||||||
inject: mail.inject,
|
|
||||||
injectSource: mail.injectSource,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildKiroVerificationPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
|
|
||||||
const runtimeState = readKiroRuntime(state);
|
|
||||||
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
|
|
||||||
const targetEmailHints = targetEmail ? [targetEmail] : [];
|
|
||||||
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
|
|
||||||
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
|
|
||||||
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
|
|
||||||
? 3
|
|
||||||
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
|
|
||||||
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
|
|
||||||
? 15000
|
|
||||||
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
|
|
||||||
|
|
||||||
return {
|
|
||||||
flowId: 'kiro',
|
|
||||||
step,
|
|
||||||
targetEmail,
|
|
||||||
targetEmailHints,
|
|
||||||
filterAfterTimestamp,
|
|
||||||
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
|
|
||||||
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
|
|
||||||
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
|
|
||||||
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
|
|
||||||
mail2925MatchTargetEmail: isMail2925Provider
|
|
||||||
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
|
|
||||||
maxAttempts,
|
|
||||||
intervalMs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMailPollingResponseTimeoutMs(payload = {}) {
|
|
||||||
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
|
|
||||||
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
|
|
||||||
return Math.max(45000, maxAttempts * intervalMs + 25000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollKiroVerificationCode(step, state = {}, nodeId = '') {
|
async function pollKiroVerificationCode(step, state = {}, nodeId = '') {
|
||||||
if (typeof getMailConfig !== 'function') {
|
if (typeof pollFlowVerificationCode !== 'function') {
|
||||||
throw new Error('Kiro 验证码步骤缺少邮箱配置能力,无法继续执行。');
|
throw new Error('Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。');
|
||||||
}
|
|
||||||
const mail = getMailConfig(state);
|
|
||||||
if (mail?.error) {
|
|
||||||
throw new Error(mail.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtimeState = readKiroRuntime(state);
|
const runtimeState = readKiroRuntime(state);
|
||||||
const recordedRequestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || 0);
|
const recordedRequestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || 0);
|
||||||
const requestedAt = recordedRequestedAt || Math.max(0, Date.now() - MAIL_2925_FILTER_LOOKBACK_MS);
|
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)
|
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||||
: requestedAt;
|
: requestedAt;
|
||||||
const pollPayload = buildKiroVerificationPollPayload(step, state, mail, filterAfterTimestamp);
|
|
||||||
|
|
||||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
return pollFlowVerificationCode({
|
||||||
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
|
actionLabel: 'Kiro 验证码',
|
||||||
await ensureIcloudMailSession({
|
filterAfterTimestamp,
|
||||||
state,
|
flowId: 'kiro',
|
||||||
step,
|
logStep: step,
|
||||||
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
|
logStepKey: 'kiro-submit-verification-code',
|
||||||
});
|
missingCapabilityMessage: 'Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。',
|
||||||
}
|
nodeId: 'kiro-submit-verification-code',
|
||||||
|
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到 Kiro 验证码。`,
|
||||||
throwIfStopped();
|
state: {
|
||||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
...state,
|
||||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询验证码...`, 'info', nodeId);
|
activeFlowId: 'kiro',
|
||||||
return pollHotmailVerificationCode(step, state, pollPayload);
|
flowId: 'kiro',
|
||||||
}
|
visibleStep: step,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
{
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeKiroOpenRegisterPage(state = {}) {
|
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",
|
"content/activation-utils.js",
|
||||||
"flows/openai/index.js",
|
"flows/openai/index.js",
|
||||||
"flows/kiro/index.js",
|
"flows/kiro/index.js",
|
||||||
|
"flows/grok/index.js",
|
||||||
"flows/index.js",
|
"flows/index.js",
|
||||||
"core/flow-kernel/flow-registry.js",
|
"core/flow-kernel/flow-registry.js",
|
||||||
"core/flow-kernel/source-registry.js",
|
"core/flow-kernel/source-registry.js",
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
"content/activation-utils.js",
|
"content/activation-utils.js",
|
||||||
"flows/openai/index.js",
|
"flows/openai/index.js",
|
||||||
"flows/kiro/index.js",
|
"flows/kiro/index.js",
|
||||||
|
"flows/grok/index.js",
|
||||||
"flows/index.js",
|
"flows/index.js",
|
||||||
"core/flow-kernel/flow-registry.js",
|
"core/flow-kernel/flow-registry.js",
|
||||||
"core/flow-kernel/source-registry.js",
|
"core/flow-kernel/source-registry.js",
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
"content/activation-utils.js",
|
"content/activation-utils.js",
|
||||||
"flows/openai/index.js",
|
"flows/openai/index.js",
|
||||||
"flows/kiro/index.js",
|
"flows/kiro/index.js",
|
||||||
|
"flows/grok/index.js",
|
||||||
"flows/index.js",
|
"flows/index.js",
|
||||||
"core/flow-kernel/flow-registry.js",
|
"core/flow-kernel/flow-registry.js",
|
||||||
"core/flow-kernel/source-registry.js",
|
"core/flow-kernel/source-registry.js",
|
||||||
@@ -111,6 +114,7 @@
|
|||||||
"content/activation-utils.js",
|
"content/activation-utils.js",
|
||||||
"flows/openai/index.js",
|
"flows/openai/index.js",
|
||||||
"flows/kiro/index.js",
|
"flows/kiro/index.js",
|
||||||
|
"flows/grok/index.js",
|
||||||
"flows/index.js",
|
"flows/index.js",
|
||||||
"core/flow-kernel/flow-registry.js",
|
"core/flow-kernel/flow-registry.js",
|
||||||
"core/flow-kernel/source-registry.js",
|
"core/flow-kernel/source-registry.js",
|
||||||
@@ -128,6 +132,7 @@
|
|||||||
"content/activation-utils.js",
|
"content/activation-utils.js",
|
||||||
"flows/openai/index.js",
|
"flows/openai/index.js",
|
||||||
"flows/kiro/index.js",
|
"flows/kiro/index.js",
|
||||||
|
"flows/grok/index.js",
|
||||||
"flows/index.js",
|
"flows/index.js",
|
||||||
"core/flow-kernel/flow-registry.js",
|
"core/flow-kernel/flow-registry.js",
|
||||||
"core/flow-kernel/source-registry.js",
|
"core/flow-kernel/source-registry.js",
|
||||||
|
|||||||
@@ -191,15 +191,44 @@
|
|||||||
return Boolean(normalizedAdapterId) && getContributionAdapterIds(flowId).includes(normalizedAdapterId);
|
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)
|
const ids = Array.isArray(flowIds)
|
||||||
? flowIds
|
? flowIds
|
||||||
: (typeof flowRegistryApi.getRegisteredFlowIds === 'function'
|
: (typeof flowRegistryApi.getRegisteredFlowIds === 'function'
|
||||||
? flowRegistryApi.getRegisteredFlowIds()
|
? flowRegistryApi.getRegisteredFlowIds()
|
||||||
: Object.keys(FLOW_ADAPTER_IDS));
|
: Object.keys(FLOW_ADAPTER_IDS));
|
||||||
const missing = ids
|
return ids
|
||||||
.map((flowId) => normalizeString(flowId).toLowerCase())
|
.map((flowId) => normalizeString(flowId).toLowerCase())
|
||||||
.filter(Boolean)
|
.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);
|
.filter((flowId) => getContributionAdapterIds(flowId).length === 0);
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
throw new Error(`缺少账号贡献适配器:${missing.join(', ')}`);
|
throw new Error(`缺少账号贡献适配器:${missing.join(', ')}`);
|
||||||
@@ -217,6 +246,7 @@
|
|||||||
getContributionAdapterIds,
|
getContributionAdapterIds,
|
||||||
getContributionAdapters,
|
getContributionAdapters,
|
||||||
getDefaultContributionAdapterId,
|
getDefaultContributionAdapterId,
|
||||||
|
getPublishedContributionFlowIds,
|
||||||
hasContributionAdapter,
|
hasContributionAdapter,
|
||||||
normalizeAdapterId,
|
normalizeAdapterId,
|
||||||
normalizeFlowId,
|
normalizeFlowId,
|
||||||
|
|||||||
@@ -126,6 +126,7 @@
|
|||||||
<select id="select-flow" class="data-select" aria-label="Flow 选择">
|
<select id="select-flow" class="data-select" aria-label="Flow 选择">
|
||||||
<option value="openai" selected>Codex / OpenAI</option>
|
<option value="openai" selected>Codex / OpenAI</option>
|
||||||
<option value="kiro">Kiro</option>
|
<option value="kiro">Kiro</option>
|
||||||
|
<option value="grok">Grok</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
@@ -290,6 +291,23 @@
|
|||||||
<span class="data-label">上传状态</span>
|
<span class="data-label">上传状态</span>
|
||||||
<span id="display-kiro-upload-status" class="data-value mono">未开始</span>
|
<span id="display-kiro-upload-status" class="data-value mono">未开始</span>
|
||||||
</div>
|
</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">
|
<div class="data-row module-divider-start" id="row-custom-password">
|
||||||
<span class="data-label">账户密码</span>
|
<span class="data-label">账户密码</span>
|
||||||
<div class="input-with-icon">
|
<div class="input-with-icon">
|
||||||
@@ -1881,6 +1899,8 @@
|
|||||||
<script src="../flows/openai/workflow.js"></script>
|
<script src="../flows/openai/workflow.js"></script>
|
||||||
<script src="../flows/kiro/index.js"></script>
|
<script src="../flows/kiro/index.js"></script>
|
||||||
<script src="../flows/kiro/workflow.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="../flows/index.js"></script>
|
||||||
<script src="../core/flow-kernel/flow-registry.js"></script>
|
<script src="../core/flow-kernel/flow-registry.js"></script>
|
||||||
<script src="../shared/contribution-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 displayKiroLoginUrl = document.getElementById('display-kiro-login-url');
|
||||||
const rowKiroUploadStatus = document.getElementById('row-kiro-upload-status');
|
const rowKiroUploadStatus = document.getElementById('row-kiro-upload-status');
|
||||||
const displayKiroUploadStatus = document.getElementById('display-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 rowCustomPassword = document.getElementById('row-custom-password');
|
||||||
const rowPlusMode = document.getElementById('row-plus-mode');
|
const rowPlusMode = document.getElementById('row-plus-mode');
|
||||||
const inputPlusModeEnabled = document.getElementById('input-plus-mode-enabled');
|
const inputPlusModeEnabled = document.getElementById('input-plus-mode-enabled');
|
||||||
@@ -2661,6 +2670,95 @@ function getKiroRuntimeState(state = {}) {
|
|||||||
return {};
|
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 = '') {
|
function setKiroRsConnectionTestStatus(message = '') {
|
||||||
const nextText = String(message || '').trim() || '未测试';
|
const nextText = String(message || '').trim() || '未测试';
|
||||||
kiroRsConnectionTestStatusText = nextText;
|
kiroRsConnectionTestStatusText = nextText;
|
||||||
@@ -11069,6 +11167,7 @@ function applySettingsState(state) {
|
|||||||
).trim();
|
).trim();
|
||||||
displayKiroUploadStatus.textContent = getKiroUploadStatusLabel(kiroUploadStatus);
|
displayKiroUploadStatus.textContent = getKiroUploadStatusLabel(kiroUploadStatus);
|
||||||
}
|
}
|
||||||
|
renderGrokRuntimeState(state);
|
||||||
const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService);
|
const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService);
|
||||||
const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function'
|
const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function'
|
||||||
? normalizeIpProxyServiceProfiles(state?.ipProxyServiceProfiles || {}, state || {})
|
? normalizeIpProxyServiceProfiles(state?.ipProxyServiceProfiles || {}, state || {})
|
||||||
@@ -13944,6 +14043,64 @@ async function copyTextToClipboard(text) {
|
|||||||
await navigator.clipboard.writeText(value);
|
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({
|
const hotmailManager = window.SidepanelHotmailManager?.createHotmailManager({
|
||||||
state: {
|
state: {
|
||||||
getLatestState: () => latestState,
|
getLatestState: () => latestState,
|
||||||
@@ -14590,7 +14747,9 @@ stepsList?.addEventListener('click', async (event) => {
|
|||||||
if (step === gpcCreateStep && !(await ensureGpcApiKeyReadyForStart())) {
|
if (step === gpcCreateStep && !(await ensureGpcApiKeyReadyForStart())) {
|
||||||
return;
|
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 || '')) {
|
if (shouldPersistSharedPassword && inputPassword.value !== (latestState?.customPassword || '')) {
|
||||||
await chrome.runtime.sendMessage({
|
await chrome.runtime.sendMessage({
|
||||||
type: 'SAVE_SETTING',
|
type: 'SAVE_SETTING',
|
||||||
@@ -17331,6 +17490,17 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
) {
|
) {
|
||||||
syncPasswordField(latestState || {});
|
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) {
|
if (message.payload.localCpaStep9Mode !== undefined) {
|
||||||
setLocalCpaStep9Mode(message.payload.localCpaStep9Mode);
|
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/);
|
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', () => {
|
test('background wires the Kiro register injector into desktop authorization runner', () => {
|
||||||
const source = fs.readFileSync('background.js', 'utf8');
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
const start = source.indexOf('const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({');
|
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,/);
|
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', () => {
|
test('kiro register consent step treats Kiro Web signed-in page as completion', () => {
|
||||||
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
|
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
|
||||||
assert.match(source, /readKiroRegisterPageState\(tabId, \{/);
|
assert.match(source, /readKiroRegisterPageState\(tabId, \{/);
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ const test = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const fs = require('node:fs');
|
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');
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
assert.match(source, /background\/mail-rule-registry\.js/);
|
assert.match(source, /background\/mail-rule-registry\.js/);
|
||||||
assert.match(source, /flows\/openai\/mail-rules\.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', () => {
|
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 的邮件轮询规则构造器/
|
/未找到 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 () => {
|
test('SAVE_SETTING syncs canonical kiro settingsState back into session state', async () => {
|
||||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||||
const globalScope = { console };
|
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-client\.js/);
|
||||||
assert.match(source, /flows\/kiro\/background\/desktop-authorize-runner\.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\/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.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/);
|
||||||
|
|
||||||
assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/);
|
assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/);
|
||||||
assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/);
|
assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/);
|
||||||
assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/);
|
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-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/);
|
||||||
assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(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-start-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroStartDesktopAuthorize\(state\)/);
|
||||||
assert.match(source, /'kiro-complete-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroCompleteDesktopAuthorize\(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, /'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(
|
assert.match(
|
||||||
source,
|
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'/
|
/'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', () => {
|
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', () => {
|
test('contribution registry fails fast when a published flow has no adapter', () => {
|
||||||
const api = loadApi();
|
const api = loadApi();
|
||||||
|
|
||||||
|
assert.deepEqual(api.getPublishedContributionFlowIds(), ['openai', 'kiro']);
|
||||||
|
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(), true);
|
||||||
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true);
|
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true);
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => api.assertPublishedFlowsHaveContributionAdapters(['openai', 'missing-flow']),
|
() => 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', () => {
|
test('flow capability registry exposes shared auto-run validation for phone locks and target support', () => {
|
||||||
const api = loadApi();
|
const api = loadApi();
|
||||||
const registry = api.createFlowCapabilityRegistry({
|
const registry = api.createFlowCapabilityRegistry({
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ function loadApis() {
|
|||||||
test('flow registry exposes canonical flow and target metadata', () => {
|
test('flow registry exposes canonical flow and target metadata', () => {
|
||||||
const { flowRegistry } = loadApis();
|
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('kiro'), 'kiro');
|
||||||
|
assert.equal(flowRegistry.normalizeFlowId('grok'), 'grok');
|
||||||
assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai');
|
assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai');
|
||||||
assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI');
|
assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI');
|
||||||
assert.equal(flowRegistry.normalizeTargetId('openai', 'sub2api'), 'sub2api');
|
assert.equal(flowRegistry.normalizeTargetId('openai', 'sub2api'), 'sub2api');
|
||||||
assert.equal(flowRegistry.normalizeTargetId('kiro', 'anything-else'), 'kiro-rs');
|
assert.equal(flowRegistry.normalizeTargetId('kiro', 'anything-else'), 'kiro-rs');
|
||||||
|
assert.equal(flowRegistry.normalizeTargetId('grok', 'anything-else'), 'webchat2api');
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
flowRegistry.getVisibleGroupIds('openai', 'cpa'),
|
flowRegistry.getVisibleGroupIds('openai', 'cpa'),
|
||||||
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy']
|
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-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'),
|
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
|
||||||
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
|
['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(
|
assert.deepEqual(
|
||||||
flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
|
flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
|
||||||
['cpa', 'sub2api', 'codex2api']
|
['cpa', 'sub2api', 'codex2api']
|
||||||
);
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
flowRegistry.getTargetOptions('grok').map((entry) => entry.id),
|
||||||
|
['webchat2api']
|
||||||
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
|
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
|
||||||
['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method']
|
['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.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
|
||||||
assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true);
|
assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true);
|
||||||
assert.equal(flowRegistry.getFlowCapabilities('kiro').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(
|
assert.deepEqual(
|
||||||
flowRegistry.getFlowCapabilities('openai').contributionAdapterIds,
|
flowRegistry.getFlowCapabilities('openai').contributionAdapterIds,
|
||||||
['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']
|
['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,
|
flowRegistry.getFlowCapabilities('kiro').contributionAdapterIds,
|
||||||
['kiro-builder-id']
|
['kiro-builder-id']
|
||||||
);
|
);
|
||||||
|
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').contributionAdapterIds, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('settings schema normalizes view input into canonical nested namespaces', () => {
|
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: {
|
stepExecutionRangeByFlow: {
|
||||||
openai: { enabled: true, fromStep: 2, toStep: 9 },
|
openai: { enabled: true, fromStep: 2, toStep: 9 },
|
||||||
kiro: { enabled: true, fromStep: 1, 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.selectedTargetId, 'cpa');
|
||||||
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
|
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||||
assert.equal(normalized.flows.kiro.selectedTargetId, 'kiro-rs');
|
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'].baseUrl, 'https://kiro.example.com/admin');
|
||||||
assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key');
|
assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key');
|
||||||
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
|
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
|
||||||
@@ -85,6 +100,11 @@ test('settings schema normalizes view input into canonical nested namespaces', (
|
|||||||
fromStep: 1,
|
fromStep: 1,
|
||||||
toStep: 9,
|
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', () => {
|
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.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||||
assert.equal(view.settingsSchemaVersion, 5);
|
assert.equal(view.settingsSchemaVersion, 5);
|
||||||
assert.equal(view.settingsState.activeFlowId, 'kiro');
|
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', () => {
|
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(normalized.flows.openai.plus.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||||
assert.equal(view.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([
|
const FLOW_DEFINITION_FILES = Object.freeze([
|
||||||
'flows/openai/index.js',
|
'flows/openai/index.js',
|
||||||
'flows/kiro/index.js',
|
'flows/kiro/index.js',
|
||||||
|
'flows/grok/index.js',
|
||||||
'flows/index.js',
|
'flows/index.js',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const FLOW_WORKFLOW_FILES = Object.freeze([
|
const FLOW_WORKFLOW_FILES = Object.freeze([
|
||||||
'flows/openai/workflow.js',
|
'flows/openai/workflow.js',
|
||||||
'flows/kiro/workflow.js',
|
'flows/kiro/workflow.js',
|
||||||
|
'flows/grok/workflow.js',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function readBundle(files = []) {
|
function readBundle(files = []) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ function extractFunction(source, name) {
|
|||||||
test('sidepanel html exposes flow selector and kiro source fields', () => {
|
test('sidepanel html exposes flow selector and kiro source fields', () => {
|
||||||
[
|
[
|
||||||
'id="select-flow"',
|
'id="select-flow"',
|
||||||
|
'<option value="grok">Grok</option>',
|
||||||
'id="label-source-selector"',
|
'id="label-source-selector"',
|
||||||
'id="row-step6-cookie-settings"',
|
'id="row-step6-cookie-settings"',
|
||||||
'id="row-kiro-rs-url"',
|
'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-web-status"',
|
||||||
'id="row-kiro-login-url"',
|
'id="row-kiro-login-url"',
|
||||||
'id="row-kiro-upload-status"',
|
'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) => {
|
].forEach((snippet) => {
|
||||||
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
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', () => {
|
test('sidepanel Kiro GitHub button opens the configured fork', () => {
|
||||||
|
|||||||
@@ -525,6 +525,7 @@ function updateAccountRunHistorySettingsUI() {}
|
|||||||
function updatePhoneVerificationSettingsUI() {}
|
function updatePhoneVerificationSettingsUI() {}
|
||||||
function updatePanelModeUI() {}
|
function updatePanelModeUI() {}
|
||||||
function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); }
|
function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); }
|
||||||
|
function renderGrokRuntimeState() {}
|
||||||
function renderSub2ApiGroupOptions() {}
|
function renderSub2ApiGroupOptions() {}
|
||||||
function isLuckmailProvider() { return false; }
|
function isLuckmailProvider() { return false; }
|
||||||
function updateButtonStates() {}
|
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\/settings-schema\.js/);
|
||||||
assert.match(source, /core\/flow-kernel\/source-registry\.js/);
|
assert.match(source, /core\/flow-kernel\/source-registry\.js/);
|
||||||
assert.match(source, /shared\/kiro-timeouts\.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', () => {
|
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);
|
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', () => {
|
test('background injects shared Kiro timeout module before Kiro content scripts', () => {
|
||||||
const source = fs.readFileSync('background.js', 'utf8');
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
assert.match(
|
assert.match(
|
||||||
source,
|
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(
|
assert.match(
|
||||||
source,
|
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'
|
'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(
|
assert.equal(
|
||||||
registry.detectSourceFromLocation({
|
registry.detectSourceFromLocation({
|
||||||
url: 'https://example.com/',
|
url: 'https://example.com/',
|
||||||
@@ -115,6 +157,22 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
|
|||||||
),
|
),
|
||||||
true
|
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(
|
assert.equal(
|
||||||
registry.matchesSourceUrlFamily(
|
registry.matchesSourceUrlFamily(
|
||||||
'kiro-desktop-authorize',
|
'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/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/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/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 goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
|
||||||
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
|
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
|
||||||
const kiroSteps = api.getSteps({ activeFlowId: 'kiro' });
|
const kiroSteps = api.getSteps({ activeFlowId: 'kiro' });
|
||||||
|
const grokSteps = api.getSteps({ activeFlowId: 'grok' });
|
||||||
|
|
||||||
assert.equal(Array.isArray(steps), true);
|
assert.equal(Array.isArray(steps), true);
|
||||||
assert.equal(steps.length, 11);
|
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.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19);
|
||||||
assert.equal(api.hasFlow('openai'), true);
|
assert.equal(api.hasFlow('openai'), true);
|
||||||
assert.equal(api.hasFlow('kiro'), true);
|
assert.equal(api.hasFlow('kiro'), true);
|
||||||
|
assert.equal(api.hasFlow('grok'), true);
|
||||||
assert.equal(api.hasFlow('site-a'), false);
|
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.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
|
||||||
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
|
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
|
||||||
assert.deepStrictEqual(
|
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[6].title, '创建 Plus Checkout');
|
||||||
assert.equal(plusSteps[8].title, 'PayPal 登录与授权');
|
assert.equal(plusSteps[8].title, 'PayPal 登录与授权');
|
||||||
|
|
||||||
|
|||||||
+40
-10
@@ -10,10 +10,11 @@
|
|||||||
|
|
||||||
## 1. 项目目标
|
## 1. 项目目标
|
||||||
|
|
||||||
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册两条主 flow:
|
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册三条主 flow:
|
||||||
|
|
||||||
- `openai`:OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路
|
- `openai`:OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路
|
||||||
- `kiro`:AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路
|
- `kiro`:AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路
|
||||||
|
- `grok`:Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 本地导出链路
|
||||||
|
|
||||||
它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路:
|
它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路:
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
- 自动确认 OAuth 同意页
|
- 自动确认 OAuth 同意页
|
||||||
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
|
- 把 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. 核心运行参与者
|
## 2. 核心运行参与者
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@
|
|||||||
- 向后台发送命令
|
- 向后台发送命令
|
||||||
- 接收后台广播并更新 UI
|
- 接收后台广播并更新 UI
|
||||||
- 动态渲染步骤列表
|
- 动态渲染步骤列表
|
||||||
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target,`kiro` flow 则使用独立的 `flows.kiro.targetId`
|
- 维护 `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 运行状态,不显示 OpenAI 接码 / Plus / 平台绑定配置
|
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态;Grok flow 只显示 `webchat2api` SSO 导出状态、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置
|
||||||
- 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源
|
- 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源
|
||||||
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
|
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
|
||||||
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
|
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
|
||||||
@@ -150,13 +151,13 @@
|
|||||||
- `runtimeState`:当前运行中的 node、nodeStatuses、activeRunId 等核心执行态
|
- `runtimeState`:当前运行中的 node、nodeStatuses、activeRunId 等核心执行态
|
||||||
- `sharedState`:automation window、tab registry、最近来源 URL 这类跨 flow 共享运行态
|
- `sharedState`:automation window、tab registry、最近来源 URL 这类跨 flow 共享运行态
|
||||||
- `serviceState`:代理、邮箱等共享服务运行态
|
- `serviceState`:代理、邮箱等共享服务运行态
|
||||||
- `flowState`:OpenAI / Kiro 各自的 flow 私有运行态
|
- `flowState`:OpenAI / Kiro / Grok 各自的 flow 私有运行态
|
||||||
|
|
||||||
这两个模块的作用,是把“当前该跑哪些节点”“当前 state 应该怎么看”“前后台恢复时如何合并 patch”从 `background.js` 主入口里抽离出来,避免再次退回到旧的平铺 step 状态机。
|
这两个模块的作用,是把“当前该跑哪些节点”“当前 state 应该怎么看”“前后台恢复时如何合并 patch”从 `background.js` 主入口里抽离出来,避免再次退回到旧的平铺 step 状态机。
|
||||||
|
|
||||||
### 3.4 步骤注册
|
### 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` 等稳定节点元数据。
|
[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 验证码规则注册
|
### 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 验证码过滤条件。
|
- `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 规则。
|
- 新 flow 如需接入邮箱验证码,应优先新增自己的 mail rules builder,而不是继续改写 OpenAI 规则。
|
||||||
|
|
||||||
### 3.5 日志步骤号链路
|
### 3.5 日志步骤号链路
|
||||||
@@ -214,6 +216,7 @@
|
|||||||
- `contributionLastPollAt`
|
- `contributionLastPollAt`
|
||||||
- OAuth 链接
|
- OAuth 链接
|
||||||
- Kiro 运行态:使用 `kiroRuntime.session / register / webAuth / desktopAuth / upload` 命名空间,保存 Kiro 官方登录入口、Builder ID 注册页状态、Kiro Web 登录态摘要、桌面授权 PKCE 凭据、上传状态和当前 Kiro 会话进度
|
- 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`
|
- 当前轮冻结后的注册方式 `resolvedSignupMethod`
|
||||||
- 当前统一账号标识 `accountIdentifierType / accountIdentifier`
|
- 当前统一账号标识 `accountIdentifierType / accountIdentifier`
|
||||||
- 当前邮箱 / 密码
|
- 当前邮箱 / 密码
|
||||||
@@ -762,9 +765,32 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
|
|||||||
- Kiro 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
|
- Kiro 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
|
||||||
- Kiro 自动运行走通用 linear node runner,但执行器、运行态、页面驱动和上传器全部独立在 `background/kiro/*` 与 `content/kiro/*`
|
- Kiro 自动运行走通用 linear node runner,但执行器、运行态、页面驱动和上传器全部独立在 `background/kiro/*` 与 `content/kiro/*`
|
||||||
- Kiro 运行态统一落在 `kiroRuntime` 命名空间,不再散落为多个平铺字段
|
- 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.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 子链路
|
- 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. 邮箱与 provider 链路
|
||||||
|
|
||||||
### 7.1 共享邮箱生成补充
|
### 7.1 共享邮箱生成补充
|
||||||
@@ -840,8 +866,11 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
|
|||||||
- `background/registration-email-state.js`
|
- `background/registration-email-state.js`
|
||||||
统一维护当前注册邮箱、上一份比较基线邮箱、来源与更新时间,并提供 Duck 生成前解析比较基线、Step 8 清空当前邮箱但保留历史基线,以及 Step 8 `add-email` 写入邮箱时保留手机号身份的共享工具。
|
统一维护当前注册邮箱、上一份比较基线邮箱、来源与更新时间,并提供 Duck 生成前解析比较基线、Step 8 清空当前邮箱但保留历史基线,以及 Step 8 `add-email` 写入邮箱时保留手机号身份的共享工具。
|
||||||
|
|
||||||
- `background/mail-rule-registry.js` + `flows/openai/mail-rules.js`
|
- `background/mail-rule-registry.js` + `flows/*/mail-rules.js`
|
||||||
统一构造 Step 4 / Step 8 的验证码规则 payload,区分注册验证码与登录/绑定验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到同一层。
|
统一构造各 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`
|
- `background/generated-email-helpers.js`
|
||||||
在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入;Duck 生成前会按“侧栏当前可见邮箱 -> `registrationEmailState.current` -> `registrationEmailState.previous`”的顺序解析比较基线。
|
在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入;Duck 生成前会按“侧栏当前可见邮箱 -> `registrationEmailState.current` -> `registrationEmailState.previous`”的顺序解析比较基线。
|
||||||
@@ -1131,6 +1160,7 @@ Hide My Email 获取与管理链路:
|
|||||||
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
|
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
|
||||||
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
|
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
|
||||||
- fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口
|
- fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口
|
||||||
|
- Grok fresh-attempt reset 会清理注册页 runtime,但保留已提取的 `grokSsoCookies`,避免用户导出前被下一轮准备动作清空
|
||||||
5. 执行 `runAutoSequenceFromStep`
|
5. 执行 `runAutoSequenceFromStep`
|
||||||
- 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog
|
- 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog
|
||||||
- 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点
|
- 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点
|
||||||
|
|||||||
@@ -171,6 +171,7 @@
|
|||||||
- `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod`。
|
- `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod`。
|
||||||
- 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod`。
|
- 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod`。
|
||||||
- flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。
|
- flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。
|
||||||
|
- 本地导出型 flow、发布型 flow、贡献型 flow 必须显式区分:只有声明了 `publicationTargets` 或明确支持贡献能力的 flow 才能进入发布/贡献 adapter 校验;例如 Grok / `webchat2api` SSO Cookie 导出属于本地导出 flow,不能因为有 target 就默认接入贡献模式。
|
||||||
|
|
||||||
### 1.7 日志步骤号原则
|
### 1.7 日志步骤号原则
|
||||||
|
|
||||||
@@ -285,6 +286,7 @@
|
|||||||
- 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js`
|
- 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js`
|
||||||
- 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js`
|
- 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js`
|
||||||
- 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败
|
- 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败
|
||||||
|
- 如果新增 flow 只是本地导出产物,例如 Cookie、JSON、会话文件、手动复制值,必须保持 `publicationTargets` 为空并关闭 `supportsAccountContribution`;如果后续产品决定发布到远端服务,必须先设计 publication target、贡献 adapter、敏感字段脱敏和测试,再打开能力开关。
|
||||||
|
|
||||||
### 3.4.1 iCloud Hide My Email 维护补充
|
### 3.4.1 iCloud Hide My Email 维护补充
|
||||||
|
|
||||||
@@ -495,6 +497,7 @@ npm test
|
|||||||
15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交?
|
15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交?
|
||||||
16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态?
|
16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态?
|
||||||
17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId`?
|
17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId`?
|
||||||
|
18. 如果新增或调整 flow target,我有没有确认它到底是本地导出、正式发布还是贡献 flow,并同步检查 `publicationTargets / supportsAccountContribution / contributionAdapterIds / shared/contribution-registry.js`?
|
||||||
|
|
||||||
## 9. 完成标准
|
## 9. 完成标准
|
||||||
|
|
||||||
|
|||||||
+17
-4
@@ -21,7 +21,7 @@
|
|||||||
- `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/`、`.github/`、`_metadata/`、`.vscode/` 等目录。
|
- `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/`、`.github/`、`_metadata/`、`.vscode/` 等目录。
|
||||||
- `LICENSE`:项目许可证文件。
|
- `LICENSE`:项目许可证文件。
|
||||||
- `README.md`:面向使用者的精简说明文档,主要介绍项目用途、功能范围、快速开始与文档入口;不再承载过多技术实现细节。
|
- `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 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
|
- `cloudmail-utils.js`:Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
|
||||||
- `cloudflare-temp-email-utils.js`:Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
|
- `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 与余额响应解析。
|
- `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-client.js`:Kiro 桌面授权协议层,负责桌面 OIDC client 注册、PKCE 参数生成、授权地址组装、callback 参数校验和授权码换 token。
|
||||||
- `flows/kiro/background/desktop-authorize-runner.js`:Kiro 桌面授权执行器,负责步骤 7~8 的标签页打开、授权页轮询、localhost callback 捕获,以及桌面授权完成后的凭据落库。
|
- `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/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-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。
|
||||||
- `background/ip-proxy-provider-711proxy.js`:711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。
|
- `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/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-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
|
||||||
- `background/mail-rule-registry.js`:flow-aware 邮件规则注册表,负责按 `activeFlowId` 选择验证码规则构造器,并统一输出注册/登录验证码轮询 payload。
|
- `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/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` 写入邮箱时按需保留手机号身份”的统一工具。
|
- `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、节点状态默认值与兼容视图输出。
|
- `core/flow-kernel/runtime-state.js`:运行态视图与 patch 构造模块,负责把会话字段归一到 `runtimeState / sharedState / serviceState / flowState` 结构,并维护 flow-aware 的 session patch、节点状态默认值与兼容视图输出。
|
||||||
@@ -152,6 +155,11 @@
|
|||||||
## `flows/`
|
## `flows/`
|
||||||
|
|
||||||
- `flows/openai/mail-rules.js`:OpenAI flow 的验证码邮件规则定义,负责按注册/登录节点输出 code pattern、关键词、目标邮箱提示、2925 receive 弱匹配与轮询参数。
|
- `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/`
|
## `icons/`
|
||||||
|
|
||||||
@@ -178,9 +186,10 @@
|
|||||||
## `shared/`
|
## `shared/`
|
||||||
|
|
||||||
- `core/flow-kernel/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界、步骤范围 UI 作用域,以及 Plus 模式下 `账号接入策略` 的可选项、可编辑性与最终生效值。
|
- `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 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。
|
- `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 判定。
|
- `core/flow-kernel/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。
|
||||||
|
|
||||||
## `sidepanel/`
|
## `sidepanel/`
|
||||||
@@ -270,7 +279,9 @@
|
|||||||
- `tests/background-mail2925-relogin-wait.test.js`:测试 2925 强制重登入口 URL、长登录超时与前后固定等待窗口。
|
- `tests/background-mail2925-relogin-wait.test.js`:测试 2925 强制重登入口 URL、长登录超时与前后固定等待窗口。
|
||||||
- `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。
|
- `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。
|
||||||
- `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。
|
- `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-module.test.js`:测试消息路由模块已接入且导出工厂。
|
||||||
- `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。
|
- `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。
|
||||||
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。
|
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。
|
||||||
@@ -358,6 +369,7 @@
|
|||||||
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
|
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
|
||||||
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
|
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
|
||||||
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
|
- `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-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。
|
||||||
- `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。
|
- `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。
|
||||||
- `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。
|
- `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-mode-flow-scope.test.js`:测试贡献模式只在 `openai` flow 内生效,不会污染 Kiro flow 的来源与面板作用域。
|
||||||
- `tests/sidepanel-contribution-update-hint.test.js`:测试顶部贡献更新轻提示在公告、教程和托管 auto-run 提醒并存时的文案合成。
|
- `tests/sidepanel-contribution-update-hint.test.js`:测试顶部贡献更新轻提示在公告、教程和托管 auto-run 提醒并存时的文案合成。
|
||||||
- `tests/sidepanel-flow-source-registry.test.js`:测试 sidepanel 的 flow/source 选择器、Kiro 来源切换与显隐分组接线。
|
- `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-content-refresh.test.js`:测试点击“自动”时会先冻结当前轮数并刷新贡献内容更新摘要,且刷新失败或启动前状态回灌不会阻塞或改写自动流程启动目标。
|
||||||
- `tests/sidepanel-auto-run-risk-warning.test.js`:测试自动运行在高轮数且未配置节点轮询时会弹出风险提醒。
|
- `tests/sidepanel-auto-run-risk-warning.test.js`:测试自动运行在高轮数且未配置节点轮询时会弹出风险提醒。
|
||||||
- `tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js`:测试 Cloudflare Temp Email 独立区域、随机子域名开关、域名列表恢复与提示文案联动。
|
- `tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js`:测试 Cloudflare Temp Email 独立区域、随机子域名开关、域名列表恢复与提示文案联动。
|
||||||
|
|||||||
Reference in New Issue
Block a user