重构多 flow 邮件轮询并接入 Grok SSO
This commit is contained in:
@@ -0,0 +1,691 @@
|
||||
(function attachBackgroundGrokRegisterRunner(root, factory) {
|
||||
root.MultiPageBackgroundGrokRegisterRunner = factory(root);
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokRegisterRunnerModule() {
|
||||
const GROK_SIGNUP_URL = 'https://accounts.x.ai/sign-up?redirect=grok-com';
|
||||
const GROK_REGISTER_PAGE_SOURCE_ID = 'grok-register-page';
|
||||
const DEFAULT_GROK_PAGE_TIMEOUT_MS = 90 * 1000;
|
||||
const GROK_POST_PROFILE_CF_WAIT_MS = 20 * 1000;
|
||||
const GROK_PRE_SSO_EXTRACT_WAIT_MS = 10 * 1000;
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
const GROK_COOKIE_CLEAR_DOMAINS = Object.freeze([
|
||||
'x.ai',
|
||||
'accounts.x.ai',
|
||||
'grok.com',
|
||||
]);
|
||||
|
||||
function cleanString(value = '') {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return error instanceof Error ? error.message : cleanString(error) || '未知错误';
|
||||
}
|
||||
|
||||
function createGeneratedPassword() {
|
||||
const alphabet = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*';
|
||||
let output = '';
|
||||
for (let index = 0; index < 18; index += 1) {
|
||||
output += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return `${output}aA1!`;
|
||||
}
|
||||
|
||||
function createGrokRegisterRunner(deps = {}) {
|
||||
const {
|
||||
addLog = async () => {},
|
||||
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTab = null,
|
||||
generatePassword = null,
|
||||
generateRandomName = null,
|
||||
getState = async () => ({}),
|
||||
getTabId = async () => null,
|
||||
isTabAlive = async () => false,
|
||||
pollFlowVerificationCode = null,
|
||||
registerTab = async () => {},
|
||||
resolveSignupEmailForFlow = null,
|
||||
reuseOrCreateTab = async () => null,
|
||||
sendToContentScriptResilient = null,
|
||||
setPasswordState = async () => {},
|
||||
setState = async () => {},
|
||||
sleepWithStop = async (ms) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
},
|
||||
throwIfStopped = () => {},
|
||||
waitForTabStableComplete = null,
|
||||
GROK_REGISTER_INJECT_FILES = null,
|
||||
markCurrentRegistrationAccountUsed = null,
|
||||
} = deps;
|
||||
|
||||
if (typeof completeNodeFromBackground !== 'function') {
|
||||
throw new Error('Grok register runner requires completeNodeFromBackground.');
|
||||
}
|
||||
|
||||
async function log(message, level = 'info', nodeId = '') {
|
||||
await addLog(message, level, nodeId ? { nodeId } : {});
|
||||
}
|
||||
|
||||
async function activateTab(tabId) {
|
||||
if (!Number.isInteger(tabId) || !chrome?.tabs?.update) {
|
||||
return;
|
||||
}
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
|
||||
async function getExecutionState(state = {}) {
|
||||
if (state && typeof state === 'object' && !Array.isArray(state) && Object.keys(state).length) {
|
||||
return state;
|
||||
}
|
||||
return getState();
|
||||
}
|
||||
|
||||
async function persistState(patch = {}) {
|
||||
await setState(patch);
|
||||
return patch;
|
||||
}
|
||||
|
||||
function buildGrokRuntimePatch(patch = {}) {
|
||||
return {
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: patch,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function completeNode(nodeId, patch = {}) {
|
||||
await persistState(patch);
|
||||
await completeNodeFromBackground(nodeId, patch);
|
||||
return patch;
|
||||
}
|
||||
|
||||
async function isUsableTabId(tabId) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof isTabAlive === 'function' && await isTabAlive(GROK_REGISTER_PAGE_SOURCE_ID)) {
|
||||
return true;
|
||||
}
|
||||
if (chrome?.tabs?.get) {
|
||||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||
return Boolean(tab?.id === tabId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function ensureGrokRegisterTab(state = {}, options = {}) {
|
||||
const existingTabId = Number(
|
||||
state?.grokRegisterTabId
|
||||
|| state?.runtimeState?.flowState?.grok?.session?.registerTabId
|
||||
|| state?.tabRegistry?.[GROK_REGISTER_PAGE_SOURCE_ID]?.tabId
|
||||
|| 0
|
||||
);
|
||||
if (Number.isInteger(existingTabId) && existingTabId > 0 && await isUsableTabId(existingTabId)) {
|
||||
await registerTab(GROK_REGISTER_PAGE_SOURCE_ID, existingTabId);
|
||||
return existingTabId;
|
||||
}
|
||||
|
||||
const tabId = await getTabId(GROK_REGISTER_PAGE_SOURCE_ID);
|
||||
if (Number.isInteger(tabId) && await isUsableTabId(tabId)) {
|
||||
await registerTab(GROK_REGISTER_PAGE_SOURCE_ID, tabId);
|
||||
return tabId;
|
||||
}
|
||||
|
||||
if (!options.openIfMissing) {
|
||||
throw new Error(options.missingMessage || '缺少 Grok 注册页,请先执行步骤 1。');
|
||||
}
|
||||
|
||||
const openedTabId = await reuseOrCreateTab(GROK_REGISTER_PAGE_SOURCE_ID, GROK_SIGNUP_URL, {
|
||||
inject: Array.isArray(GROK_REGISTER_INJECT_FILES) ? GROK_REGISTER_INJECT_FILES : null,
|
||||
injectSource: GROK_REGISTER_PAGE_SOURCE_ID,
|
||||
});
|
||||
if (!Number.isInteger(openedTabId)) {
|
||||
throw new Error('无法打开 Grok 注册页。');
|
||||
}
|
||||
await registerTab(GROK_REGISTER_PAGE_SOURCE_ID, openedTabId);
|
||||
return openedTabId;
|
||||
}
|
||||
|
||||
async function ensureContentReady(tabId, options = {}) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error('缺少 Grok 注册页标签页,无法连接内容脚本。');
|
||||
}
|
||||
if (typeof waitForTabStableComplete === 'function') {
|
||||
await waitForTabStableComplete(tabId, {
|
||||
timeoutMs: options.timeoutMs || DEFAULT_GROK_PAGE_TIMEOUT_MS,
|
||||
retryDelayMs: 300,
|
||||
stableMs: Number(options.stableMs) || 1200,
|
||||
initialDelayMs: Number(options.initialDelayMs) || 120,
|
||||
});
|
||||
}
|
||||
if (typeof ensureContentScriptReadyOnTab === 'function') {
|
||||
await ensureContentScriptReadyOnTab(GROK_REGISTER_PAGE_SOURCE_ID, tabId, {
|
||||
inject: Array.isArray(GROK_REGISTER_INJECT_FILES) ? GROK_REGISTER_INJECT_FILES : null,
|
||||
injectSource: GROK_REGISTER_PAGE_SOURCE_ID,
|
||||
timeoutMs: options.timeoutMs || DEFAULT_GROK_PAGE_TIMEOUT_MS,
|
||||
retryDelayMs: 700,
|
||||
logMessage: options.logMessage || 'Grok 注册页内容脚本未就绪,正在等待页面恢复...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function sendGrokCommand(nodeId, payload = {}, options = {}) {
|
||||
if (typeof sendToContentScriptResilient !== 'function') {
|
||||
throw new Error('Grok 注册页通信能力不可用。');
|
||||
}
|
||||
const result = await sendToContentScriptResilient(GROK_REGISTER_PAGE_SOURCE_ID, {
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId,
|
||||
step: options.step || 0,
|
||||
source: 'background',
|
||||
payload,
|
||||
}, {
|
||||
timeoutMs: options.timeoutMs || 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: options.logMessage || '',
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
function shouldClearGrokCookie(cookie = {}) {
|
||||
const domain = cleanString(cookie.domain).replace(/^\.+/, '').toLowerCase();
|
||||
return GROK_COOKIE_CLEAR_DOMAINS.some((target) => (
|
||||
domain === target || domain.endsWith(`.${target}`)
|
||||
));
|
||||
}
|
||||
|
||||
function buildCookieRemovalUrl(cookie = {}) {
|
||||
const host = cleanString(cookie.domain).replace(/^\.+/, '').toLowerCase();
|
||||
const path = cleanString(cookie.path) || '/';
|
||||
return `https://${host}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
async function clearGrokCookiesBeforeStep1() {
|
||||
if (!chrome?.cookies?.getAll || !chrome.cookies?.remove) {
|
||||
await log('步骤 1:当前浏览器不支持 cookies API,跳过 Grok Cookie 清理。', 'warn', 'grok-open-signup-page');
|
||||
return;
|
||||
}
|
||||
|
||||
const stores = chrome.cookies.getAllCookieStores
|
||||
? await chrome.cookies.getAllCookieStores()
|
||||
: [{ id: undefined }];
|
||||
let removedCount = 0;
|
||||
const seen = new Set();
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id;
|
||||
const cookies = await chrome.cookies.getAll(storeId ? { storeId } : {}).catch(() => []);
|
||||
for (const cookie of cookies || []) {
|
||||
if (!shouldClearGrokCookie(cookie)) {
|
||||
continue;
|
||||
}
|
||||
const key = [
|
||||
cookie.storeId || storeId || '',
|
||||
cookie.domain || '',
|
||||
cookie.path || '',
|
||||
cookie.name || '',
|
||||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
try {
|
||||
const details = {
|
||||
url: buildCookieRemovalUrl(cookie),
|
||||
name: cookie.name,
|
||||
};
|
||||
if (cookie.storeId) {
|
||||
details.storeId = cookie.storeId;
|
||||
}
|
||||
if (cookie.partitionKey) {
|
||||
details.partitionKey = cookie.partitionKey;
|
||||
}
|
||||
const removed = await chrome.cookies.remove(details);
|
||||
if (removed) {
|
||||
removedCount += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MultiPage:grok-register] remove cookie failed', {
|
||||
domain: cookie?.domain,
|
||||
name: cookie?.name,
|
||||
message: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
await log(`步骤 1:已清理 Grok/xAI Cookie ${removedCount} 个。`, removedCount ? 'ok' : 'info', 'grok-open-signup-page');
|
||||
}
|
||||
|
||||
function resolveProfile(currentState = {}) {
|
||||
const firstFromState = cleanString(currentState.grokFirstName);
|
||||
const lastFromState = cleanString(currentState.grokLastName);
|
||||
if (firstFromState && lastFromState) {
|
||||
return {
|
||||
firstName: firstFromState,
|
||||
lastName: lastFromState,
|
||||
};
|
||||
}
|
||||
const generated = typeof generateRandomName === 'function' ? generateRandomName() : null;
|
||||
const fullName = cleanString(generated?.fullName || generated?.name || 'Alex Morgan');
|
||||
const parts = fullName.split(/\s+/).filter(Boolean);
|
||||
return {
|
||||
firstName: firstFromState || cleanString(generated?.firstName || parts[0] || 'Alex'),
|
||||
lastName: lastFromState || cleanString(generated?.lastName || parts.slice(1).join(' ') || 'Morgan'),
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePassword(currentState = {}) {
|
||||
return cleanString(currentState.grokPassword || currentState.customPassword || currentState.password)
|
||||
|| (typeof generatePassword === 'function' ? generatePassword() : createGeneratedPassword());
|
||||
}
|
||||
|
||||
function normalizeGrokVerificationCode(value = '') {
|
||||
return cleanString(value).replace(/[^A-Za-z0-9]/g, '');
|
||||
}
|
||||
|
||||
async function readSsoCookieFromChrome() {
|
||||
if (!chrome?.cookies?.get) {
|
||||
return '';
|
||||
}
|
||||
const candidates = [
|
||||
{ url: 'https://x.ai/', name: 'sso' },
|
||||
{ url: 'https://grok.com/', name: 'sso' },
|
||||
{ url: 'https://accounts.x.ai/', name: 'sso' },
|
||||
];
|
||||
for (const details of candidates) {
|
||||
const cookie = await chrome.cookies.get(details).catch(() => null);
|
||||
const value = cleanString(cookie?.value);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function executeGrokOpenSignupPage(state = {}) {
|
||||
const nodeId = cleanString(state?.nodeId) || 'grok-open-signup-page';
|
||||
const currentState = await getExecutionState(state);
|
||||
try {
|
||||
await clearGrokCookiesBeforeStep1();
|
||||
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: true });
|
||||
await activateTab(tabId);
|
||||
await persistState({
|
||||
grokRegisterTabId: tabId,
|
||||
grokSignupUrl: GROK_SIGNUP_URL,
|
||||
...buildGrokRuntimePatch({
|
||||
session: {
|
||||
registerTabId: tabId,
|
||||
startedAt: Date.now(),
|
||||
pageUrl: GROK_SIGNUP_URL,
|
||||
lastError: '',
|
||||
},
|
||||
}),
|
||||
});
|
||||
await ensureContentReady(tabId);
|
||||
const result = await sendGrokCommand(nodeId, {}, {
|
||||
step: 1,
|
||||
timeoutMs: DEFAULT_GROK_PAGE_TIMEOUT_MS,
|
||||
logMessage: '步骤 1:正在打开 Grok 邮箱注册入口...',
|
||||
});
|
||||
await log('步骤 1:已打开 Grok 邮箱注册页。', 'ok', nodeId);
|
||||
await completeNode(nodeId, {
|
||||
grokRegisterTabId: tabId,
|
||||
grokPageState: result.state || 'email_signup_ready',
|
||||
grokPageUrl: result.url || GROK_SIGNUP_URL,
|
||||
...buildGrokRuntimePatch({
|
||||
session: {
|
||||
registerTabId: tabId,
|
||||
startedAt: Date.now(),
|
||||
pageState: result.state || 'email_signup_ready',
|
||||
pageUrl: result.url || GROK_SIGNUP_URL,
|
||||
lastError: '',
|
||||
},
|
||||
register: {
|
||||
status: 'signup_page_opened',
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
await persistState(buildGrokRuntimePatch({
|
||||
session: {
|
||||
lastError: message,
|
||||
},
|
||||
}));
|
||||
await log(`步骤 1:${message}`, 'error', nodeId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeGrokSubmitEmail(state = {}) {
|
||||
const nodeId = cleanString(state?.nodeId) || 'grok-submit-email';
|
||||
const currentState = await getExecutionState(state);
|
||||
try {
|
||||
if (typeof resolveSignupEmailForFlow !== 'function') {
|
||||
throw new Error('Grok 邮箱步骤缺少公共邮箱解析能力,无法继续执行。');
|
||||
}
|
||||
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
|
||||
await activateTab(tabId);
|
||||
await ensureContentReady(tabId);
|
||||
const resolvedEmail = await resolveSignupEmailForFlow(currentState, {
|
||||
preserveAccountIdentity: true,
|
||||
});
|
||||
const email = cleanString(resolvedEmail).toLowerCase();
|
||||
if (!email) {
|
||||
throw new Error('Grok 注册邮箱为空,无法继续执行。');
|
||||
}
|
||||
const requestedAt = Date.now();
|
||||
await persistState({
|
||||
grokEmail: email,
|
||||
email,
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: email,
|
||||
...buildGrokRuntimePatch({
|
||||
register: {
|
||||
email,
|
||||
verificationRequestedAt: requestedAt,
|
||||
status: 'email_submitting',
|
||||
},
|
||||
}),
|
||||
});
|
||||
const result = await sendGrokCommand(nodeId, { email }, {
|
||||
step: 2,
|
||||
logMessage: '步骤 2:正在提交 Grok 注册邮箱...',
|
||||
});
|
||||
await log(`步骤 2:已提交 Grok 注册邮箱 ${email}。`, 'ok', nodeId);
|
||||
await completeNode(nodeId, {
|
||||
grokEmail: email,
|
||||
grokVerificationRequestedAt: requestedAt,
|
||||
grokPageState: result.state || '',
|
||||
grokPageUrl: result.url || '',
|
||||
email,
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: email,
|
||||
...buildGrokRuntimePatch({
|
||||
session: {
|
||||
pageState: result.state || '',
|
||||
pageUrl: result.url || '',
|
||||
lastError: '',
|
||||
},
|
||||
register: {
|
||||
email,
|
||||
verificationRequestedAt: requestedAt,
|
||||
status: 'verification_requested',
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
await persistState(buildGrokRuntimePatch({
|
||||
session: {
|
||||
lastError: message,
|
||||
},
|
||||
register: {
|
||||
status: 'error',
|
||||
},
|
||||
}));
|
||||
await log(`步骤 2:${message}`, 'error', nodeId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeGrokSubmitVerificationCode(state = {}) {
|
||||
const nodeId = cleanString(state?.nodeId) || 'grok-submit-verification-code';
|
||||
const currentState = await getExecutionState(state);
|
||||
try {
|
||||
if (typeof pollFlowVerificationCode !== 'function') {
|
||||
throw new Error('Grok 验证码步骤缺少共享邮件轮询能力,无法继续执行。');
|
||||
}
|
||||
const requestedAt = Math.max(
|
||||
0,
|
||||
Number(
|
||||
currentState.grokVerificationRequestedAt
|
||||
|| currentState.runtimeState?.flowState?.grok?.register?.verificationRequestedAt
|
||||
) || Date.now()
|
||||
);
|
||||
const filterAfterTimestamp = cleanString(currentState?.mailProvider).toLowerCase() === '2925'
|
||||
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: requestedAt;
|
||||
const email = cleanString(
|
||||
currentState.grokEmail
|
||||
|| currentState.runtimeState?.flowState?.grok?.register?.email
|
||||
|| currentState.email
|
||||
).toLowerCase();
|
||||
const pollResult = await pollFlowVerificationCode({
|
||||
actionLabel: 'Grok 验证码',
|
||||
filterAfterTimestamp,
|
||||
flowId: 'grok',
|
||||
logStep: 3,
|
||||
logStepKey: nodeId,
|
||||
nodeId,
|
||||
notFoundMessage: '步骤 3:邮箱轮询结束,但未获取到 xAI 验证码。',
|
||||
state: {
|
||||
...currentState,
|
||||
activeFlowId: 'grok',
|
||||
flowId: 'grok',
|
||||
visibleStep: 3,
|
||||
grokEmail: email,
|
||||
email,
|
||||
},
|
||||
step: 3,
|
||||
});
|
||||
const code = normalizeGrokVerificationCode(pollResult?.code);
|
||||
if (!code) {
|
||||
throw new Error('未能获取到 xAI 邮箱验证码。');
|
||||
}
|
||||
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
|
||||
await activateTab(tabId);
|
||||
await ensureContentReady(tabId);
|
||||
const result = await sendGrokCommand(nodeId, { code }, {
|
||||
step: 3,
|
||||
logMessage: '步骤 3:正在填写 xAI 邮箱验证码...',
|
||||
});
|
||||
await log(`步骤 3:已提交 xAI 邮箱验证码,当前页面状态:${result.state || 'unknown'}。`, 'ok', nodeId);
|
||||
await completeNode(nodeId, {
|
||||
grokVerificationCode: code,
|
||||
grokVerificationRawCode: cleanString(pollResult?.code),
|
||||
grokVerificationMessageId: cleanString(pollResult?.messageId || pollResult?.mailId),
|
||||
grokPageState: result.state || '',
|
||||
grokPageUrl: result.url || '',
|
||||
...buildGrokRuntimePatch({
|
||||
session: {
|
||||
pageState: result.state || '',
|
||||
pageUrl: result.url || '',
|
||||
lastError: '',
|
||||
},
|
||||
register: {
|
||||
verificationCode: code,
|
||||
status: 'verified',
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
await persistState(buildGrokRuntimePatch({
|
||||
session: {
|
||||
lastError: message,
|
||||
},
|
||||
register: {
|
||||
status: 'error',
|
||||
},
|
||||
}));
|
||||
await log(`步骤 3:${message}`, 'error', nodeId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeGrokSubmitProfile(state = {}) {
|
||||
const nodeId = cleanString(state?.nodeId) || 'grok-submit-profile';
|
||||
const currentState = await getExecutionState(state);
|
||||
try {
|
||||
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
|
||||
const profile = resolveProfile(currentState);
|
||||
const password = resolvePassword(currentState);
|
||||
await persistState({
|
||||
grokFirstName: profile.firstName,
|
||||
grokLastName: profile.lastName,
|
||||
grokPassword: password,
|
||||
...buildGrokRuntimePatch({
|
||||
register: {
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
password,
|
||||
status: 'profile_submitting',
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (typeof setPasswordState === 'function') {
|
||||
await setPasswordState(password);
|
||||
}
|
||||
await activateTab(tabId);
|
||||
await ensureContentReady(tabId);
|
||||
const result = await sendGrokCommand(nodeId, {
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
password,
|
||||
}, {
|
||||
step: 4,
|
||||
logMessage: '步骤 4:正在填写 xAI 注册资料...',
|
||||
});
|
||||
await log(`步骤 4:已提交 Grok 注册资料,等待 ${Math.floor(GROK_POST_PROFILE_CF_WAIT_MS / 1000)} 秒完成注册验证...`, 'info', nodeId);
|
||||
await sleepWithStop(GROK_POST_PROFILE_CF_WAIT_MS);
|
||||
await ensureContentReady(tabId, { timeoutMs: DEFAULT_GROK_PAGE_TIMEOUT_MS });
|
||||
await log('步骤 4:已提交 Grok 注册资料并完成等待。', 'ok', nodeId);
|
||||
await completeNode(nodeId, {
|
||||
grokFirstName: profile.firstName,
|
||||
grokLastName: profile.lastName,
|
||||
grokPassword: password,
|
||||
grokPageState: result.state || 'profile_submitted',
|
||||
grokPageUrl: result.url || '',
|
||||
...buildGrokRuntimePatch({
|
||||
session: {
|
||||
pageState: result.state || 'profile_submitted',
|
||||
pageUrl: result.url || '',
|
||||
lastError: '',
|
||||
},
|
||||
register: {
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
password,
|
||||
status: 'profile_submitted',
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
await persistState(buildGrokRuntimePatch({
|
||||
session: {
|
||||
lastError: message,
|
||||
},
|
||||
register: {
|
||||
status: 'error',
|
||||
},
|
||||
}));
|
||||
await log(`步骤 4:${message}`, 'error', nodeId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeGrokExtractSsoCookie(state = {}) {
|
||||
const nodeId = cleanString(state?.nodeId) || 'grok-extract-sso-cookie';
|
||||
const currentState = await getExecutionState(state);
|
||||
try {
|
||||
const tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
|
||||
await activateTab(tabId);
|
||||
await log(`步骤 5:等待 ${Math.floor(GROK_PRE_SSO_EXTRACT_WAIT_MS / 1000)} 秒后提取 Grok SSO...`, 'info', nodeId);
|
||||
await sleepWithStop(GROK_PRE_SSO_EXTRACT_WAIT_MS);
|
||||
|
||||
let ssoCookie = await readSsoCookieFromChrome();
|
||||
if (!ssoCookie) {
|
||||
await ensureContentReady(tabId);
|
||||
const result = await sendGrokCommand(nodeId, {}, {
|
||||
step: 5,
|
||||
logMessage: '步骤 5:正在从 Grok 注册页读取 sso Cookie...',
|
||||
});
|
||||
ssoCookie = cleanString(result?.ssoCookie);
|
||||
}
|
||||
if (!ssoCookie) {
|
||||
throw new Error('未找到 x.ai/grok sso Cookie。');
|
||||
}
|
||||
|
||||
const latestState = await getState();
|
||||
const existingSsoCookies = Array.isArray(latestState?.grokSsoCookies)
|
||||
? latestState.grokSsoCookies
|
||||
.map((entry) => cleanString(entry))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const nextSsoCookies = existingSsoCookies.includes(ssoCookie)
|
||||
? existingSsoCookies
|
||||
: [...existingSsoCookies, ssoCookie];
|
||||
const completedAt = Date.now();
|
||||
const completionPatch = {
|
||||
grokSsoCookie: ssoCookie,
|
||||
grokSsoCookies: nextSsoCookies,
|
||||
grokSsoExtractedAt: completedAt,
|
||||
grokCompletedAt: completedAt,
|
||||
grokRegisterStatus: 'completed',
|
||||
...buildGrokRuntimePatch({
|
||||
register: {
|
||||
status: 'completed',
|
||||
completedAt,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: ssoCookie,
|
||||
cookies: nextSsoCookies,
|
||||
extractedAt: completedAt,
|
||||
},
|
||||
session: {
|
||||
lastError: '',
|
||||
},
|
||||
}),
|
||||
};
|
||||
if (typeof markCurrentRegistrationAccountUsed === 'function') {
|
||||
await markCurrentRegistrationAccountUsed({
|
||||
...currentState,
|
||||
...completionPatch,
|
||||
}, {
|
||||
logPrefix: 'Grok 注册成功',
|
||||
level: 'ok',
|
||||
});
|
||||
}
|
||||
await log('步骤 5:已提取 Grok SSO Cookie。', 'ok', nodeId);
|
||||
await completeNode(nodeId, completionPatch);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
await persistState(buildGrokRuntimePatch({
|
||||
session: {
|
||||
lastError: message,
|
||||
},
|
||||
register: {
|
||||
status: 'error',
|
||||
},
|
||||
}));
|
||||
await log(`步骤 5:${message}`, 'error', nodeId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
executeGrokExtractSsoCookie,
|
||||
executeGrokOpenSignupPage,
|
||||
executeGrokSubmitEmail,
|
||||
executeGrokSubmitProfile,
|
||||
executeGrokSubmitVerificationCode,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_GROK_PAGE_TIMEOUT_MS,
|
||||
GROK_COOKIE_CLEAR_DOMAINS,
|
||||
GROK_POST_PROFILE_CF_WAIT_MS,
|
||||
GROK_PRE_SSO_EXTRACT_WAIT_MS,
|
||||
GROK_REGISTER_PAGE_SOURCE_ID,
|
||||
GROK_SIGNUP_URL,
|
||||
createGrokRegisterRunner,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,374 @@
|
||||
(function attachBackgroundGrokState(root, factory) {
|
||||
root.MultiPageBackgroundGrokState = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokStateModule() {
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => cloneValue(entry));
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function deepMerge(baseValue, patchValue) {
|
||||
if (Array.isArray(patchValue)) {
|
||||
return patchValue.map((entry) => cloneValue(entry));
|
||||
}
|
||||
if (!isPlainObject(patchValue)) {
|
||||
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
|
||||
}
|
||||
|
||||
const baseObject = isPlainObject(baseValue) ? baseValue : {};
|
||||
const next = {
|
||||
...cloneValue(baseObject),
|
||||
};
|
||||
Object.entries(patchValue).forEach(([key, value]) => {
|
||||
next[key] = deepMerge(baseObject[key], value);
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function cleanString(value = '') {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function normalizeInteger(value, fallback = 0) {
|
||||
const numeric = Math.floor(Number(value));
|
||||
return Number.isInteger(numeric) ? numeric : fallback;
|
||||
}
|
||||
|
||||
function normalizeNullableInteger(value, fallback = null) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return fallback;
|
||||
}
|
||||
const numeric = Math.floor(Number(value));
|
||||
return Number.isInteger(numeric) ? numeric : fallback;
|
||||
}
|
||||
|
||||
function normalizeSsoCookies(values = []) {
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(new Set(
|
||||
values
|
||||
.map((entry) => cleanString(entry))
|
||||
.filter(Boolean)
|
||||
));
|
||||
}
|
||||
|
||||
function buildDefaultRuntimeState() {
|
||||
return {
|
||||
session: {
|
||||
registerTabId: null,
|
||||
startedAt: 0,
|
||||
pageState: '',
|
||||
pageUrl: '',
|
||||
lastError: '',
|
||||
},
|
||||
register: {
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
password: '',
|
||||
verificationRequestedAt: 0,
|
||||
verificationCode: '',
|
||||
status: '',
|
||||
completedAt: 0,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: '',
|
||||
cookies: [],
|
||||
extractedAt: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRuntimeState(runtimeState = {}) {
|
||||
const merged = deepMerge(buildDefaultRuntimeState(), runtimeState);
|
||||
return {
|
||||
session: {
|
||||
registerTabId: normalizeNullableInteger(merged.session?.registerTabId),
|
||||
startedAt: Math.max(0, normalizeInteger(merged.session?.startedAt)),
|
||||
pageState: cleanString(merged.session?.pageState),
|
||||
pageUrl: cleanString(merged.session?.pageUrl),
|
||||
lastError: cleanString(merged.session?.lastError),
|
||||
},
|
||||
register: {
|
||||
email: cleanString(merged.register?.email).toLowerCase(),
|
||||
firstName: cleanString(merged.register?.firstName),
|
||||
lastName: cleanString(merged.register?.lastName),
|
||||
password: cleanString(merged.register?.password),
|
||||
verificationRequestedAt: Math.max(0, normalizeInteger(merged.register?.verificationRequestedAt)),
|
||||
verificationCode: cleanString(merged.register?.verificationCode),
|
||||
status: cleanString(merged.register?.status),
|
||||
completedAt: Math.max(0, normalizeInteger(merged.register?.completedAt)),
|
||||
},
|
||||
sso: {
|
||||
currentCookie: cleanString(merged.sso?.currentCookie || merged.ssoCookie),
|
||||
cookies: normalizeSsoCookies(merged.sso?.cookies || merged.ssoCookies),
|
||||
extractedAt: Math.max(0, normalizeInteger(merged.sso?.extractedAt)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildCanonicalRuntimeStatePatch(state = {}, runtimeState = {}) {
|
||||
const normalizedRuntimeState = normalizeRuntimeState(runtimeState);
|
||||
const baseRuntimeState = isPlainObject(state?.runtimeState)
|
||||
? cloneValue(state.runtimeState)
|
||||
: {};
|
||||
const baseFlowState = isPlainObject(baseRuntimeState.flowState)
|
||||
? cloneValue(baseRuntimeState.flowState)
|
||||
: {};
|
||||
return {
|
||||
...baseRuntimeState,
|
||||
flowState: {
|
||||
...baseFlowState,
|
||||
grok: normalizedRuntimeState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function projectRuntimeFields(runtimeState = {}) {
|
||||
const normalizedRuntimeState = normalizeRuntimeState(runtimeState);
|
||||
return {
|
||||
grokRegisterTabId: normalizedRuntimeState.session.registerTabId,
|
||||
grokPageState: normalizedRuntimeState.session.pageState,
|
||||
grokPageUrl: normalizedRuntimeState.session.pageUrl,
|
||||
grokEmail: normalizedRuntimeState.register.email,
|
||||
grokFirstName: normalizedRuntimeState.register.firstName,
|
||||
grokLastName: normalizedRuntimeState.register.lastName,
|
||||
grokPassword: normalizedRuntimeState.register.password,
|
||||
grokVerificationRequestedAt: normalizedRuntimeState.register.verificationRequestedAt,
|
||||
grokVerificationCode: normalizedRuntimeState.register.verificationCode,
|
||||
grokRegisterStatus: normalizedRuntimeState.register.status,
|
||||
grokCompletedAt: normalizedRuntimeState.register.completedAt,
|
||||
grokSsoCookie: normalizedRuntimeState.sso.currentCookie,
|
||||
grokSsoCookies: normalizedRuntimeState.sso.cookies,
|
||||
grokSsoExtractedAt: normalizedRuntimeState.sso.extractedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureRuntimeState(state = {}) {
|
||||
const runtimeFlowState = isPlainObject(state?.runtimeState?.flowState)
|
||||
? state.runtimeState.flowState
|
||||
: {};
|
||||
const legacyFlowState = isPlainObject(state?.flowState?.grok)
|
||||
? state.flowState.grok
|
||||
: {};
|
||||
const flatRuntime = {
|
||||
session: {
|
||||
registerTabId: state.grokRegisterTabId,
|
||||
pageState: state.grokPageState,
|
||||
pageUrl: state.grokPageUrl || state.grokPostVerificationUrl,
|
||||
},
|
||||
register: {
|
||||
email: state.grokEmail || state.email,
|
||||
firstName: state.grokFirstName,
|
||||
lastName: state.grokLastName,
|
||||
password: state.grokPassword,
|
||||
verificationRequestedAt: state.grokVerificationRequestedAt,
|
||||
verificationCode: state.grokVerificationCode,
|
||||
status: state.grokRegisterStatus,
|
||||
completedAt: state.grokCompletedAt,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: state.grokSsoCookie,
|
||||
cookies: state.grokSsoCookies,
|
||||
extractedAt: state.grokSsoExtractedAt || state.grokCompletedAt,
|
||||
},
|
||||
};
|
||||
return normalizeRuntimeState(deepMerge(deepMerge(runtimeFlowState.grok || {}, legacyFlowState), flatRuntime));
|
||||
}
|
||||
|
||||
function buildStateView(state = {}) {
|
||||
const runtimeState = ensureRuntimeState(state);
|
||||
const canonicalRuntimeState = buildCanonicalRuntimeStatePatch(state, runtimeState);
|
||||
return {
|
||||
...state,
|
||||
...projectRuntimeFields(runtimeState),
|
||||
runtimeState: canonicalRuntimeState,
|
||||
flowState: {
|
||||
...(isPlainObject(state?.flowState) ? state.flowState : {}),
|
||||
grok: runtimeState,
|
||||
},
|
||||
flows: {
|
||||
...(isPlainObject(state?.flows) ? state.flows : {}),
|
||||
grok: runtimeState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuntimeStatePatch(currentState = {}, patch = {}) {
|
||||
if (!isPlainObject(patch)) {
|
||||
return {};
|
||||
}
|
||||
const nextRuntimeState = normalizeRuntimeState(
|
||||
deepMerge(ensureRuntimeState(currentState), patch)
|
||||
);
|
||||
return {
|
||||
...projectRuntimeFields(nextRuntimeState),
|
||||
runtimeState: buildCanonicalRuntimeStatePatch(currentState, nextRuntimeState),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionStatePatch(currentState = {}, updates = {}) {
|
||||
const runtimePatch = isPlainObject(updates?.runtimeState?.flowState?.grok)
|
||||
? updates.runtimeState.flowState.grok
|
||||
: (isPlainObject(updates?.flowState?.grok) ? updates.flowState.grok : null);
|
||||
if (!runtimePatch) {
|
||||
return {};
|
||||
}
|
||||
return buildRuntimeStatePatch(currentState, runtimePatch);
|
||||
}
|
||||
|
||||
function buildRuntimeResetPatch(currentState = {}, patch = {}) {
|
||||
return buildRuntimeStatePatch(currentState, patch);
|
||||
}
|
||||
|
||||
function buildStartRegisterResetPatch(currentState = {}) {
|
||||
return buildRuntimeStatePatch(currentState, buildDefaultRuntimeState());
|
||||
}
|
||||
|
||||
function buildRegisterOnlyResetPatch(currentState = {}, registerPatch = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
return buildRuntimeStatePatch(currentState, {
|
||||
...currentRuntimeState,
|
||||
session: {
|
||||
...currentRuntimeState.session,
|
||||
pageState: '',
|
||||
pageUrl: '',
|
||||
lastError: '',
|
||||
},
|
||||
register: {
|
||||
...buildDefaultRuntimeState().register,
|
||||
email: currentRuntimeState.register?.email || '',
|
||||
...registerPatch,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildSsoResetPatch(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
return buildRuntimeStatePatch(currentState, {
|
||||
...currentRuntimeState,
|
||||
sso: buildDefaultRuntimeState().sso,
|
||||
});
|
||||
}
|
||||
|
||||
function buildDownstreamResetPatch(stepKey = '', currentState = {}) {
|
||||
switch (cleanString(stepKey)) {
|
||||
case 'grok-open-signup-page':
|
||||
return {
|
||||
flowStartTime: null,
|
||||
...buildStartRegisterResetPatch(currentState),
|
||||
};
|
||||
case 'grok-submit-email':
|
||||
return buildRegisterOnlyResetPatch(currentState, {
|
||||
email: '',
|
||||
});
|
||||
case 'grok-submit-verification-code':
|
||||
return buildRegisterOnlyResetPatch(currentState, {});
|
||||
case 'grok-submit-profile':
|
||||
case 'grok-extract-sso-cookie':
|
||||
return buildSsoResetPatch(currentState);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function applyNodeCompletionPayload(currentState = {}, payload = {}) {
|
||||
const runtimePatch = isPlainObject(payload?.runtimeState?.flowState?.grok)
|
||||
? payload.runtimeState.flowState.grok
|
||||
: (isPlainObject(payload?.flowState?.grok) ? payload.flowState.grok : null);
|
||||
if (runtimePatch) {
|
||||
return buildRuntimeStatePatch(currentState, runtimePatch);
|
||||
}
|
||||
|
||||
const patch = {};
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokRegisterTabId')) {
|
||||
patch.session = { ...(patch.session || {}), registerTabId: payload.grokRegisterTabId };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokPageState')) {
|
||||
patch.session = { ...(patch.session || {}), pageState: payload.grokPageState };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokPageUrl') || Object.prototype.hasOwnProperty.call(payload, 'grokSignupUrl') || Object.prototype.hasOwnProperty.call(payload, 'grokPostVerificationUrl')) {
|
||||
patch.session = {
|
||||
...(patch.session || {}),
|
||||
pageUrl: payload.grokPageUrl || payload.grokPostVerificationUrl || payload.grokSignupUrl || '',
|
||||
};
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokEmail') || Object.prototype.hasOwnProperty.call(payload, 'email')) {
|
||||
patch.register = {
|
||||
...(patch.register || {}),
|
||||
email: payload.grokEmail || payload.email || '',
|
||||
};
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokFirstName')) {
|
||||
patch.register = { ...(patch.register || {}), firstName: payload.grokFirstName };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokLastName')) {
|
||||
patch.register = { ...(patch.register || {}), lastName: payload.grokLastName };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokPassword')) {
|
||||
patch.register = { ...(patch.register || {}), password: payload.grokPassword };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokVerificationRequestedAt')) {
|
||||
patch.register = {
|
||||
...(patch.register || {}),
|
||||
verificationRequestedAt: payload.grokVerificationRequestedAt,
|
||||
};
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokVerificationCode')) {
|
||||
patch.register = { ...(patch.register || {}), verificationCode: payload.grokVerificationCode };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokRegisterStatus')) {
|
||||
patch.register = { ...(patch.register || {}), status: payload.grokRegisterStatus };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokCompletedAt')) {
|
||||
patch.register = { ...(patch.register || {}), completedAt: payload.grokCompletedAt };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokSsoCookie')) {
|
||||
patch.sso = { ...(patch.sso || {}), currentCookie: payload.grokSsoCookie };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokSsoCookies')) {
|
||||
patch.sso = { ...(patch.sso || {}), cookies: payload.grokSsoCookies };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'grokSsoExtractedAt') || Object.prototype.hasOwnProperty.call(payload, 'grokCompletedAt')) {
|
||||
patch.sso = {
|
||||
...(patch.sso || {}),
|
||||
extractedAt: payload.grokSsoExtractedAt || payload.grokCompletedAt || 0,
|
||||
};
|
||||
}
|
||||
if (!Object.keys(patch).length) {
|
||||
return {};
|
||||
}
|
||||
return buildRuntimeStatePatch(currentState, patch);
|
||||
}
|
||||
|
||||
function buildFreshKeepState(currentState = {}) {
|
||||
const currentRuntimeState = ensureRuntimeState(currentState);
|
||||
const nextRuntimeState = buildDefaultRuntimeState();
|
||||
nextRuntimeState.sso = currentRuntimeState.sso || buildDefaultRuntimeState().sso;
|
||||
return buildRuntimeStatePatch(currentState, nextRuntimeState);
|
||||
}
|
||||
|
||||
return {
|
||||
applyNodeCompletionPayload,
|
||||
buildDefaultRuntimeState,
|
||||
buildDownstreamResetPatch,
|
||||
buildFreshKeepState,
|
||||
buildRuntimeStatePatch,
|
||||
buildSessionStatePatch,
|
||||
buildStateView,
|
||||
ensureRuntimeState,
|
||||
normalizeSsoCookies,
|
||||
projectRuntimeFields,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,317 @@
|
||||
console.log('[MultiPage:grok-register-page] Content script loaded on', location.href);
|
||||
|
||||
const GROK_REGISTER_PAGE_LISTENER_SENTINEL = 'data-multipage-grok-register-page-listener';
|
||||
const GROK_SIGNUP_URL = 'https://accounts.x.ai/sign-up?redirect=grok-com';
|
||||
const GROK_EMAIL_SIGNUP_TEXT_PATTERN = /使用邮箱注册|sign\s*up\s*with\s*email|continue\s*with\s*email|email/i;
|
||||
const GROK_CONTINUE_TEXT_PATTERN = /continue|next|sign\s*up|submit|verify|继续|下一步|注册|提交|验证/i;
|
||||
const GROK_PROFILE_TEXT_PATTERN = /given\s*name|family\s*name|first\s*name|last\s*name|password|名字|姓氏|密码/i;
|
||||
const GROK_PROFILE_SUBMIT_PRE_CLICK_DELAY_MS = 2000;
|
||||
|
||||
function isVisibleGrokElement(element) {
|
||||
if (!element || !(element instanceof Element)) return false;
|
||||
const style = window.getComputedStyle(element);
|
||||
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}
|
||||
|
||||
function getGrokElementText(element) {
|
||||
if (!element) return '';
|
||||
return String(
|
||||
element.innerText
|
||||
|| element.textContent
|
||||
|| element.getAttribute?.('aria-label')
|
||||
|| element.getAttribute?.('title')
|
||||
|| ''
|
||||
)
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function queryVisibleGrokElement(selector) {
|
||||
return Array.from(document.querySelectorAll(selector)).find(isVisibleGrokElement) || null;
|
||||
}
|
||||
|
||||
function findGrokClickableByText(pattern) {
|
||||
const selectors = 'button, a, [role="button"], input[type="button"], input[type="submit"]';
|
||||
return Array.from(document.querySelectorAll(selectors)).find((element) => {
|
||||
if (!isVisibleGrokElement(element)) return false;
|
||||
const text = element instanceof HTMLInputElement ? element.value : getGrokElementText(element);
|
||||
return pattern.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function simulateGrokClick(element) {
|
||||
throwIfStopped();
|
||||
if (!element) {
|
||||
throw new Error('无法点击空元素。');
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
const clientX = Math.max(0, Math.floor(rect.left + Math.min(rect.width - 1, Math.max(1, rect.width / 2))));
|
||||
const clientY = Math.max(0, Math.floor(rect.top + Math.min(rect.height - 1, Math.max(1, rect.height / 2))));
|
||||
const eventOptions = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
clientX,
|
||||
clientY,
|
||||
screenX: window.screenX + clientX,
|
||||
screenY: window.screenY + clientY,
|
||||
};
|
||||
element.dispatchEvent(new MouseEvent('mouseover', eventOptions));
|
||||
element.dispatchEvent(new MouseEvent('mousedown', eventOptions));
|
||||
element.dispatchEvent(new MouseEvent('mouseup', eventOptions));
|
||||
if (typeof element.click === 'function') {
|
||||
element.click();
|
||||
return;
|
||||
}
|
||||
element.dispatchEvent(new MouseEvent('click', eventOptions));
|
||||
}
|
||||
|
||||
async function waitForGrok(predicate, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 30000);
|
||||
const intervalMs = Math.max(100, Number(options.intervalMs) || 250);
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastValue = null;
|
||||
while (Date.now() <= deadline) {
|
||||
throwIfStopped();
|
||||
lastValue = predicate();
|
||||
if (lastValue) return lastValue;
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
return lastValue;
|
||||
}
|
||||
|
||||
function findGrokEmailInput() {
|
||||
return queryVisibleGrokElement([
|
||||
'input[type="email"]',
|
||||
'input[name="email" i]',
|
||||
'input[autocomplete="email"]',
|
||||
'input[placeholder*="email" i]',
|
||||
'input[inputmode="email"]',
|
||||
].join(', '));
|
||||
}
|
||||
|
||||
function findGrokOtpInputs() {
|
||||
const inputs = Array.from(document.querySelectorAll([
|
||||
'input[autocomplete="one-time-code"]',
|
||||
'input[inputmode="numeric"]',
|
||||
'input[name*="otp" i]',
|
||||
'input[name*="code" i]',
|
||||
'input[aria-label*="code" i]',
|
||||
'input[placeholder*="code" i]',
|
||||
].join(', '))).filter(isVisibleGrokElement);
|
||||
if (inputs.length) return inputs;
|
||||
const oneCharInputs = Array.from(document.querySelectorAll('input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"])'))
|
||||
.filter((input) => isVisibleGrokElement(input) && Number(input.maxLength || 0) === 1);
|
||||
return oneCharInputs.length >= 4 ? oneCharInputs : [];
|
||||
}
|
||||
|
||||
function findGrokProfileInput(names) {
|
||||
const selectors = names.flatMap((name) => [
|
||||
`input[name="${name}" i]`,
|
||||
`input[id="${name}" i]`,
|
||||
`input[autocomplete="${name}" i]`,
|
||||
`input[placeholder*="${name}" i]`,
|
||||
`input[aria-label*="${name}" i]`,
|
||||
]).join(', ');
|
||||
return queryVisibleGrokElement(selectors);
|
||||
}
|
||||
|
||||
function findGrokPasswordInputs() {
|
||||
return Array.from(document.querySelectorAll([
|
||||
'input[type="password"]',
|
||||
'input[name*="password" i]',
|
||||
'input[autocomplete="new-password"]',
|
||||
'input[placeholder*="password" i]',
|
||||
'input[aria-label*="password" i]',
|
||||
].join(', '))).filter(isVisibleGrokElement);
|
||||
}
|
||||
|
||||
function findGrokSubmitButton(contextPattern = GROK_CONTINUE_TEXT_PATTERN) {
|
||||
return findGrokClickableByText(contextPattern)
|
||||
|| Array.from(document.querySelectorAll('button:not([disabled]), [role="button"]')).filter(isVisibleGrokElement).at(-1)
|
||||
|| null;
|
||||
}
|
||||
|
||||
function getGrokPageState() {
|
||||
const pageText = document.body?.innerText || '';
|
||||
if (/grok|xai|x\.ai/i.test(location.hostname) && /(?:^|;\s*)sso=/.test(document.cookie || '')) return 'signed_in';
|
||||
if (findGrokProfileInput(['givenName', 'firstName']) || findGrokPasswordInputs().length || GROK_PROFILE_TEXT_PATTERN.test(pageText)) return 'profile_entry';
|
||||
if (findGrokOtpInputs().length) return 'verification_code_entry';
|
||||
if (findGrokEmailInput()) return 'email_entry';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
async function openGrokSignupPage() {
|
||||
if (!/accounts\.x\.ai$/i.test(location.hostname) || !/\/sign-up/i.test(location.pathname)) {
|
||||
location.href = GROK_SIGNUP_URL;
|
||||
return { submitted: true, state: 'navigating', url: location.href };
|
||||
}
|
||||
const emailButton = await waitForGrok(() => (
|
||||
findGrokClickableByText(GROK_EMAIL_SIGNUP_TEXT_PATTERN) || findGrokEmailInput()
|
||||
), { timeoutMs: 30000 });
|
||||
if (!emailButton) throw new Error('未找到 x.ai 邮箱注册入口。');
|
||||
if (!(emailButton instanceof HTMLInputElement)) {
|
||||
simulateGrokClick(emailButton);
|
||||
await sleep(500);
|
||||
}
|
||||
return { submitted: true, state: getGrokPageState(), url: location.href };
|
||||
}
|
||||
|
||||
function getGrokEmailErrorText() {
|
||||
const text = String(document.body?.innerText || '').trim();
|
||||
const patterns = [
|
||||
/Your email domain[^\n]+has been rejected[^\n]*/i,
|
||||
/Please use a different email address[^\n]*/i,
|
||||
/邮箱域名[^\n]*(?:被拒绝|不可用|不支持)[^\n]*/i,
|
||||
/请使用其他邮箱[^\n]*/i,
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match?.[0]) return match[0].trim();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function submitGrokEmail(payload = {}) {
|
||||
const email = String(payload.email || '').trim();
|
||||
if (!email) throw new Error('缺少 Grok 注册邮箱。');
|
||||
const input = await waitForGrok(findGrokEmailInput, { timeoutMs: 45000 });
|
||||
if (!input) throw new Error('未找到 x.ai 邮箱输入框。');
|
||||
fillInput(input, email);
|
||||
await sleep(200);
|
||||
const button = findGrokSubmitButton();
|
||||
if (!button) throw new Error('未找到 x.ai 邮箱提交按钮。');
|
||||
simulateGrokClick(button);
|
||||
await sleep(1200);
|
||||
const errorText = getGrokEmailErrorText();
|
||||
if (errorText) {
|
||||
throw new Error(errorText);
|
||||
}
|
||||
return { submitted: true, state: getGrokPageState(), url: location.href };
|
||||
}
|
||||
|
||||
function getGrokVerificationErrorText() {
|
||||
const text = String(document.body?.innerText || '').trim();
|
||||
const patterns = [
|
||||
/(?:verification|confirmation)?\s*code\s*(?:is\s*)?(?:invalid|incorrect|expired)[^\n]*/i,
|
||||
/invalid\s*(?:verification|confirmation)?\s*code[^\n]*/i,
|
||||
/验证码[^\n]*(?:错误|无效|过期)[^\n]*/i,
|
||||
/代码[^\n]*(?:错误|无效|过期)[^\n]*/i,
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match?.[0]) return match[0].trim();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function submitGrokVerificationCode(payload = {}) {
|
||||
const normalizedCode = String(payload.code || '').replace(/[^A-Za-z0-9]/g, '').trim();
|
||||
if (!normalizedCode) throw new Error('缺少 xAI 验证码。');
|
||||
const inputs = await waitForGrok(() => findGrokOtpInputs(), { timeoutMs: 45000 });
|
||||
if (!inputs?.length) throw new Error('未找到 xAI 验证码输入框。');
|
||||
if (inputs.length === 1) {
|
||||
fillInput(inputs[0], normalizedCode);
|
||||
} else {
|
||||
normalizedCode.split('').forEach((char, index) => {
|
||||
if (inputs[index]) fillInput(inputs[index], char);
|
||||
});
|
||||
}
|
||||
await sleep(200);
|
||||
const button = findGrokSubmitButton();
|
||||
if (button) simulateGrokClick(button);
|
||||
const settledState = await waitForGrok(() => {
|
||||
const errorText = getGrokVerificationErrorText();
|
||||
if (errorText) return { state: 'verification_error', error: errorText };
|
||||
const state = getGrokPageState();
|
||||
return state && state !== 'verification_code_entry' ? { state } : null;
|
||||
}, { timeoutMs: 20000, intervalMs: 500 });
|
||||
const finalState = settledState?.state || getGrokPageState();
|
||||
if (settledState?.error) {
|
||||
throw new Error(settledState.error);
|
||||
}
|
||||
if (finalState === 'email_entry') {
|
||||
throw new Error('x.ai 验证码提交后回到邮箱注册页,可能是验证码无效、会话过期或注册风控重置。');
|
||||
}
|
||||
if (!['profile_entry', 'signed_in'].includes(finalState)) {
|
||||
throw new Error(`x.ai 验证码提交后进入未知页面状态:${finalState || 'unknown'}。`);
|
||||
}
|
||||
return { submitted: true, state: finalState, url: location.href };
|
||||
}
|
||||
|
||||
async function submitGrokProfile(payload = {}) {
|
||||
const firstName = String(payload.firstName || '').trim();
|
||||
const lastName = String(payload.lastName || '').trim();
|
||||
const password = String(payload.password || '');
|
||||
if (!firstName || !lastName || !password) throw new Error('缺少 Grok 注册资料。');
|
||||
const ready = await waitForGrok(() => {
|
||||
const firstInput = findGrokProfileInput(['givenName', 'firstName', 'given-name']);
|
||||
const lastInput = findGrokProfileInput(['familyName', 'lastName', 'family-name']);
|
||||
const passwordInputs = findGrokPasswordInputs();
|
||||
return firstInput && lastInput && passwordInputs.length ? { firstInput, lastInput, passwordInputs } : null;
|
||||
}, { timeoutMs: 45000 });
|
||||
if (!ready) throw new Error('未找到 x.ai 资料或密码表单。');
|
||||
fillInput(ready.firstInput, firstName);
|
||||
fillInput(ready.lastInput, lastName);
|
||||
ready.passwordInputs.forEach((input) => fillInput(input, password));
|
||||
await sleep(GROK_PROFILE_SUBMIT_PRE_CLICK_DELAY_MS);
|
||||
const button = findGrokSubmitButton();
|
||||
if (!button) throw new Error('未找到 x.ai 资料提交按钮。');
|
||||
simulateGrokClick(button);
|
||||
return { submitted: true, state: 'profile_submitted', url: location.href };
|
||||
}
|
||||
|
||||
async function extractGrokSsoCookie() {
|
||||
const match = String(document.cookie || '').match(/(?:^|;\s*)sso=([^;]+)/);
|
||||
return {
|
||||
submitted: true,
|
||||
state: match ? 'sso_cookie_found' : getGrokPageState(),
|
||||
ssoCookie: match ? decodeURIComponent(match[1]) : '',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeGrokCommand(command, payload = {}) {
|
||||
switch (command) {
|
||||
case 'grok-open-signup-page':
|
||||
return openGrokSignupPage(payload);
|
||||
case 'grok-submit-email':
|
||||
return submitGrokEmail(payload);
|
||||
case 'grok-submit-verification-code':
|
||||
return submitGrokVerificationCode(payload);
|
||||
case 'grok-submit-profile':
|
||||
return submitGrokProfile(payload);
|
||||
case 'grok-extract-sso-cookie':
|
||||
return extractGrokSsoCookie(payload);
|
||||
case 'GET_PAGE_STATE':
|
||||
return { state: getGrokPageState(), url: location.href };
|
||||
default:
|
||||
throw new Error(`未知 Grok 注册命令:${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!document.documentElement.hasAttribute(GROK_REGISTER_PAGE_LISTENER_SENTINEL)) {
|
||||
document.documentElement.setAttribute(GROK_REGISTER_PAGE_LISTENER_SENTINEL, '1');
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message?.type !== 'EXECUTE_NODE' && message?.type !== 'GET_PAGE_STATE') return false;
|
||||
resetStopState();
|
||||
const command = message.command || message.nodeId || message.type;
|
||||
executeGrokCommand(command, message.payload || {})
|
||||
.then((result) => sendResponse({ ok: true, ...result }))
|
||||
.catch((error) => {
|
||||
if (isStopError(error)) {
|
||||
sendResponse({ stopped: true, error: error.message });
|
||||
return;
|
||||
}
|
||||
sendResponse({ ok: false, error: error?.message || String(error) });
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
window.__MULTIPAGE_GROK_REGISTER_PAGE__ = {
|
||||
executeGrokCommand,
|
||||
getGrokPageState,
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
(function attachMultiPageGrokFlowDefinition(root, factory) {
|
||||
root.MultiPageGrokFlowDefinition = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createMultiPageGrokFlowDefinition() {
|
||||
function freezeDeep(entry) {
|
||||
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
|
||||
return entry;
|
||||
}
|
||||
Object.getOwnPropertyNames(entry).forEach((key) => {
|
||||
freezeDeep(entry[key]);
|
||||
});
|
||||
return Object.freeze(entry);
|
||||
}
|
||||
|
||||
const VALUE = freezeDeep({
|
||||
id: 'grok',
|
||||
label: 'Grok / xAI',
|
||||
services: [
|
||||
'account',
|
||||
'email',
|
||||
'proxy',
|
||||
],
|
||||
capabilities: {
|
||||
supportsEmailSignup: true,
|
||||
supportsPhoneSignup: false,
|
||||
supportsPhoneVerificationSettings: false,
|
||||
supportsPlusMode: false,
|
||||
supportsContributionMode: false,
|
||||
supportsAccountContribution: false,
|
||||
supportsOpenAiOAuthContribution: false,
|
||||
contributionAdapterIds: [],
|
||||
supportedTargetIds: ['webchat2api'],
|
||||
supportsLuckmail: false,
|
||||
supportsOauthTimeoutBudget: false,
|
||||
canSwitchFlow: true,
|
||||
stepDefinitionMode: 'grok',
|
||||
targetSelectorLabel: '来源',
|
||||
},
|
||||
baseGroups: ['grok-runtime-status'],
|
||||
targets: {
|
||||
webchat2api: {
|
||||
id: 'webchat2api',
|
||||
label: 'webchat2api',
|
||||
groups: [
|
||||
'grok-target-webchat2api',
|
||||
],
|
||||
},
|
||||
},
|
||||
publicationTargets: {},
|
||||
runtimeSources: {
|
||||
'grok-register-page': {
|
||||
flowId: 'grok',
|
||||
kind: 'flow-page',
|
||||
label: 'Grok 注册页',
|
||||
readyPolicy: 'top-frame-only',
|
||||
family: 'grok-register-page-family',
|
||||
driverId: 'flows/grok/content/register-page',
|
||||
cleanupScopes: [],
|
||||
detectionMatchers: [
|
||||
{
|
||||
hostnames: [
|
||||
'accounts.x.ai',
|
||||
'x.ai',
|
||||
'grok.com',
|
||||
],
|
||||
hostnameEndsWith: [
|
||||
'.x.ai',
|
||||
'.grok.com',
|
||||
],
|
||||
matchMode: 'any',
|
||||
},
|
||||
],
|
||||
familyMatchers: [
|
||||
{
|
||||
hostnames: [
|
||||
'accounts.x.ai',
|
||||
'x.ai',
|
||||
'grok.com',
|
||||
],
|
||||
hostnameEndsWith: [
|
||||
'.x.ai',
|
||||
'.grok.com',
|
||||
],
|
||||
matchMode: 'any',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
driverDefinitions: {
|
||||
'flows/grok/content/register-page': {
|
||||
sourceId: 'grok-register-page',
|
||||
commands: [
|
||||
'grok-open-signup-page',
|
||||
'grok-submit-email',
|
||||
'grok-submit-verification-code',
|
||||
'grok-submit-profile',
|
||||
'grok-extract-sso-cookie',
|
||||
],
|
||||
},
|
||||
'flows/grok/background/register-runner': {
|
||||
sourceId: 'grok-register-page',
|
||||
commands: [
|
||||
'grok-open-signup-page',
|
||||
'grok-submit-email',
|
||||
'grok-submit-verification-code',
|
||||
'grok-submit-profile',
|
||||
'grok-extract-sso-cookie',
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultTargetId: 'webchat2api',
|
||||
settingsDefaults: {
|
||||
autoRun: {
|
||||
stepExecutionRange: {
|
||||
enabled: false,
|
||||
fromStep: 1,
|
||||
toStep: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
settingsGroups: {
|
||||
'grok-target-webchat2api': {
|
||||
id: 'grok-target-webchat2api',
|
||||
label: 'webchat2api',
|
||||
rowIds: [
|
||||
'row-grok-sso-settings',
|
||||
],
|
||||
},
|
||||
'grok-runtime-status': {
|
||||
id: 'grok-runtime-status',
|
||||
label: 'Grok 运行态',
|
||||
rowIds: [
|
||||
'row-grok-register-status',
|
||||
'row-grok-sso-status',
|
||||
],
|
||||
},
|
||||
},
|
||||
sourceAliases: {},
|
||||
});
|
||||
|
||||
return VALUE;
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
(function attachGrokMailRules(root, factory) {
|
||||
root.MultiPageGrokMailRules = factory(root);
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createGrokMailRulesModule(root) {
|
||||
const grokStateApi = root.MultiPageBackgroundGrokState || null;
|
||||
const SUBMIT_VERIFICATION_CODE_RULE_ID = 'grok-submit-verification-code';
|
||||
const SUBMIT_VERIFICATION_CODE_NODE_ID = 'grok-submit-verification-code';
|
||||
const GROK_VERIFICATION_CODE_PATTERNS = Object.freeze([
|
||||
Object.freeze({
|
||||
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
|
||||
flags: 'gi',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[::\\s]*(\\d{6})',
|
||||
flags: 'gi',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '(?:验证码|代码|确认码)[::\\s为]+(\\d{6})',
|
||||
flags: 'gi',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '(?<!#)\\b(\\d{6})\\b',
|
||||
flags: 'g',
|
||||
}),
|
||||
]);
|
||||
const GROK_SENDER_FILTERS = Object.freeze([
|
||||
'x.ai',
|
||||
'xai',
|
||||
'grok',
|
||||
]);
|
||||
const GROK_SUBJECT_FILTERS = Object.freeze([
|
||||
'xai',
|
||||
'x.ai',
|
||||
'grok',
|
||||
'verification',
|
||||
'confirmation',
|
||||
'code',
|
||||
'验证码',
|
||||
'确认码',
|
||||
]);
|
||||
const GROK_REQUIRED_KEYWORDS = Object.freeze([
|
||||
'xai',
|
||||
'x.ai',
|
||||
'grok',
|
||||
'verification',
|
||||
'confirmation',
|
||||
'code',
|
||||
'验证码',
|
||||
'确认码',
|
||||
]);
|
||||
|
||||
function cleanString(value = '') {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function readGrokRuntime(state = {}) {
|
||||
if (typeof grokStateApi?.ensureRuntimeState === 'function') {
|
||||
return grokStateApi.ensureRuntimeState(state);
|
||||
}
|
||||
return state?.runtimeState?.flowState?.grok || state?.flowState?.grok || {};
|
||||
}
|
||||
|
||||
function buildTargetEmailHints(targetEmail = '') {
|
||||
const normalizedTarget = cleanString(targetEmail).toLowerCase();
|
||||
return normalizedTarget ? [normalizedTarget] : [];
|
||||
}
|
||||
|
||||
function getVisibleStep(state = {}) {
|
||||
const explicitStep = Number(state?.visibleStep || state?.step);
|
||||
return Number.isInteger(explicitStep) && explicitStep > 0 ? explicitStep : 3;
|
||||
}
|
||||
|
||||
function isMail2925Provider(state = {}) {
|
||||
return cleanString(state?.mailProvider).toLowerCase() === '2925';
|
||||
}
|
||||
|
||||
function shouldMatchMail2925TargetEmail(state = {}) {
|
||||
return isMail2925Provider(state)
|
||||
&& cleanString(state?.mail2925Mode).toLowerCase() === 'receive';
|
||||
}
|
||||
|
||||
function createGrokMailRules(deps = {}) {
|
||||
const {
|
||||
LUCKMAIL_PROVIDER = 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
|
||||
} = deps;
|
||||
|
||||
function getRuleDefinition(_input, state = {}) {
|
||||
const runtimeState = readGrokRuntime(state);
|
||||
const targetEmail = cleanString(runtimeState.register?.email || state?.grokEmail || state?.email).toLowerCase();
|
||||
const normalizedProvider = cleanString(state?.mailProvider).toLowerCase();
|
||||
const mail2925Provider = isMail2925Provider(state);
|
||||
const luckmailProvider = normalizedProvider === cleanString(LUCKMAIL_PROVIDER).toLowerCase();
|
||||
|
||||
return {
|
||||
flowId: 'grok',
|
||||
ruleId: SUBMIT_VERIFICATION_CODE_RULE_ID,
|
||||
nodeId: SUBMIT_VERIFICATION_CODE_NODE_ID,
|
||||
step: getVisibleStep(state),
|
||||
artifactType: 'code',
|
||||
codePatterns: GROK_VERIFICATION_CODE_PATTERNS,
|
||||
filterAfterTimestamp: 0,
|
||||
requiredKeywords: GROK_REQUIRED_KEYWORDS,
|
||||
senderFilters: GROK_SENDER_FILTERS,
|
||||
subjectFilters: GROK_SUBJECT_FILTERS,
|
||||
targetEmail,
|
||||
targetEmailHints: buildTargetEmailHints(targetEmail),
|
||||
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
|
||||
maxAttempts: luckmailProvider
|
||||
? 3
|
||||
: (mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5),
|
||||
intervalMs: luckmailProvider
|
||||
? 15000
|
||||
: (mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 5000),
|
||||
};
|
||||
}
|
||||
|
||||
function getRuleDefinitionForNode(nodeId, state = {}) {
|
||||
const normalizedNodeId = cleanString(nodeId);
|
||||
if (normalizedNodeId && normalizedNodeId !== SUBMIT_VERIFICATION_CODE_NODE_ID) {
|
||||
throw new Error(`Grok 邮件规则不支持节点:${normalizedNodeId}`);
|
||||
}
|
||||
return getRuleDefinition({ nodeId: SUBMIT_VERIFICATION_CODE_NODE_ID }, state);
|
||||
}
|
||||
|
||||
function buildVerificationPollPayload(input, state = {}, overrides = {}) {
|
||||
return {
|
||||
...getRuleDefinition(input, state),
|
||||
...(overrides || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
|
||||
return {
|
||||
...getRuleDefinitionForNode(nodeId, state),
|
||||
...(overrides || {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buildVerificationPollPayload,
|
||||
buildVerificationPollPayloadForNode,
|
||||
getRuleDefinition,
|
||||
getRuleDefinitionForNode,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
GROK_REQUIRED_KEYWORDS,
|
||||
GROK_SENDER_FILTERS,
|
||||
GROK_SUBJECT_FILTERS,
|
||||
GROK_VERIFICATION_CODE_PATTERNS,
|
||||
SUBMIT_VERIFICATION_CODE_NODE_ID,
|
||||
SUBMIT_VERIFICATION_CODE_RULE_ID,
|
||||
createGrokMailRules,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
(function attachMultiPageGrokWorkflow(root, factory) {
|
||||
root.MultiPageGrokWorkflow = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createMultiPageGrokWorkflow() {
|
||||
function freezeDeep(entry) {
|
||||
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
|
||||
return entry;
|
||||
}
|
||||
Object.getOwnPropertyNames(entry).forEach((key) => {
|
||||
freezeDeep(entry[key]);
|
||||
});
|
||||
return Object.freeze(entry);
|
||||
}
|
||||
|
||||
const STEP_VARIANTS = freezeDeep({
|
||||
default: [
|
||||
{
|
||||
id: 1,
|
||||
order: 10,
|
||||
key: 'grok-open-signup-page',
|
||||
title: '打开 Grok 注册页',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-open-signup-page',
|
||||
flowId: 'grok',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order: 20,
|
||||
key: 'grok-submit-email',
|
||||
title: '获取邮箱并继续',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-submit-email',
|
||||
flowId: 'grok',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
order: 30,
|
||||
key: 'grok-submit-verification-code',
|
||||
title: '获取验证码并继续',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-submit-verification-code',
|
||||
mailRuleId: 'grok-submit-verification-code',
|
||||
flowId: 'grok',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
order: 40,
|
||||
key: 'grok-submit-profile',
|
||||
title: '填写资料并继续',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-submit-profile',
|
||||
flowId: 'grok',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
order: 50,
|
||||
key: 'grok-extract-sso-cookie',
|
||||
title: '提取 SSO Cookie',
|
||||
sourceId: 'grok-register-page',
|
||||
driverId: 'flows/grok/background/register-runner',
|
||||
command: 'grok-extract-sso-cookie',
|
||||
flowId: 'grok',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function getVariantStepDefinitions(variantKey = 'default') {
|
||||
return Array.isArray(STEP_VARIANTS[variantKey]) ? STEP_VARIANTS[variantKey] : STEP_VARIANTS.default;
|
||||
}
|
||||
|
||||
function getModeStepDefinitions() {
|
||||
return getVariantStepDefinitions('default');
|
||||
}
|
||||
|
||||
function getAllSteps() {
|
||||
return getVariantStepDefinitions('default');
|
||||
}
|
||||
|
||||
function getPlusPaymentStepTitle() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveStepTitle(step = {}) {
|
||||
return step?.title || '';
|
||||
}
|
||||
|
||||
return {
|
||||
flowId: 'grok',
|
||||
getAllSteps,
|
||||
getModeStepDefinitions,
|
||||
getPlusPaymentStepTitle,
|
||||
getVariantStepDefinitions,
|
||||
resolveStepTitle,
|
||||
};
|
||||
});
|
||||
+10
-2
@@ -12,6 +12,10 @@
|
||||
id: 'kiro',
|
||||
path: 'flows/kiro/',
|
||||
},
|
||||
grok: {
|
||||
id: 'grok',
|
||||
path: 'flows/grok/',
|
||||
},
|
||||
});
|
||||
|
||||
function normalizeFlowId(value = '') {
|
||||
@@ -32,10 +36,14 @@
|
||||
...baseEntry,
|
||||
definition: normalized === 'openai'
|
||||
? (rootScope.MultiPageOpenAiFlowDefinition || null)
|
||||
: (rootScope.MultiPageKiroFlowDefinition || null),
|
||||
: (normalized === 'kiro'
|
||||
? (rootScope.MultiPageKiroFlowDefinition || null)
|
||||
: (rootScope.MultiPageGrokFlowDefinition || null)),
|
||||
workflow: normalized === 'openai'
|
||||
? (rootScope.MultiPageOpenAiWorkflow || null)
|
||||
: (rootScope.MultiPageKiroWorkflow || null),
|
||||
: (normalized === 'kiro'
|
||||
? (rootScope.MultiPageKiroWorkflow || null)
|
||||
: (rootScope.MultiPageGrokWorkflow || null)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,43 +14,6 @@
|
||||
'https://app.kiro.dev/*',
|
||||
'https://kiro.dev/*',
|
||||
]);
|
||||
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
|
||||
Object.freeze({
|
||||
source: '(?:verification\\s*code|验证码|Your code is|code is)[::\\s]*(\\d{6})',
|
||||
flags: 'gi',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '^\\s*(\\d{6})\\s*$',
|
||||
flags: 'gm',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '>\\s*(\\d{6})\\s*<',
|
||||
flags: 'g',
|
||||
}),
|
||||
]);
|
||||
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
|
||||
'no-reply@signin.aws',
|
||||
'no-reply@login.awsapps.com',
|
||||
'noreply@amazon.com',
|
||||
'account-update@amazon.com',
|
||||
'no-reply@aws.amazon.com',
|
||||
'noreply@aws.amazon.com',
|
||||
'aws',
|
||||
]);
|
||||
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
|
||||
'aws builder id',
|
||||
'verification',
|
||||
'验证码',
|
||||
'code',
|
||||
'aws',
|
||||
]);
|
||||
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
|
||||
'verification',
|
||||
'验证码',
|
||||
'code',
|
||||
'aws',
|
||||
]);
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
@@ -375,32 +338,17 @@
|
||||
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTab = null,
|
||||
ensureIcloudMailSession = null,
|
||||
ensureMail2925MailboxSession = null,
|
||||
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||
getMailConfig = null,
|
||||
getState = async () => ({}),
|
||||
getTabId = async () => null,
|
||||
HOTMAIL_PROVIDER = 'hotmail-api',
|
||||
LUCKMAIL_PROVIDER = 'luckmail-api',
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
|
||||
CLOUD_MAIL_PROVIDER = 'cloudmail',
|
||||
YYDS_MAIL_PROVIDER = 'yyds-mail',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
|
||||
isTabAlive = async () => false,
|
||||
maybeSubmitFlowContribution = async () => null,
|
||||
KIRO_REGISTER_INJECT_FILES = null,
|
||||
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
|
||||
pollCloudflareTempEmailVerificationCode = null,
|
||||
pollCloudMailVerificationCode = null,
|
||||
pollHotmailVerificationCode = null,
|
||||
pollLuckmailVerificationCode = null,
|
||||
pollYydsMailVerificationCode = null,
|
||||
pollFlowVerificationCode = null,
|
||||
registerTab = async () => {},
|
||||
reuseOrCreateTab = async () => null,
|
||||
sendToContentScriptResilient = null,
|
||||
sendToMailContentScriptResilient = null,
|
||||
setState = async () => {},
|
||||
sleepWithStop = async (ms) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@@ -658,44 +606,6 @@
|
||||
return password;
|
||||
}
|
||||
|
||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
|
||||
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
|
||||
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
|
||||
if (accountEmail) {
|
||||
return accountEmail;
|
||||
}
|
||||
}
|
||||
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function focusOrOpenMailTab(mail) {
|
||||
if (!mail?.source) {
|
||||
return;
|
||||
}
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const tabId = await getTabId(mail.source);
|
||||
if (Number.isInteger(tabId)) {
|
||||
await activateTab(tabId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
}
|
||||
|
||||
async function collectKiroWebSessionTabs(currentState = {}) {
|
||||
const runtimeState = readKiroRuntime(currentState);
|
||||
const candidates = [];
|
||||
@@ -901,131 +811,35 @@
|
||||
throw new Error(`Kiro Web 登录态尚未建立。请在自动打开的 Kiro 账号页登录后,从步骤 7 继续。${detail}`);
|
||||
}
|
||||
|
||||
function buildDesktopOtpPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
|
||||
const runtimeState = readKiroRuntime(state);
|
||||
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
|
||||
const targetEmailHints = targetEmail ? [targetEmail] : [];
|
||||
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
|
||||
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
|
||||
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
|
||||
? 3
|
||||
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
|
||||
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
|
||||
? 15000
|
||||
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
|
||||
|
||||
return {
|
||||
flowId: 'kiro',
|
||||
step,
|
||||
targetEmail,
|
||||
targetEmailHints,
|
||||
filterAfterTimestamp,
|
||||
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
|
||||
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
|
||||
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
|
||||
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
|
||||
mail2925MatchTargetEmail: isMail2925Provider
|
||||
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
};
|
||||
}
|
||||
|
||||
function getMailPollingResponseTimeoutMs(payload = {}) {
|
||||
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
|
||||
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
|
||||
return Math.max(45000, maxAttempts * intervalMs + 25000);
|
||||
}
|
||||
|
||||
async function pollDesktopOtpCode(step, state = {}, nodeId = '') {
|
||||
if (typeof getMailConfig !== 'function') {
|
||||
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱配置能力,无法继续执行。');
|
||||
}
|
||||
const mail = getMailConfig(state);
|
||||
if (mail?.error) {
|
||||
throw new Error(mail.error);
|
||||
if (typeof pollFlowVerificationCode !== 'function') {
|
||||
throw new Error('Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。');
|
||||
}
|
||||
|
||||
const runtimeState = readKiroRuntime(state);
|
||||
const requestedAt = Math.max(0, Number(runtimeState.desktopAuth?.otpRequestedAt) || Date.now());
|
||||
const filterAfterTimestamp = mail.provider === '2925'
|
||||
const mailProvider = cleanString(state?.mailProvider).toLowerCase();
|
||||
const filterAfterTimestamp = mailProvider === '2925'
|
||||
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: requestedAt;
|
||||
const pollPayload = buildDesktopOtpPollPayload(step, state, mail, filterAfterTimestamp);
|
||||
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
|
||||
await ensureIcloudMailSession({
|
||||
state,
|
||||
step,
|
||||
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
|
||||
});
|
||||
}
|
||||
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询桌面授权验证码...`, 'info', nodeId);
|
||||
return pollHotmailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
if (mail.provider === LUCKMAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'LuckMail'} 轮询桌面授权验证码...`, 'info', nodeId);
|
||||
return pollLuckmailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloudflare Temp Email'} 轮询桌面授权验证码...`, 'info', nodeId);
|
||||
return pollCloudflareTempEmailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
if (mail.provider === CLOUD_MAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloud Mail'} 轮询桌面授权验证码...`, 'info', nodeId);
|
||||
return pollCloudMailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
if (mail.provider === YYDS_MAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'YYDS Mail'} 轮询桌面授权验证码...`, 'info', nodeId);
|
||||
return pollYydsMailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
|
||||
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
|
||||
await log(`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`, 'info', nodeId);
|
||||
await ensureMail2925MailboxSession({
|
||||
accountId: state.currentMail2925AccountId || null,
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
|
||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
|
||||
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
|
||||
});
|
||||
} else {
|
||||
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', nodeId);
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
|
||||
if (typeof sendToMailContentScriptResilient !== 'function') {
|
||||
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
|
||||
}
|
||||
|
||||
const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload);
|
||||
const result = await sendToMailContentScriptResilient(
|
||||
mail,
|
||||
{
|
||||
type: 'POLL_EMAIL',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: pollPayload,
|
||||
return pollFlowVerificationCode({
|
||||
actionLabel: '桌面授权验证码',
|
||||
filterAfterTimestamp,
|
||||
flowId: 'kiro',
|
||||
logStep: step,
|
||||
logStepKey: 'kiro-complete-desktop-authorize',
|
||||
missingCapabilityMessage: 'Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。',
|
||||
nodeId: 'kiro-complete-desktop-authorize',
|
||||
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`,
|
||||
state: {
|
||||
...state,
|
||||
activeFlowId: 'kiro',
|
||||
flowId: 'kiro',
|
||||
visibleStep: step,
|
||||
},
|
||||
{
|
||||
timeoutMs: responseTimeoutMs,
|
||||
responseTimeoutMs,
|
||||
maxRecoveryAttempts: 2,
|
||||
logStep: step,
|
||||
logStepKey: 'kiro-complete-desktop-authorize',
|
||||
}
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
if (!result?.code) {
|
||||
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`);
|
||||
}
|
||||
return result;
|
||||
step,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeKiroStartDesktopAuthorize(state = {}) {
|
||||
|
||||
@@ -32,42 +32,6 @@
|
||||
'https://profile.aws.amazon.com',
|
||||
]);
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
|
||||
Object.freeze({
|
||||
source: '(?:verification\\s*code|验证码|Your code is|code is)[::\\s]*(\\d{6})',
|
||||
flags: 'gi',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '^\\s*(\\d{6})\\s*$',
|
||||
flags: 'gm',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '>\\s*(\\d{6})\\s*<',
|
||||
flags: 'g',
|
||||
}),
|
||||
]);
|
||||
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
|
||||
'no-reply@signin.aws',
|
||||
'no-reply@login.awsapps.com',
|
||||
'noreply@amazon.com',
|
||||
'account-update@amazon.com',
|
||||
'no-reply@aws.amazon.com',
|
||||
'noreply@aws.amazon.com',
|
||||
'aws',
|
||||
]);
|
||||
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
|
||||
'aws builder id',
|
||||
'verification',
|
||||
'验证码',
|
||||
'code',
|
||||
'aws',
|
||||
]);
|
||||
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
|
||||
'verification',
|
||||
'验证码',
|
||||
'code',
|
||||
'aws',
|
||||
]);
|
||||
const KIRO_REGISTER_PAGE_STATES = Object.freeze([
|
||||
'kiro_signin_page',
|
||||
'email_entry',
|
||||
@@ -387,32 +351,17 @@
|
||||
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTab = null,
|
||||
ensureIcloudMailSession = null,
|
||||
ensureMail2925MailboxSession = null,
|
||||
generatePassword = null,
|
||||
generateRandomName = null,
|
||||
getMailConfig = null,
|
||||
getState = async () => ({}),
|
||||
getTabId = async () => null,
|
||||
HOTMAIL_PROVIDER = 'hotmail-api',
|
||||
LUCKMAIL_PROVIDER = 'luckmail-api',
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
|
||||
CLOUD_MAIL_PROVIDER = 'cloudmail',
|
||||
YYDS_MAIL_PROVIDER = 'yyds-mail',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
|
||||
isTabAlive = async () => false,
|
||||
KIRO_REGISTER_INJECT_FILES = null,
|
||||
pollCloudflareTempEmailVerificationCode = null,
|
||||
pollCloudMailVerificationCode = null,
|
||||
pollHotmailVerificationCode = null,
|
||||
pollLuckmailVerificationCode = null,
|
||||
pollYydsMailVerificationCode = null,
|
||||
pollFlowVerificationCode = null,
|
||||
registerTab = async () => {},
|
||||
resolveSignupEmailForFlow = null,
|
||||
reuseOrCreateTab = async () => null,
|
||||
sendToContentScriptResilient = null,
|
||||
sendToMailContentScriptResilient = null,
|
||||
setPasswordState = async () => {},
|
||||
setState = async () => {},
|
||||
sleepWithStop = async (ms) => {
|
||||
@@ -836,174 +785,36 @@
|
||||
};
|
||||
}
|
||||
|
||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
|
||||
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
|
||||
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
|
||||
if (accountEmail) {
|
||||
return accountEmail;
|
||||
}
|
||||
}
|
||||
|
||||
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function focusOrOpenMailTab(mail) {
|
||||
if (!mail?.source) {
|
||||
return;
|
||||
}
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = await getTabId(mail.source);
|
||||
if (Number.isInteger(tabId)) {
|
||||
await activateTab(tabId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
}
|
||||
|
||||
function buildKiroVerificationPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
|
||||
const runtimeState = readKiroRuntime(state);
|
||||
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
|
||||
const targetEmailHints = targetEmail ? [targetEmail] : [];
|
||||
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
|
||||
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
|
||||
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
|
||||
? 3
|
||||
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
|
||||
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
|
||||
? 15000
|
||||
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
|
||||
|
||||
return {
|
||||
flowId: 'kiro',
|
||||
step,
|
||||
targetEmail,
|
||||
targetEmailHints,
|
||||
filterAfterTimestamp,
|
||||
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
|
||||
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
|
||||
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
|
||||
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
|
||||
mail2925MatchTargetEmail: isMail2925Provider
|
||||
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
};
|
||||
}
|
||||
|
||||
function getMailPollingResponseTimeoutMs(payload = {}) {
|
||||
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
|
||||
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
|
||||
return Math.max(45000, maxAttempts * intervalMs + 25000);
|
||||
}
|
||||
|
||||
async function pollKiroVerificationCode(step, state = {}, nodeId = '') {
|
||||
if (typeof getMailConfig !== 'function') {
|
||||
throw new Error('Kiro 验证码步骤缺少邮箱配置能力,无法继续执行。');
|
||||
}
|
||||
const mail = getMailConfig(state);
|
||||
if (mail?.error) {
|
||||
throw new Error(mail.error);
|
||||
if (typeof pollFlowVerificationCode !== 'function') {
|
||||
throw new Error('Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。');
|
||||
}
|
||||
|
||||
const runtimeState = readKiroRuntime(state);
|
||||
const recordedRequestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || 0);
|
||||
const requestedAt = recordedRequestedAt || Math.max(0, Date.now() - MAIL_2925_FILTER_LOOKBACK_MS);
|
||||
const filterAfterTimestamp = mail.provider === '2925'
|
||||
const mailProvider = cleanString(state?.mailProvider).toLowerCase();
|
||||
const filterAfterTimestamp = mailProvider === '2925'
|
||||
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: requestedAt;
|
||||
const pollPayload = buildKiroVerificationPollPayload(step, state, mail, filterAfterTimestamp);
|
||||
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
|
||||
await ensureIcloudMailSession({
|
||||
state,
|
||||
step,
|
||||
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
|
||||
});
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询验证码...`, 'info', nodeId);
|
||||
return pollHotmailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
if (mail.provider === LUCKMAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'LuckMail'} 轮询验证码...`, 'info', nodeId);
|
||||
return pollLuckmailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloudflare Temp Email'} 轮询验证码...`, 'info', nodeId);
|
||||
return pollCloudflareTempEmailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
if (mail.provider === CLOUD_MAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloud Mail'} 轮询验证码...`, 'info', nodeId);
|
||||
return pollCloudMailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
if (mail.provider === YYDS_MAIL_PROVIDER) {
|
||||
await log(`步骤 ${step}:正在通过 ${mail.label || 'YYDS Mail'} 轮询验证码...`, 'info', nodeId);
|
||||
return pollYydsMailVerificationCode(step, state, pollPayload);
|
||||
}
|
||||
|
||||
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
|
||||
await log(`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`, 'info', nodeId);
|
||||
await ensureMail2925MailboxSession({
|
||||
accountId: state.currentMail2925AccountId || null,
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
|
||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
|
||||
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
|
||||
});
|
||||
} else {
|
||||
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', nodeId);
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
|
||||
if (typeof sendToMailContentScriptResilient !== 'function') {
|
||||
throw new Error('Kiro 验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
|
||||
}
|
||||
|
||||
const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload);
|
||||
const result = await sendToMailContentScriptResilient(
|
||||
mail,
|
||||
{
|
||||
type: 'POLL_EMAIL',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: pollPayload,
|
||||
return pollFlowVerificationCode({
|
||||
actionLabel: 'Kiro 验证码',
|
||||
filterAfterTimestamp,
|
||||
flowId: 'kiro',
|
||||
logStep: step,
|
||||
logStepKey: 'kiro-submit-verification-code',
|
||||
missingCapabilityMessage: 'Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到 Kiro 验证码。`,
|
||||
state: {
|
||||
...state,
|
||||
activeFlowId: 'kiro',
|
||||
flowId: 'kiro',
|
||||
visibleStep: step,
|
||||
},
|
||||
{
|
||||
timeoutMs: responseTimeoutMs,
|
||||
responseTimeoutMs,
|
||||
maxRecoveryAttempts: 2,
|
||||
logStep: step,
|
||||
logStepKey: 'kiro-submit-verification-code',
|
||||
}
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
if (!result?.code) {
|
||||
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
|
||||
}
|
||||
return result;
|
||||
step,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeKiroOpenRegisterPage(state = {}) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user