Merge branch 'dev'

# Conflicts:
#	manifest.json
This commit is contained in:
QLHazyCoder
2026-05-24 22:09:31 +08:00
76 changed files with 6095 additions and 1751 deletions
+373 -333
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -158,8 +158,6 @@
kiroRsKey: state.kiroRsKey,
autoRunSkipFailures: state.autoRunSkipFailures,
autoRunFallbackThreadIntervalMinutes: state.autoRunFallbackThreadIntervalMinutes,
autoRunDelayEnabled: state.autoRunDelayEnabled,
autoRunDelayMinutes: state.autoRunDelayMinutes,
autoStepDelaySeconds: state.autoStepDelaySeconds,
stepExecutionRangeByFlow: state.stepExecutionRangeByFlow,
signupMethod: state.signupMethod,
@@ -502,7 +500,6 @@
}, {
autoRunSessionId: 0,
autoRunTimerPlan: null,
scheduledAutoRunPlan: null,
});
clearStopRequest();
}
@@ -1159,7 +1156,6 @@
autoRunSessionId: 0,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
autoRunTimerPlan: null,
scheduledAutoRunPlan: null,
...getAutoRunStatusPayload(deps.getStopRequested() || stoppedEarly ? 'stopped' : 'complete', {
currentRun: deps.getStopRequested() || stoppedEarly ? afterRuntime.autoRunCurrentRun : afterRuntime.autoRunTotalRuns,
totalRuns: afterRuntime.autoRunTotalRuns,
+354
View File
@@ -0,0 +1,354 @@
(function attachBackgroundFlowMailPolling(root, factory) {
root.MultiPageBackgroundFlowMailPolling = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundFlowMailPollingModule() {
const ICLOUD_MAIL_POLL_MIN_ATTEMPTS = 5;
const ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS = 25000;
function cleanString(value = '') {
return String(value ?? '').trim();
}
function normalizeProviderId(value = '') {
return cleanString(value).toLowerCase();
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS);
}
function isIcloudMail(mail = {}) {
return mail?.source === 'icloud-mail' || mail?.provider === 'icloud';
}
function normalizeIcloudMailPollPayload(mail = {}, payload = {}) {
if (!isIcloudMail(mail)) {
return payload;
}
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
if (maxAttempts >= ICLOUD_MAIL_POLL_MIN_ATTEMPTS) {
return payload;
}
return {
...payload,
maxAttempts: ICLOUD_MAIL_POLL_MIN_ATTEMPTS,
};
}
function resolveMailPollingTimeouts(mail = {}, payload = {}) {
const normalizedPayload = normalizeIcloudMailPollPayload(mail, payload);
const responseTimeoutMs = getMailPollingResponseTimeoutMs(normalizedPayload);
return {
payload: normalizedPayload,
responseTimeoutMs,
timeoutMs: responseTimeoutMs,
};
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = cleanString(state?.currentMail2925AccountId);
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => cleanString(account?.id) === currentAccountId) || null;
const accountEmail = cleanString(currentAccount?.email).toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return cleanString(state?.mail2925BaseEmail).toLowerCase();
}
function createFlowMailPollingService(deps = {}) {
const {
addLog = async () => {},
buildVerificationPollPayloadForNode = null,
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
getMailConfig = null,
getTabId = async () => null,
handleMail2925LimitReachedError = null,
HOTMAIL_PROVIDER = 'hotmail-api',
isMail2925LimitReachedError = null,
isStopError = null,
isTabAlive = async () => false,
LUCKMAIL_PROVIDER = 'luckmail-api',
pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
reuseOrCreateTab = async () => null,
sendToMailContentScriptResilient = null,
throwIfStopped = () => {},
YYDS_MAIL_PROVIDER = 'yyds-mail',
} = deps;
const apiProviderHandlers = new Map([
[normalizeProviderId(HOTMAIL_PROVIDER), {
label: 'Hotmail',
poll: pollHotmailVerificationCode,
}],
[normalizeProviderId(LUCKMAIL_PROVIDER), {
label: 'LuckMail',
poll: pollLuckmailVerificationCode,
}],
[normalizeProviderId(CLOUDFLARE_TEMP_EMAIL_PROVIDER), {
label: 'Cloudflare Temp Email',
poll: pollCloudflareTempEmailVerificationCode,
}],
[normalizeProviderId(CLOUD_MAIL_PROVIDER), {
label: 'Cloud Mail',
poll: pollCloudMailVerificationCode,
}],
[normalizeProviderId(YYDS_MAIL_PROVIDER), {
label: 'YYDS Mail',
poll: pollYydsMailVerificationCode,
}],
]);
async function log(message, level = 'info', options = {}) {
const logOptions = options && typeof options === 'object' ? { ...options } : {};
await addLog(message, level, logOptions);
}
async function activateTab(tabId) {
if (!Number.isInteger(tabId) || !chrome?.tabs?.update) {
return;
}
await chrome.tabs.update(tabId, { active: true });
}
async function focusOrOpenMailTab(mail = {}) {
if (!mail?.source) {
return;
}
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
if (Number.isInteger(tabId)) {
await activateTab(tabId);
}
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
function assertPollResult(result, notFoundMessage) {
if (result?.error) {
throw new Error(result.error);
}
if (!result?.code) {
throw new Error(notFoundMessage || '邮箱轮询结束,但未获取到验证码。');
}
return result;
}
function isMail2925Provider(mail = {}) {
return normalizeProviderId(mail?.provider) === '2925';
}
async function pollThroughApiProvider(providerId, step, state, pollPayload, mail, options) {
const handler = apiProviderHandlers.get(providerId);
if (!handler) {
return null;
}
if (typeof handler.poll !== 'function') {
throw new Error(`${handler.label} 邮箱轮询能力未接入,无法继续执行。`);
}
await log(
`步骤 ${step}:正在通过 ${mail.label || handler.label} 轮询${options.actionLabel || '验证码'}...`,
'info',
options.logOptions
);
const result = await handler.poll(step, state, pollPayload);
return assertPollResult(result, options.notFoundMessage);
}
async function ensureBrowserMailSession(step, state, mail, options) {
if (isIcloudMail(mail) && typeof ensureIcloudMailSession === 'function') {
await log(
`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`,
'info',
options.logOptions
);
await ensureIcloudMailSession({
state,
step,
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
});
return;
}
if (isMail2925Provider(mail) && typeof ensureMail2925MailboxSession === 'function') {
await log(
`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`,
'info',
options.logOptions
);
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
});
return;
}
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', options.logOptions);
await focusOrOpenMailTab(mail);
}
async function pollThroughBrowserProvider(step, state, mail, pollPayload, options) {
await ensureBrowserMailSession(step, state, mail, options);
if (typeof sendToMailContentScriptResilient !== 'function') {
throw new Error(options.missingContentScriptMessage || '当前验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
}
const timeoutWindow = resolveMailPollingTimeouts(mail, pollPayload);
try {
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: timeoutWindow.payload,
},
{
timeoutMs: timeoutWindow.timeoutMs,
responseTimeoutMs: timeoutWindow.responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: options.logStep || step,
logStepKey: options.logStepKey || options.nodeId || '',
}
);
return assertPollResult(result, options.notFoundMessage);
} catch (error) {
if (typeof isStopError === 'function' && isStopError(error)) {
throw error;
}
if (
isMail2925Provider(mail)
&& typeof isMail2925LimitReachedError === 'function'
&& isMail2925LimitReachedError(error)
&& typeof handleMail2925LimitReachedError === 'function'
) {
throw await handleMail2925LimitReachedError(step, error);
}
throw error;
}
}
async function pollFlowVerificationCode(options = {}) {
const {
actionLabel = '验证码',
filterAfterTimestamp,
flowId = '',
logStep,
logStepKey = '',
missingCapabilityMessage = '当前验证码步骤缺少共享邮件轮询能力,无法继续执行。',
nodeId = '',
notFoundMessage = '',
payloadOverrides = {},
state = {},
step = 0,
} = options;
if (typeof getMailConfig !== 'function') {
throw new Error('当前验证码步骤缺少邮箱配置能力,无法继续执行。');
}
if (typeof buildVerificationPollPayloadForNode !== 'function') {
throw new Error(missingCapabilityMessage);
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
}
const ruleState = flowId
? {
...state,
activeFlowId: flowId,
flowId,
}
: state;
const nextOverrides = {
...(payloadOverrides || {}),
};
if (filterAfterTimestamp !== undefined) {
nextOverrides.filterAfterTimestamp = filterAfterTimestamp;
}
const pollPayload = buildVerificationPollPayloadForNode(nodeId, ruleState, nextOverrides);
const normalizedStep = Math.max(1, Math.floor(Number(pollPayload?.step || step) || 1));
const providerId = normalizeProviderId(mail?.provider);
const logOptions = {};
const normalizedLogStep = Math.floor(Number(logStep || normalizedStep) || 0);
if (normalizedLogStep > 0) {
logOptions.step = normalizedLogStep;
}
if (logStepKey || nodeId) {
logOptions.stepKey = logStepKey || nodeId;
logOptions.nodeId = nodeId || logStepKey;
}
throwIfStopped();
const apiResult = await pollThroughApiProvider(providerId, normalizedStep, ruleState, pollPayload, mail, {
actionLabel,
logOptions,
notFoundMessage,
});
if (apiResult) {
return apiResult;
}
return pollThroughBrowserProvider(normalizedStep, ruleState, mail, pollPayload, {
actionLabel,
logOptions,
logStep: normalizedLogStep || normalizedStep,
logStepKey: logStepKey || nodeId,
missingContentScriptMessage: missingCapabilityMessage,
nodeId,
notFoundMessage: notFoundMessage || `步骤 ${normalizedStep}:邮箱轮询结束,但未获取到验证码。`,
});
}
return {
focusOrOpenMailTab,
getExpectedMail2925MailboxEmail,
getMailPollingResponseTimeoutMs,
pollFlowVerificationCode,
resolveMailPollingTimeouts,
};
}
return {
createFlowMailPollingService,
getExpectedMail2925MailboxEmail,
getMailPollingResponseTimeoutMs,
resolveMailPollingTimeouts,
};
});
+1 -3
View File
@@ -209,8 +209,7 @@
function getAutoRunStatusPayload(phase, payload = {}) {
return {
autoRunning: phase === 'scheduled'
|| phase === 'running'
autoRunning: phase === 'running'
|| phase === 'waiting_step'
|| phase === 'waiting_email'
|| phase === 'retrying'
@@ -220,7 +219,6 @@
autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: Math.max(0, Math.floor(Number(payload.sessionId ?? payload.autoRunSessionId) || 0)),
scheduledAutoRunAt: Number.isFinite(Number(payload.scheduledAt)) ? Number(payload.scheduledAt) : null,
autoRunCountdownAt: Number.isFinite(Number(payload.countdownAt)) ? Number(payload.countdownAt) : null,
autoRunCountdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''),
autoRunCountdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''),
+9 -72
View File
@@ -11,12 +11,12 @@
buildPersistentSettingsPayload,
broadcastDataUpdate,
applyIpProxySettingsFromState,
cancelScheduledAutoRun,
checkIcloudSession,
clearAccountRunHistory,
deleteAccountRunHistoryRecords,
clearAutoRunTimerAlarm,
clearFreeReusablePhoneActivation,
clearGrokSsoCookies,
clearLuckmailRuntimeState,
clearYydsMailRuntimeState,
clearStopRequest,
@@ -144,7 +144,6 @@
normalizeMail2925Accounts,
normalizePayPalAccounts,
normalizeRunCount,
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
notifyNodeComplete,
notifyNodeError,
patchMail2925Account,
@@ -157,7 +156,6 @@
handleCloudflareSecurityBlocked,
resetState,
resumeAutoRun,
scheduleAutoRun,
selectLuckmailPurchase,
switchIpProxy,
changeIpProxyExit,
@@ -1148,6 +1146,13 @@
return await clearFreeReusablePhoneActivation();
}
case 'CLEAR_GROK_SSO_COOKIES': {
if (typeof clearGrokSsoCookies !== 'function') {
throw new Error('Grok SSO 清空能力未接入。');
}
return await clearGrokSsoCookies();
}
case 'SET_FREE_REUSABLE_PHONE': {
if (typeof setFreeReusablePhoneActivation !== 'function') {
throw new Error('白嫖复用手机号记录能力未接入。');
@@ -1316,7 +1321,7 @@
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
}
if (getPendingAutoRunTimerPlan(state)) {
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
throw new Error('已有线程间隔等待,请先停止或立即继续。');
}
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
@@ -1326,68 +1331,6 @@
return { ok: true };
}
case 'SCHEDULE_AUTO_RUN': {
clearStopRequest();
if (message.source === 'sidepanel') {
await lockAutomationWindowFromMessage(message, sender);
}
if (Boolean(message.payload?.accountContributionEnabled) && typeof setAccountContributionMode === 'function') {
await setAccountContributionMode(true, {
adapterId: message.payload?.contributionAdapterId,
flowId: message.payload?.activeFlowId || message.payload?.flowId,
});
if (typeof setState === 'function') {
const contributionNickname = String(message.payload?.contributionNickname || '').trim();
const contributionQq = String(message.payload?.contributionQq || '').trim();
await setState({
contributionNickname,
contributionQq,
});
}
}
const autoRunFlowStateUpdates = buildAutoRunFlowStateUpdates(message.payload || {});
if (Object.keys(autoRunFlowStateUpdates).length > 0 && typeof setState === 'function') {
await setState(autoRunFlowStateUpdates);
}
const state = await getState();
const autoRunStartValidation = validateAutoRunStart(state, {
activeFlowId: autoRunFlowStateUpdates.activeFlowId ?? state?.activeFlowId,
targetId: autoRunFlowStateUpdates.targetId ?? state?.targetId,
state,
});
if (autoRunStartValidation?.ok === false) {
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
}
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
return await scheduleAutoRun(totalRuns, {
delayMinutes: message.payload?.delayMinutes,
autoRunSkipFailures: Boolean(message.payload?.autoRunSkipFailures),
mode: message.payload?.mode,
});
}
case 'START_SCHEDULED_AUTO_RUN_NOW': {
clearStopRequest();
if (message.source === 'sidepanel') {
await lockAutomationWindowFromMessage(message, sender);
}
const started = await launchAutoRunTimerPlan('manual', {
expectedKinds: [AUTO_RUN_TIMER_KIND_SCHEDULED_START],
});
if (!started) {
throw new Error('当前没有可立即开始的倒计时计划。');
}
return { ok: true };
}
case 'CANCEL_SCHEDULED_AUTO_RUN': {
const cancelled = await cancelScheduledAutoRun();
if (!cancelled) {
throw new Error('当前没有可取消的倒计时计划。');
}
return { ok: true };
}
case 'SKIP_AUTO_RUN_COUNTDOWN': {
clearStopRequest();
if (message.source === 'sidepanel') {
@@ -1480,16 +1423,10 @@
|| (nextPlusModeEnabled && plusPaymentChanged)
|| (nextPlusModeEnabled && plusAccountAccessStrategyChanged)
|| phoneSignupReloginAfterBindEmailChanged;
const oauthFlowTimeoutDisabled = Object.prototype.hasOwnProperty.call(updates, 'oauthFlowTimeoutEnabled')
&& updates.oauthFlowTimeoutEnabled === false;
const canonicalSettingsUpdates = await setPersistentSettings(updates);
const stateUpdates = {
...canonicalSettingsUpdates,
...sessionUpdates,
...(oauthFlowTimeoutDisabled ? {
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
} : {}),
};
if (Object.prototype.hasOwnProperty.call(canonicalSettingsUpdates, 'activeFlowId')
&& !Object.prototype.hasOwnProperty.call(stateUpdates, 'flowId')) {
+2 -1
View File
@@ -33,7 +33,6 @@
contributionAdapterIds: [],
supportedTargetIds: [],
supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
canSwitchFlow: true,
stepDefinitionMode: 'default',
targetSelectorLabel: '来源',
@@ -59,6 +58,7 @@
const DEFAULT_TARGET_CAPABILITIES = Object.freeze({
supportsPhoneSignup: true,
requiresPhoneSignupWarning: false,
usesOauthTimeoutBudget: false,
supportedPlusAccountAccessStrategies: Object.freeze([PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]),
});
@@ -405,6 +405,7 @@
targetId: effectiveTargetId,
plusAccountAccessStrategy: effectivePlusAccountAccessStrategy,
plusModeEnabled: runtimeLocks.plusModeEnabled,
phoneVerificationEnabled: runtimeLocks.phoneVerificationEnabled,
signupMethod: effectiveSignupMethod,
},
supportedPanelModes: supportedTargetIds,
+14 -1
View File
@@ -18,7 +18,6 @@
contributionAdapterIds: [],
supportedTargetIds: [],
supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
canSwitchFlow: true,
stepDefinitionMode: 'default',
targetSelectorLabel: '\u6765\u6e90',
@@ -49,6 +48,20 @@
label: 'IP \u4ee3\u7406',
sectionIds: ['ip-proxy-section'],
},
'shared-auto-run': {
id: 'shared-auto-run',
label: '\u81ea\u52a8\u8fd0\u884c',
rowIds: [
'row-shared-auto-run',
'row-auto-run-thread-interval',
'row-step-execution-range',
],
},
'shared-settings-actions': {
id: 'shared-settings-actions',
label: '\u8bbe\u7f6e\u64cd\u4f5c',
rowIds: ['row-settings-actions'],
},
});
function buildFlowDefinitions() {
+1 -3
View File
@@ -209,8 +209,7 @@
function getAutoRunStatusPayload(phase, payload = {}) {
return {
autoRunning: phase === 'scheduled'
|| phase === 'running'
autoRunning: phase === 'running'
|| phase === 'waiting_step'
|| phase === 'waiting_email'
|| phase === 'retrying'
@@ -220,7 +219,6 @@
autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: Math.max(0, Math.floor(Number(payload.sessionId ?? payload.autoRunSessionId) || 0)),
scheduledAutoRunAt: Number.isFinite(Number(payload.scheduledAt)) ? Number(payload.scheduledAt) : null,
autoRunCountdownAt: Number.isFinite(Number(payload.countdownAt)) ? Number(payload.countdownAt) : null,
autoRunCountdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''),
autoRunCountdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''),
+425 -335
View File
@@ -17,6 +17,23 @@
return value;
}
function mergePlainObjects(baseValue = {}, patchValue = {}) {
if (Array.isArray(patchValue)) {
return patchValue.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchValue)) {
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
}
const baseObject = isPlainObject(baseValue) ? baseValue : {};
const next = {
...cloneValue(baseObject),
};
Object.entries(patchValue).forEach(([key, value]) => {
next[key] = mergePlainObjects(baseObject[key], value);
});
return next;
}
function normalizeStepExecutionRangeEntry(value = {}, fallback = {}) {
const source = isPlainObject(value) ? value : {};
const fallbackSource = isPlainObject(fallback) ? fallback : {};
@@ -47,6 +64,19 @@
const normalizeTargetId = typeof flowRegistry.normalizeTargetId === 'function'
? flowRegistry.normalizeTargetId
: ((_flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase());
const getRegisteredFlowIds = typeof flowRegistry.getRegisteredFlowIds === 'function'
? flowRegistry.getRegisteredFlowIds
: (() => ['openai', 'kiro']);
const getFlowDefinition = typeof flowRegistry.getFlowDefinition === 'function'
? flowRegistry.getFlowDefinition
: (() => null);
const getDefaultTargetId = typeof flowRegistry.getDefaultTargetId === 'function'
? flowRegistry.getDefaultTargetId
: ((flowId) => (flowId === 'kiro' ? defaultKiroTargetId : defaultOpenAiTargetId));
const getTargetDefinitions = typeof flowRegistry.getTargetDefinitions === 'function'
? flowRegistry.getTargetDefinitions
: (() => ({}));
const normalizePlusAccountAccessStrategy = (value = '') => {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'sub2api_codex_session') {
@@ -58,6 +88,92 @@
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 });
}
const lastStep = Math.max(1, Number(getFlowDefinition(flowId)?.workflowStepCount) || 1);
return { enabled: false, fromStep: 1, toStep: lastStep };
}
function buildDefaultTargetState(flowId, targetId) {
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),
},
};
return mergePlainObjects(base, getFlowSettingsDefaults(flowId));
}
function buildDefaultFlows() {
const flows = {};
getCanonicalFlowIds().forEach((flowId) => {
flows[flowId] = buildDefaultFlowSettings(flowId);
});
return flows;
}
function buildDefaultSettingsState() {
return {
schemaVersion: 5,
@@ -75,67 +191,7 @@
mode: 'account',
},
},
flows: {
openai: {
selectedTargetId: defaultOpenAiTargetId,
targets: {
cpa: {
vpsUrl: '',
vpsPassword: '',
localCpaStep9Mode: 'submit',
},
sub2api: {
sub2apiUrl: '',
sub2apiEmail: '',
sub2apiPassword: '',
sub2apiGroupName: 'codex',
sub2apiGroupNames: ['codex', 'openai-plus'],
sub2apiAccountPriority: 1,
sub2apiDefaultProxyName: '',
},
codex2api: {
codex2apiUrl: '',
codex2apiAdminKey: '',
},
},
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
},
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal-hosted',
plusAccountAccessStrategy: 'oauth',
hostedCheckoutVerificationUrl: '',
hostedCheckoutPhoneNumber: '',
plusHostedCheckoutOauthDelaySeconds: 3,
},
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 11,
},
},
},
kiro: {
selectedTargetId: defaultKiroTargetId,
targets: {
'kiro-rs': {
baseUrl: defaultKiroRsUrl,
apiKey: '',
},
},
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 9,
},
},
},
},
flows: buildDefaultFlows(),
};
}
@@ -147,6 +203,258 @@
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 ?? ''),
};
}
if (flowId === 'grok' && targetId === 'webchat2api') {
return {
...targetState,
baseUrl: String(targetState.baseUrl ?? '').trim(),
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 defaultOpenAiFlow = isPlainObject(defaults?.flows?.openai)
? defaults.flows.openai
: {};
const defaultOpenAiTargets = isPlainObject(defaultOpenAiFlow.targets)
? defaultOpenAiFlow.targets
: {};
const defaultOpenAiSignup = isPlainObject(defaultOpenAiFlow.signup)
? defaultOpenAiFlow.signup
: {};
const defaultOpenAiPlus = isPlainObject(defaultOpenAiFlow.plus)
? defaultOpenAiFlow.plus
: {};
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, defaultOpenAiTargets.cpa || {}),
sub2api: normalizeFlowTargetState('openai', 'sub2api', sub2apiSource, defaultOpenAiTargets.sub2api || {}),
codex2api: normalizeFlowTargetState('openai', 'codex2api', codex2apiSource, defaultOpenAiTargets.codex2api || {}),
},
signup: {
signupMethod: String(
input?.signupMethod
?? currentFlow.signup?.signupMethod
?? defaultOpenAiSignup.signupMethod
?? 'email'
).trim().toLowerCase() === 'phone' ? 'phone' : 'email',
phoneVerificationEnabled: Boolean(
input?.phoneVerificationEnabled
?? currentFlow.signup?.phoneVerificationEnabled
?? defaultOpenAiSignup.phoneVerificationEnabled
?? false
),
phoneSignupReloginAfterBindEmailEnabled: Boolean(
input?.phoneSignupReloginAfterBindEmailEnabled
?? currentFlow.signup?.phoneSignupReloginAfterBindEmailEnabled
?? defaultOpenAiSignup.phoneSignupReloginAfterBindEmailEnabled
?? false
),
},
plus: {
plusModeEnabled: Boolean(
input?.plusModeEnabled
?? currentFlow.plus?.plusModeEnabled
?? defaultOpenAiPlus.plusModeEnabled
?? false
),
plusPaymentMethod: String(
input?.plusPaymentMethod
?? currentFlow.plus?.plusPaymentMethod
?? defaultOpenAiPlus.plusPaymentMethod
?? 'paypal-hosted'
).trim() || defaultOpenAiPlus.plusPaymentMethod || 'paypal-hosted',
plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(
input?.plusAccountAccessStrategy
?? currentFlow.plus?.plusAccountAccessStrategy
?? defaultOpenAiPlus.plusAccountAccessStrategy
?? 'oauth'
),
hostedCheckoutVerificationUrl: String(
input?.hostedCheckoutVerificationUrl
?? currentFlow.plus?.hostedCheckoutVerificationUrl
?? defaultOpenAiPlus.hostedCheckoutVerificationUrl
?? ''
).trim(),
hostedCheckoutPhoneNumber: String(
input?.hostedCheckoutPhoneNumber
?? currentFlow.plus?.hostedCheckoutPhoneNumber
?? defaultOpenAiPlus.hostedCheckoutPhoneNumber
?? ''
).trim(),
plusHostedCheckoutOauthDelaySeconds: (() => {
const numeric = Number(
input?.plusHostedCheckoutOauthDelaySeconds
?? currentFlow.plus?.plusHostedCheckoutOauthDelaySeconds
?? defaultOpenAiPlus.plusHostedCheckoutOauthDelaySeconds
?? 3
);
const fallback = Number(defaultOpenAiPlus.plusHostedCheckoutOauthDelaySeconds ?? 3) || 3;
return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : fallback)));
})(),
},
};
}
function normalizeKiroSettings(input = {}, defaults = {}, currentFlow = {}) {
const defaultKiroFlow = isPlainObject(defaults?.flows?.kiro)
? defaults.flows.kiro
: {};
const defaultKiroTargets = isPlainObject(defaultKiroFlow.targets)
? defaultKiroFlow.targets
: {};
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, defaultKiroTargets['kiro-rs'] || {}),
},
};
}
function normalizeSettingsState(input = {}, options = {}) {
const defaults = buildDefaultSettingsState();
const nested = isPlainObject(input?.settingsState)
@@ -159,31 +467,7 @@
?? defaults.activeFlowId,
defaults.activeFlowId
);
const openaiSelectedTargetId = normalizeTargetId(
'openai',
nested?.flows?.openai?.selectedTargetId
?? nested?.flows?.openai?.integrationTargetId
?? (activeFlowId === 'openai'
? (input?.selectedTargetId ?? input?.targetId)
: undefined)
?? defaults.flows.openai.selectedTargetId,
defaults.flows.openai.selectedTargetId
);
const kiroSelectedTargetId = normalizeTargetId(
'kiro',
nested?.flows?.kiro?.selectedTargetId
?? nested?.flows?.kiro?.targetId
?? (activeFlowId === 'kiro'
? (input?.selectedTargetId ?? input?.targetId)
: undefined)
?? defaults.flows.kiro.selectedTargetId,
defaults.flows.kiro.selectedTargetId
);
const stepExecutionRangeByFlow = isPlainObject(input?.stepExecutionRangeByFlow)
? input.stepExecutionRangeByFlow
: {};
return {
const normalized = {
schemaVersion: Number(input?.settingsSchemaVersion || nested?.schemaVersion || defaults.schemaVersion) || defaults.schemaVersion,
activeFlowId,
services: {
@@ -219,203 +503,22 @@
).trim(),
},
},
flows: {
openai: {
selectedTargetId: openaiSelectedTargetId,
targets: {
cpa: {
...defaults.flows.openai.targets.cpa,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.cpa,
(state) => state.flows?.openai?.integrationTargets?.cpa
),
vpsUrl: String(
input?.vpsUrl
?? nested?.flows?.openai?.targets?.cpa?.vpsUrl
?? nested?.flows?.openai?.integrationTargets?.cpa?.vpsUrl
?? ''
).trim(),
vpsPassword: String(
input?.vpsPassword
?? nested?.flows?.openai?.targets?.cpa?.vpsPassword
?? nested?.flows?.openai?.integrationTargets?.cpa?.vpsPassword
?? ''
),
localCpaStep9Mode: String(
input?.localCpaStep9Mode
?? nested?.flows?.openai?.targets?.cpa?.localCpaStep9Mode
?? nested?.flows?.openai?.integrationTargets?.cpa?.localCpaStep9Mode
?? defaults.flows.openai.targets.cpa.localCpaStep9Mode
).trim() || defaults.flows.openai.targets.cpa.localCpaStep9Mode,
},
sub2api: {
...defaults.flows.openai.targets.sub2api,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.sub2api,
(state) => state.flows?.openai?.integrationTargets?.sub2api
),
sub2apiUrl: String(
input?.sub2apiUrl
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiUrl
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiUrl
?? ''
).trim(),
sub2apiEmail: String(
input?.sub2apiEmail
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiEmail
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiEmail
?? ''
).trim(),
sub2apiPassword: String(
input?.sub2apiPassword
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiPassword
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiPassword
?? ''
),
sub2apiGroupName: String(
input?.sub2apiGroupName
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiGroupName
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiGroupName
?? defaults.flows.openai.targets.sub2api.sub2apiGroupName
).trim() || defaults.flows.openai.targets.sub2api.sub2apiGroupName,
sub2apiGroupNames: Array.isArray(input?.sub2apiGroupNames)
? input.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: (Array.isArray(nested?.flows?.openai?.targets?.sub2api?.sub2apiGroupNames)
? nested.flows.openai.targets.sub2api.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: (Array.isArray(nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiGroupNames)
? nested.flows.openai.integrationTargets.sub2api.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: [...defaults.flows.openai.targets.sub2api.sub2apiGroupNames])),
sub2apiAccountPriority: Math.max(1, Number(
input?.sub2apiAccountPriority
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiAccountPriority
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiAccountPriority
?? defaults.flows.openai.targets.sub2api.sub2apiAccountPriority
) || defaults.flows.openai.targets.sub2api.sub2apiAccountPriority),
sub2apiDefaultProxyName: String(
input?.sub2apiDefaultProxyName
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiDefaultProxyName
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiDefaultProxyName
?? ''
).trim(),
},
codex2api: {
...defaults.flows.openai.targets.codex2api,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.codex2api,
(state) => state.flows?.openai?.integrationTargets?.codex2api
),
codex2apiUrl: String(
input?.codex2apiUrl
?? nested?.flows?.openai?.targets?.codex2api?.codex2apiUrl
?? nested?.flows?.openai?.integrationTargets?.codex2api?.codex2apiUrl
?? ''
).trim(),
codex2apiAdminKey: String(
input?.codex2apiAdminKey
?? nested?.flows?.openai?.targets?.codex2api?.codex2apiAdminKey
?? nested?.flows?.openai?.integrationTargets?.codex2api?.codex2apiAdminKey
?? ''
).trim(),
},
},
signup: {
signupMethod: String(
input?.signupMethod
?? nested?.flows?.openai?.signup?.signupMethod
?? defaults.flows.openai.signup.signupMethod
).trim().toLowerCase() === 'phone' ? 'phone' : 'email',
phoneVerificationEnabled: Boolean(
input?.phoneVerificationEnabled
?? nested?.flows?.openai?.signup?.phoneVerificationEnabled
?? defaults.flows.openai.signup.phoneVerificationEnabled
),
phoneSignupReloginAfterBindEmailEnabled: Boolean(
input?.phoneSignupReloginAfterBindEmailEnabled
?? nested?.flows?.openai?.signup?.phoneSignupReloginAfterBindEmailEnabled
?? defaults.flows.openai.signup.phoneSignupReloginAfterBindEmailEnabled
),
},
plus: {
plusModeEnabled: Boolean(
input?.plusModeEnabled
?? nested?.flows?.openai?.plus?.plusModeEnabled
?? defaults.flows.openai.plus.plusModeEnabled
),
plusPaymentMethod: String(
input?.plusPaymentMethod
?? nested?.flows?.openai?.plus?.plusPaymentMethod
?? defaults.flows.openai.plus.plusPaymentMethod
).trim() || defaults.flows.openai.plus.plusPaymentMethod,
plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(
input?.plusAccountAccessStrategy
?? nested?.flows?.openai?.plus?.plusAccountAccessStrategy
?? defaults.flows.openai.plus.plusAccountAccessStrategy
),
hostedCheckoutVerificationUrl: String(
input?.hostedCheckoutVerificationUrl
?? nested?.flows?.openai?.plus?.hostedCheckoutVerificationUrl
?? defaults.flows.openai.plus.hostedCheckoutVerificationUrl
).trim(),
hostedCheckoutPhoneNumber: String(
input?.hostedCheckoutPhoneNumber
?? nested?.flows?.openai?.plus?.hostedCheckoutPhoneNumber
?? defaults.flows.openai.plus.hostedCheckoutPhoneNumber
).trim(),
plusHostedCheckoutOauthDelaySeconds: (() => {
const numeric = Number(
input?.plusHostedCheckoutOauthDelaySeconds
?? nested?.flows?.openai?.plus?.plusHostedCheckoutOauthDelaySeconds
?? defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds
);
return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds)));
})(),
},
autoRun: {
stepExecutionRange: normalizeStepExecutionRangeEntry(
stepExecutionRangeByFlow.openai
?? nested?.flows?.openai?.autoRun?.stepExecutionRange
?? {},
defaults.flows.openai.autoRun.stepExecutionRange
),
},
},
kiro: {
selectedTargetId: kiroSelectedTargetId,
targets: {
'kiro-rs': {
...defaults.flows.kiro.targets['kiro-rs'],
...getTargetValue(
nested,
(state) => state.flows?.kiro?.targets?.['kiro-rs']
),
baseUrl: String(
input?.kiroRsUrl
?? input?.kiroRsBaseUrl
?? nested?.flows?.kiro?.targets?.['kiro-rs']?.baseUrl
?? defaults.flows.kiro.targets['kiro-rs'].baseUrl
).trim() || defaults.flows.kiro.targets['kiro-rs'].baseUrl,
apiKey: String(
input?.kiroRsKey
?? input?.kiroRsApiKey
?? nested?.flows?.kiro?.targets?.['kiro-rs']?.apiKey
?? defaults.flows.kiro.targets['kiro-rs'].apiKey
),
},
},
autoRun: {
stepExecutionRange: normalizeStepExecutionRangeEntry(
stepExecutionRangeByFlow.kiro
?? nested?.flows?.kiro?.autoRun?.stepExecutionRange
?? {},
defaults.flows.kiro.autoRun.stepExecutionRange
),
},
},
},
flows: {},
};
getCanonicalFlowIds().forEach((flowId) => {
normalized.flows[flowId] = normalizeFlowSettings(flowId, {
...input,
activeFlowId,
}, nested, defaults);
});
if (normalized.flows.openai) {
normalized.flows.openai = normalizeOpenAiSettings(input, nested, defaults, normalized.flows.openai);
}
if (normalized.flows.kiro) {
normalized.flows.kiro = normalizeKiroSettings(input, defaults, normalized.flows.kiro);
}
return normalized;
}
function mergeSettingsState(baseValue = {}, patchValue = {}) {
@@ -425,24 +528,8 @@
activeFlowId: patchValue?.activeFlowId ?? baseSettingsState.activeFlowId,
});
function mergeRecursive(baseNode, patchNode) {
if (Array.isArray(patchNode)) {
return patchNode.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchNode)) {
return patchNode === undefined ? cloneValue(baseNode) : patchNode;
}
const next = {
...cloneValue(isPlainObject(baseNode) ? baseNode : {}),
};
Object.entries(patchNode).forEach(([key, value]) => {
next[key] = mergeRecursive(baseNode?.[key], value);
});
return next;
}
return normalizeSettingsState({
settingsState: mergeRecursive(baseSettingsState, patchSettingsState),
settingsState: mergePlainObjects(baseSettingsState, patchSettingsState),
});
}
@@ -459,7 +546,7 @@
return normalizeTargetId(
normalizedFlowId,
flowSettings?.selectedTargetId,
normalizedFlowId === 'kiro' ? defaultKiroTargetId : defaultOpenAiTargetId
getDefaultTargetId(normalizedFlowId)
);
}
@@ -476,16 +563,16 @@
function buildStepExecutionRangeByFlow(settingsState = {}) {
const normalizedState = normalizeSettingsState(settingsState);
return {
openai: normalizeStepExecutionRangeEntry(
normalizedState?.flows?.openai?.autoRun?.stepExecutionRange,
buildDefaultSettingsState().flows.openai.autoRun.stepExecutionRange
),
kiro: normalizeStepExecutionRangeEntry(
normalizedState?.flows?.kiro?.autoRun?.stepExecutionRange,
buildDefaultSettingsState().flows.kiro.autoRun.stepExecutionRange
),
};
const defaults = buildDefaultSettingsState();
return Object.fromEntries(
Object.entries(normalizedState.flows || {}).map(([flowId, flowSettings]) => [
flowId,
normalizeStepExecutionRangeEntry(
flowSettings?.autoRun?.stepExecutionRange,
defaults.flows?.[flowId]?.autoRun?.stepExecutionRange || getDefaultStepExecutionRange(flowId)
),
])
);
}
function buildSettingsView(settingsState = {}, baseInput = {}) {
@@ -493,38 +580,41 @@
const next = {
...(isPlainObject(baseInput) ? cloneValue(baseInput) : {}),
};
const openaiState = normalizedState.flows.openai;
const kiroState = normalizedState.flows.kiro;
const openaiState = normalizedState.flows.openai || buildDefaultFlowSettings('openai');
const kiroState = normalizedState.flows.kiro || buildDefaultFlowSettings('kiro');
const grokState = normalizedState.flows.grok || buildDefaultFlowSettings('grok');
next.activeFlowId = normalizedState.activeFlowId;
next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId);
next.vpsUrl = openaiState.targets.cpa.vpsUrl;
next.vpsPassword = openaiState.targets.cpa.vpsPassword;
next.localCpaStep9Mode = openaiState.targets.cpa.localCpaStep9Mode;
next.sub2apiUrl = openaiState.targets.sub2api.sub2apiUrl;
next.sub2apiEmail = openaiState.targets.sub2api.sub2apiEmail;
next.sub2apiPassword = openaiState.targets.sub2api.sub2apiPassword;
next.sub2apiGroupName = openaiState.targets.sub2api.sub2apiGroupName;
next.sub2apiGroupNames = cloneValue(openaiState.targets.sub2api.sub2apiGroupNames);
next.sub2apiAccountPriority = openaiState.targets.sub2api.sub2apiAccountPriority;
next.sub2apiDefaultProxyName = openaiState.targets.sub2api.sub2apiDefaultProxyName;
next.codex2apiUrl = openaiState.targets.codex2api.codex2apiUrl;
next.codex2apiAdminKey = openaiState.targets.codex2api.codex2apiAdminKey;
next.vpsUrl = openaiState.targets.cpa?.vpsUrl || '';
next.vpsPassword = openaiState.targets.cpa?.vpsPassword || '';
next.localCpaStep9Mode = openaiState.targets.cpa?.localCpaStep9Mode || 'submit';
next.sub2apiUrl = openaiState.targets.sub2api?.sub2apiUrl || '';
next.sub2apiEmail = openaiState.targets.sub2api?.sub2apiEmail || '';
next.sub2apiPassword = openaiState.targets.sub2api?.sub2apiPassword || '';
next.sub2apiGroupName = openaiState.targets.sub2api?.sub2apiGroupName || 'codex';
next.sub2apiGroupNames = cloneValue(openaiState.targets.sub2api?.sub2apiGroupNames || ['codex', 'openai-plus']);
next.sub2apiAccountPriority = openaiState.targets.sub2api?.sub2apiAccountPriority || 1;
next.sub2apiDefaultProxyName = openaiState.targets.sub2api?.sub2apiDefaultProxyName || '';
next.codex2apiUrl = openaiState.targets.codex2api?.codex2apiUrl || '';
next.codex2apiAdminKey = openaiState.targets.codex2api?.codex2apiAdminKey || '';
next.customPassword = normalizedState.services.account.customPassword;
next.signupMethod = openaiState.signup.signupMethod;
next.phoneVerificationEnabled = openaiState.signup.phoneVerificationEnabled;
next.phoneSignupReloginAfterBindEmailEnabled = openaiState.signup.phoneSignupReloginAfterBindEmailEnabled;
next.plusModeEnabled = openaiState.plus.plusModeEnabled;
next.plusPaymentMethod = openaiState.plus.plusPaymentMethod;
next.plusAccountAccessStrategy = openaiState.plus.plusAccountAccessStrategy;
next.hostedCheckoutVerificationUrl = openaiState.plus.hostedCheckoutVerificationUrl;
next.hostedCheckoutPhoneNumber = openaiState.plus.hostedCheckoutPhoneNumber;
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus.plusHostedCheckoutOauthDelaySeconds;
next.signupMethod = openaiState.signup?.signupMethod || 'email';
next.phoneVerificationEnabled = Boolean(openaiState.signup?.phoneVerificationEnabled);
next.phoneSignupReloginAfterBindEmailEnabled = Boolean(openaiState.signup?.phoneSignupReloginAfterBindEmailEnabled);
next.plusModeEnabled = Boolean(openaiState.plus?.plusModeEnabled);
next.plusPaymentMethod = openaiState.plus?.plusPaymentMethod || 'paypal-hosted';
next.plusAccountAccessStrategy = openaiState.plus?.plusAccountAccessStrategy || 'oauth';
next.hostedCheckoutVerificationUrl = openaiState.plus?.hostedCheckoutVerificationUrl || '';
next.hostedCheckoutPhoneNumber = openaiState.plus?.hostedCheckoutPhoneNumber || '';
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus?.plusHostedCheckoutOauthDelaySeconds ?? 3;
next.mailProvider = normalizedState.services.email.provider;
next.ipProxyEnabled = normalizedState.services.proxy.enabled;
next.ipProxyService = normalizedState.services.proxy.provider;
next.ipProxyMode = normalizedState.services.proxy.mode;
next.kiroRsUrl = kiroState.targets['kiro-rs'].baseUrl;
next.kiroRsKey = kiroState.targets['kiro-rs'].apiKey;
next.kiroRsUrl = kiroState.targets['kiro-rs']?.baseUrl || '';
next.kiroRsKey = kiroState.targets['kiro-rs']?.apiKey || '';
next.grokWebchat2ApiUrl = grokState.targets.webchat2api?.baseUrl || '';
next.grokWebchat2ApiAdminKey = grokState.targets.webchat2api?.apiKey || '';
next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState);
next.settingsSchemaVersion = normalizedState.schemaVersion;
next.settingsState = cloneValue(normalizedState);
+1 -2
View File
@@ -49,7 +49,6 @@ flowCapabilities.openai = {
supportsContributionMode: true,
supportsPlatformBinding: ['cpa', 'sub2api', 'codex2api'],
supportsLuckmail: true,
supportsOauthTimeoutBudget: true,
stepDefinitionMode: 'openai-dynamic',
};
@@ -61,7 +60,6 @@ flowCapabilities.siteA = {
supportsContributionMode: false,
supportsPlatformBinding: ['siteA-panel'],
supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
stepDefinitionMode: 'siteA',
};
```
@@ -82,6 +80,7 @@ panelCapabilities = {
cpa: {
supportsPhoneSignup: true,
requiresPhoneSignupWarning: true,
usesOauthTimeoutBudget: true,
},
sub2api: {
supportsPhoneSignup: true,
@@ -0,0 +1,311 @@
(function attachBackgroundGrokPublisherWebchat2Api(root, factory) {
root.MultiPageBackgroundGrokPublisherWebchat2Api = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGrokPublisherWebchat2ApiModule(root) {
const grokStateApi = root?.MultiPageBackgroundGrokState || null;
const WEBCHAT2API_INJECT_PATH = '/api/remote-account/inject';
const DEFAULT_SOURCE_ID = 'flowpilot-grok-sso';
const DEFAULT_SOURCE_NAME = 'FlowPilot Grok SSO';
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 getErrorMessage(error) {
return error instanceof Error ? error.message : cleanString(error) || '未知错误';
}
async function readResponse(response) {
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch (_error) {
json = null;
}
return { text, json };
}
function readWebchat2ApiResponseMessage(body = {}, fallback = '') {
return cleanString(
body?.json?.error?.message
|| body?.json?.error
|| body?.json?.message
|| fallback
);
}
function normalizeWebchat2ApiBaseUrl(value = '') {
const rawUrl = cleanString(value);
if (!rawUrl) {
throw new Error('缺少 webchat2api 地址。');
}
const withProtocol = /^https?:\/\//i.test(rawUrl) ? rawUrl : `http://${rawUrl}`;
let parsed = null;
try {
parsed = new URL(withProtocol);
} catch (_error) {
throw new Error('webchat2api 地址格式无效,请检查配置。');
}
if (!/^https?:$/.test(parsed.protocol)) {
throw new Error('webchat2api 地址只支持 http 或 https。');
}
return parsed.origin;
}
function buildWebchat2ApiInjectUrl(value = '') {
return `${normalizeWebchat2ApiBaseUrl(value)}${WEBCHAT2API_INJECT_PATH}`;
}
function normalizeWebchat2ApiAdminKey(value = '') {
return cleanString(value);
}
function readGrokRuntime(state = {}) {
return grokStateApi?.ensureRuntimeState
? grokStateApi.ensureRuntimeState(state)
: (isPlainObject(state?.runtimeState?.flowState?.grok)
? state.runtimeState.flowState.grok
: (isPlainObject(state?.flowState?.grok) ? state.flowState.grok : {}));
}
function buildCanonicalRuntimePatch(currentState = {}, nextRuntimeState = {}) {
if (typeof grokStateApi?.buildRuntimeStatePatch === 'function') {
return grokStateApi.buildRuntimeStatePatch(currentState, nextRuntimeState);
}
const baseRuntimeState = isPlainObject(currentState?.runtimeState)
? cloneValue(currentState.runtimeState)
: {};
const baseFlowState = isPlainObject(baseRuntimeState.flowState)
? cloneValue(baseRuntimeState.flowState)
: {};
return {
runtimeState: {
...baseRuntimeState,
flowState: {
...baseFlowState,
grok: deepMerge(readGrokRuntime(currentState), nextRuntimeState),
},
},
};
}
function mergeRuntimePatch(currentState = {}, patch = {}) {
return buildCanonicalRuntimePatch(
currentState,
deepMerge(readGrokRuntime(currentState), patch)
);
}
function resolveGrokWebchat2ApiConfig(state = {}) {
const nestedConfig = state?.settingsState?.flows?.grok?.targets?.webchat2api || {};
return {
baseUrl: cleanString(nestedConfig.baseUrl || state?.grokWebchat2ApiUrl),
apiKey: normalizeWebchat2ApiAdminKey(nestedConfig.apiKey ?? state?.grokWebchat2ApiAdminKey ?? ''),
};
}
function resolveGrokSsoCookie(state = {}) {
const runtimeState = readGrokRuntime(state);
return cleanString(runtimeState?.sso?.currentCookie || state?.grokSsoCookie);
}
function buildGrokSsoInjectPayload(ssoCookie = '') {
const normalizedCookie = cleanString(ssoCookie);
if (!normalizedCookie) {
throw new Error('缺少 Grok SSO Cookie,请先完成步骤 5。');
}
return {
accounts: [{
token: normalizedCookie,
provider: 'grok',
type: 'sso',
}],
strategy: 'merge',
source_id: DEFAULT_SOURCE_ID,
source_name: DEFAULT_SOURCE_NAME,
provider: 'grok',
};
}
async function uploadGrokSsoToWebchat2Api(baseUrl, apiKey, ssoCookie, fetchImpl) {
const endpointUrl = buildWebchat2ApiInjectUrl(baseUrl);
const normalizedApiKey = normalizeWebchat2ApiAdminKey(apiKey);
if (!normalizedApiKey) {
throw new Error('缺少 webchat2api 管理密钥。');
}
const response = await fetchImpl(endpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${normalizedApiKey}`,
},
body: JSON.stringify(buildGrokSsoInjectPayload(ssoCookie)),
});
const body = await readResponse(response);
if (!response.ok) {
const message = readWebchat2ApiResponseMessage(body, response.statusText) || `HTTP ${response.status}`;
throw new Error(`webchat2api SSO 上传失败:${message}`);
}
if (isPlainObject(body.json) && Object.prototype.hasOwnProperty.call(body.json, 'code') && Number(body.json.code) !== 0) {
const message = readWebchat2ApiResponseMessage(body, `code=${body.json.code}`);
throw new Error(`webchat2api SSO 上传失败:${message}`);
}
return {
endpointUrl,
message: readWebchat2ApiResponseMessage(body, '') || '上传成功',
raw: body.json,
};
}
function createGrokWebchat2ApiPublisher(deps = {}) {
const {
addLog = async () => {},
completeNodeFromBackground,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getState = async () => ({}),
setState = async () => {},
} = deps;
if (typeof completeNodeFromBackground !== 'function') {
throw new Error('Grok webchat2api publisher requires completeNodeFromBackground.');
}
if (typeof fetchImpl !== 'function') {
throw new Error('Grok webchat2api publisher requires fetch support.');
}
async function log(message, level = 'info', nodeId = '') {
await addLog(message, level, nodeId ? { nodeId } : {});
}
async function applyRuntimeState(currentState = {}, patch = {}) {
const nextPatch = mergeRuntimePatch(currentState, patch);
await setState(nextPatch);
return nextPatch;
}
async function persistFailure(currentState = {}, message = '', targetUrl = '') {
const uploadPatch = {
status: 'error',
uploadedAt: 0,
message,
};
const normalizedTargetUrl = cleanString(targetUrl);
if (normalizedTargetUrl) {
uploadPatch.targetUrl = normalizedTargetUrl;
}
const nextPatch = mergeRuntimePatch(currentState, {
session: {
lastError: message,
},
upload: uploadPatch,
});
await setState(nextPatch);
}
async function executeGrokUploadSsoToWebchat2Api(state = {}) {
const nodeId = cleanString(state?.nodeId) || 'grok-upload-sso-to-webchat2api';
const currentState = await getState();
let failureTargetUrl = '';
try {
const targetConfig = resolveGrokWebchat2ApiConfig(currentState);
const endpointUrl = buildWebchat2ApiInjectUrl(targetConfig.baseUrl);
failureTargetUrl = endpointUrl;
const apiKey = normalizeWebchat2ApiAdminKey(targetConfig.apiKey);
if (!apiKey) {
throw new Error('缺少 webchat2api 管理密钥。');
}
const ssoCookie = resolveGrokSsoCookie(currentState);
if (!ssoCookie) {
throw new Error('缺少 Grok SSO Cookie,请先完成步骤 5。');
}
await applyRuntimeState(currentState, {
session: {
lastError: '',
},
upload: {
status: 'uploading',
uploadedAt: 0,
message: '',
targetUrl: endpointUrl,
},
});
await log('步骤 6:正在上传 Grok SSO 到 webchat2api...', 'info', nodeId);
const uploadResult = await uploadGrokSsoToWebchat2Api(
targetConfig.baseUrl,
apiKey,
ssoCookie,
fetchImpl
);
const uploadedAt = Date.now();
const payload = await applyRuntimeState(currentState, {
session: {
lastError: '',
},
upload: {
status: 'uploaded',
uploadedAt,
message: uploadResult.message || '上传成功',
targetUrl: uploadResult.endpointUrl,
},
});
await log(`步骤 6Grok SSO 已上传到 webchat2api,状态:${uploadResult.message || '上传成功'}`, 'ok', nodeId);
await completeNodeFromBackground(nodeId, payload);
} catch (error) {
const message = getErrorMessage(error);
await persistFailure(currentState, message, failureTargetUrl);
await log(`步骤 6${message}`, 'error', nodeId);
throw error;
}
}
return {
executeGrokUploadSsoToWebchat2Api,
};
}
return {
buildGrokSsoInjectPayload,
buildWebchat2ApiInjectUrl,
createGrokWebchat2ApiPublisher,
normalizeWebchat2ApiBaseUrl,
uploadGrokSsoToWebchat2Api,
};
});
+758
View File
@@ -0,0 +1,758 @@
(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_VERIFICATION_PAGE_STATE = 'verification_code_entry';
const GROK_VERIFICATION_READY_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 || {};
}
async function getGrokRegisterPageState(options = {}) {
return sendGrokCommand('GET_PAGE_STATE', {}, {
step: options.step || 0,
timeoutMs: options.timeoutMs || 15000,
logMessage: options.logMessage || '',
});
}
async function waitForGrokVerificationPageReady(tabId, options = {}) {
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || GROK_VERIFICATION_READY_TIMEOUT_MS);
const intervalMs = Math.max(250, Number(options.intervalMs) || 1000);
const deadline = Date.now() + timeoutMs;
let lastState = null;
let lastError = '';
while (Date.now() <= deadline) {
throwIfStopped();
try {
await ensureContentReady(tabId, {
timeoutMs: Math.min(DEFAULT_GROK_PAGE_TIMEOUT_MS, Math.max(5000, intervalMs + 3000)),
stableMs: 500,
initialDelayMs: 0,
logMessage: options.logMessage || '',
});
lastState = await getGrokRegisterPageState({
step: options.step || 0,
timeoutMs: Math.max(5000, intervalMs + 3000),
});
lastError = '';
if (lastState?.state === GROK_VERIFICATION_PAGE_STATE) {
return lastState;
}
} catch (error) {
lastError = getErrorMessage(error);
}
await sleepWithStop(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
}
const stateLabel = cleanString(lastState?.state) || 'unknown';
const urlLabel = cleanString(lastState?.url);
const errorLabel = lastError ? `,最后通信错误:${lastError}` : '';
throw new Error(`Grok 邮箱提交后尚未进入验证码页面,当前状态:${stateLabel}${urlLabel ? `URL${urlLabel}` : ''}${errorLabel}`);
}
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,
timeoutMs: GROK_VERIFICATION_READY_TIMEOUT_MS + 15000,
logMessage: '步骤 2:正在提交 Grok 注册邮箱...',
});
if (result.state !== GROK_VERIFICATION_PAGE_STATE) {
throw new Error(`Grok 邮箱提交后尚未进入验证码页面,当前状态:${cleanString(result.state) || 'unknown'}${cleanString(result.url) ? `URL${cleanString(result.url)}` : ''}`);
}
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 tabId = await ensureGrokRegisterTab(currentState, { openIfMissing: false });
await activateTab(tabId);
const readyState = await waitForGrokVerificationPageReady(tabId, {
step: 3,
logMessage: '步骤 3:正在等待 Grok 验证码页面就绪...',
});
await persistState({
grokPageState: readyState.state || '',
grokPageUrl: readyState.url || '',
...buildGrokRuntimePatch({
session: {
pageState: readyState.state || '',
pageUrl: readyState.url || '',
lastError: '',
},
}),
});
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 邮箱验证码。');
}
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 completedAt = Date.now();
const completionPatch = {
grokSsoCookie: ssoCookie,
grokSsoCookies: [ssoCookie],
grokSsoExtractedAt: completedAt,
grokCompletedAt: completedAt,
grokRegisterStatus: 'completed',
grokWebchat2ApiUploadStatus: '',
grokWebchat2ApiUploadedAt: 0,
grokWebchat2ApiUploadMessage: '',
grokWebchat2ApiTargetUrl: '',
...buildGrokRuntimePatch({
register: {
status: 'completed',
completedAt,
},
sso: {
currentCookie: ssoCookie,
cookies: [ssoCookie],
extractedAt: completedAt,
},
upload: {
status: '',
uploadedAt: 0,
message: '',
targetUrl: '',
},
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,
};
});
+421
View File
@@ -0,0 +1,421 @@
(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 assignCleanString(target = {}, key = '', value = '') {
const normalized = cleanString(value);
if (normalized) {
target[key] = normalized;
}
}
function assignPositiveInteger(target = {}, key = '', value) {
const numeric = Math.floor(Number(value));
if (Number.isInteger(numeric) && numeric > 0) {
target[key] = numeric;
}
}
function assignNonEmptyArray(target = {}, key = '', value) {
if (Array.isArray(value) && value.length) {
target[key] = value;
}
}
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,
},
upload: {
status: '',
uploadedAt: 0,
message: '',
targetUrl: '',
},
};
}
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)),
},
upload: {
status: cleanString(merged.upload?.status),
uploadedAt: Math.max(0, normalizeInteger(merged.upload?.uploadedAt)),
message: cleanString(merged.upload?.message),
targetUrl: cleanString(merged.upload?.targetUrl),
},
};
}
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,
grokWebchat2ApiUploadStatus: normalizedRuntimeState.upload.status,
grokWebchat2ApiUploadedAt: normalizedRuntimeState.upload.uploadedAt,
grokWebchat2ApiUploadMessage: normalizedRuntimeState.upload.message,
grokWebchat2ApiTargetUrl: normalizedRuntimeState.upload.targetUrl,
};
}
function ensureRuntimeState(state = {}) {
const runtimeFlowState = isPlainObject(state?.runtimeState?.flowState)
? state.runtimeState.flowState
: {};
const legacyFlowState = isPlainObject(state?.flowState?.grok)
? state.flowState.grok
: {};
const flatRuntime = {
session: {},
register: {},
sso: {},
upload: {},
};
assignPositiveInteger(flatRuntime.session, 'registerTabId', state.grokRegisterTabId);
assignCleanString(flatRuntime.session, 'pageState', state.grokPageState);
assignCleanString(flatRuntime.session, 'pageUrl', state.grokPageUrl || state.grokPostVerificationUrl);
assignCleanString(flatRuntime.register, 'email', state.grokEmail || state.email);
assignCleanString(flatRuntime.register, 'firstName', state.grokFirstName);
assignCleanString(flatRuntime.register, 'lastName', state.grokLastName);
assignCleanString(flatRuntime.register, 'password', state.grokPassword);
assignPositiveInteger(flatRuntime.register, 'verificationRequestedAt', state.grokVerificationRequestedAt);
assignCleanString(flatRuntime.register, 'verificationCode', state.grokVerificationCode);
assignCleanString(flatRuntime.register, 'status', state.grokRegisterStatus);
assignPositiveInteger(flatRuntime.register, 'completedAt', state.grokCompletedAt);
assignCleanString(flatRuntime.sso, 'currentCookie', state.grokSsoCookie);
assignNonEmptyArray(flatRuntime.sso, 'cookies', state.grokSsoCookies);
assignPositiveInteger(flatRuntime.sso, 'extractedAt', state.grokSsoExtractedAt || state.grokCompletedAt);
assignCleanString(flatRuntime.upload, 'status', state.grokWebchat2ApiUploadStatus);
assignPositiveInteger(flatRuntime.upload, 'uploadedAt', state.grokWebchat2ApiUploadedAt);
assignCleanString(flatRuntime.upload, 'message', state.grokWebchat2ApiUploadMessage);
assignCleanString(flatRuntime.upload, 'targetUrl', state.grokWebchat2ApiTargetUrl);
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.prototype.hasOwnProperty.call(payload, 'grokWebchat2ApiUploadStatus')) {
patch.upload = { ...(patch.upload || {}), status: payload.grokWebchat2ApiUploadStatus };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokWebchat2ApiUploadedAt')) {
patch.upload = { ...(patch.upload || {}), uploadedAt: payload.grokWebchat2ApiUploadedAt };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokWebchat2ApiUploadMessage')) {
patch.upload = { ...(patch.upload || {}), message: payload.grokWebchat2ApiUploadMessage };
}
if (Object.prototype.hasOwnProperty.call(payload, 'grokWebchat2ApiTargetUrl')) {
patch.upload = { ...(patch.upload || {}), targetUrl: payload.grokWebchat2ApiTargetUrl };
}
if (!Object.keys(patch).length) {
return {};
}
return buildRuntimeStatePatch(currentState, patch);
}
function buildFreshKeepState(currentState = {}) {
return buildRuntimeStatePatch(currentState, buildDefaultRuntimeState());
}
return {
applyNodeCompletionPayload,
buildDefaultRuntimeState,
buildDownstreamResetPatch,
buildFreshKeepState,
buildRuntimeStatePatch,
buildSessionStatePatch,
buildStateView,
ensureRuntimeState,
normalizeSsoCookies,
projectRuntimeFields,
};
});
+342
View File
@@ -0,0 +1,342 @@
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_EMAIL_VERIFICATION_READY_TIMEOUT_MS = 90 * 1000;
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 waitForGrokVerificationPageAfterEmailSubmit() {
const settledState = await waitForGrok(() => {
const errorText = getGrokEmailErrorText();
if (errorText) return { state: 'email_error', error: errorText, url: location.href };
const state = getGrokPageState();
return state === 'verification_code_entry' ? { state, url: location.href } : null;
}, { timeoutMs: GROK_EMAIL_VERIFICATION_READY_TIMEOUT_MS, intervalMs: 500 });
if (settledState?.error) {
throw new Error(settledState.error);
}
if (settledState?.state === 'verification_code_entry') {
return settledState;
}
const errorText = getGrokEmailErrorText();
if (errorText) {
throw new Error(errorText);
}
const finalState = getGrokPageState();
throw new Error(`提交 Grok 注册邮箱后未进入验证码页面,当前页面状态:${finalState || 'unknown'}。请确认页面已跳转到“验证您的邮箱”后再继续。`);
}
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);
}
const readyState = await waitForGrokVerificationPageAfterEmailSubmit();
return { submitted: true, state: readyState.state, url: readyState.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,
};
+159
View File
@@ -0,0 +1,159 @@
(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,
canSwitchFlow: true,
stepDefinitionMode: 'grok',
targetSelectorLabel: '来源',
},
baseGroups: ['grok-runtime-status', 'shared-auto-run', 'shared-settings-actions'],
targets: {
webchat2api: {
id: 'webchat2api',
label: 'webchat2api',
groups: [
'grok-target-webchat2api',
],
defaultState: {
baseUrl: '',
apiKey: '',
},
},
},
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',
],
},
'flows/grok/background/publisher-webchat2api': {
sourceId: 'grok-webchat2api',
commands: [
'grok-upload-sso-to-webchat2api',
],
},
},
defaultTargetId: 'webchat2api',
settingsDefaults: {
targets: {
webchat2api: {
baseUrl: '',
apiKey: '',
},
},
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 6,
},
},
},
settingsGroups: {
'grok-target-webchat2api': {
id: 'grok-target-webchat2api',
label: 'webchat2api',
rowIds: [
'row-grok-webchat2api-url',
'row-grok-webchat2api-key',
'row-grok-sso-settings',
],
},
'grok-runtime-status': {
id: 'grok-runtime-status',
label: 'Grok 运行态',
rowIds: [
'row-grok-register-status',
'row-grok-sso-status',
'row-grok-webchat2api-upload-status',
],
},
},
sourceAliases: {},
});
return VALUE;
});
+157
View File
@@ -0,0 +1,157 @@
(function attachGrokMailRules(root, factory) {
root.MultiPageGrokMailRules = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createGrokMailRulesModule(root) {
const grokStateApi = root.MultiPageBackgroundGrokState || null;
const SUBMIT_VERIFICATION_CODE_RULE_ID = 'grok-submit-verification-code';
const SUBMIT_VERIFICATION_CODE_NODE_ID = 'grok-submit-verification-code';
const GROK_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
flags: 'gi',
}),
Object.freeze({
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '(?:验证码|代码|确认码)[:\\s为]+(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '(?<!#)\\b(\\d{6})\\b',
flags: 'g',
}),
]);
const GROK_SENDER_FILTERS = Object.freeze([
'x.ai',
'xai',
'grok',
]);
const GROK_SUBJECT_FILTERS = Object.freeze([
'xai',
'x.ai',
'grok',
'verification',
'confirmation',
'code',
'验证码',
'确认码',
]);
const GROK_REQUIRED_KEYWORDS = Object.freeze([
'xai',
'x.ai',
'grok',
'verification',
'confirmation',
'code',
'验证码',
'确认码',
]);
function cleanString(value = '') {
return String(value ?? '').trim();
}
function readGrokRuntime(state = {}) {
if (typeof grokStateApi?.ensureRuntimeState === 'function') {
return grokStateApi.ensureRuntimeState(state);
}
return state?.runtimeState?.flowState?.grok || state?.flowState?.grok || {};
}
function buildTargetEmailHints(targetEmail = '') {
const normalizedTarget = cleanString(targetEmail).toLowerCase();
return normalizedTarget ? [normalizedTarget] : [];
}
function getVisibleStep(state = {}) {
const explicitStep = Number(state?.visibleStep || state?.step);
return Number.isInteger(explicitStep) && explicitStep > 0 ? explicitStep : 3;
}
function isMail2925Provider(state = {}) {
return cleanString(state?.mailProvider).toLowerCase() === '2925';
}
function shouldMatchMail2925TargetEmail(state = {}) {
return isMail2925Provider(state)
&& cleanString(state?.mail2925Mode).toLowerCase() === 'receive';
}
function createGrokMailRules(deps = {}) {
const {
LUCKMAIL_PROVIDER = 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
} = deps;
function getRuleDefinition(_input, state = {}) {
const runtimeState = readGrokRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.grokEmail || state?.email).toLowerCase();
const normalizedProvider = cleanString(state?.mailProvider).toLowerCase();
const mail2925Provider = isMail2925Provider(state);
const luckmailProvider = normalizedProvider === cleanString(LUCKMAIL_PROVIDER).toLowerCase();
return {
flowId: 'grok',
ruleId: SUBMIT_VERIFICATION_CODE_RULE_ID,
nodeId: SUBMIT_VERIFICATION_CODE_NODE_ID,
step: getVisibleStep(state),
artifactType: 'code',
codePatterns: GROK_VERIFICATION_CODE_PATTERNS,
filterAfterTimestamp: 0,
requiredKeywords: GROK_REQUIRED_KEYWORDS,
senderFilters: GROK_SENDER_FILTERS,
subjectFilters: GROK_SUBJECT_FILTERS,
targetEmail,
targetEmailHints: buildTargetEmailHints(targetEmail),
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
maxAttempts: luckmailProvider
? 3
: (mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5),
intervalMs: luckmailProvider
? 15000
: (mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 5000),
};
}
function getRuleDefinitionForNode(nodeId, state = {}) {
const normalizedNodeId = cleanString(nodeId);
if (normalizedNodeId && normalizedNodeId !== SUBMIT_VERIFICATION_CODE_NODE_ID) {
throw new Error(`Grok 邮件规则不支持节点:${normalizedNodeId}`);
}
return getRuleDefinition({ nodeId: SUBMIT_VERIFICATION_CODE_NODE_ID }, state);
}
function buildVerificationPollPayload(input, state = {}, overrides = {}) {
return {
...getRuleDefinition(input, state),
...(overrides || {}),
};
}
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
return {
...getRuleDefinitionForNode(nodeId, state),
...(overrides || {}),
};
}
return {
buildVerificationPollPayload,
buildVerificationPollPayloadForNode,
getRuleDefinition,
getRuleDefinitionForNode,
};
}
return {
GROK_REQUIRED_KEYWORDS,
GROK_SENDER_FILTERS,
GROK_SUBJECT_FILTERS,
GROK_VERIFICATION_CODE_PATTERNS,
SUBMIT_VERIFICATION_CODE_NODE_ID,
SUBMIT_VERIFICATION_CODE_RULE_ID,
createGrokMailRules,
};
});
+108
View File
@@ -0,0 +1,108 @@
(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',
},
{
id: 6,
order: 60,
key: 'grok-upload-sso-to-webchat2api',
title: '上传 SSO 到 webchat2api',
sourceId: 'grok-webchat2api',
driverId: 'flows/grok/background/publisher-webchat2api',
command: 'grok-upload-sso-to-webchat2api',
flowId: 'grok',
},
],
});
function getVariantStepDefinitions(variantKey = 'default') {
return Array.isArray(STEP_VARIANTS[variantKey]) ? STEP_VARIANTS[variantKey] : STEP_VARIANTS.default;
}
function getModeStepDefinitions() {
return getVariantStepDefinitions('default');
}
function getAllSteps() {
return getVariantStepDefinitions('default');
}
function getPlusPaymentStepTitle() {
return '';
}
function resolveStepTitle(step = {}) {
return step?.title || '';
}
return {
flowId: 'grok',
getAllSteps,
getModeStepDefinitions,
getPlusPaymentStepTitle,
getVariantStepDefinitions,
resolveStepTitle,
};
});
+10 -2
View File
@@ -12,6 +12,10 @@
id: 'kiro',
path: 'flows/kiro/',
},
grok: {
id: 'grok',
path: 'flows/grok/',
},
});
function normalizeFlowId(value = '') {
@@ -32,10 +36,14 @@
...baseEntry,
definition: normalized === 'openai'
? (rootScope.MultiPageOpenAiFlowDefinition || null)
: (rootScope.MultiPageKiroFlowDefinition || null),
: (normalized === 'kiro'
? (rootScope.MultiPageKiroFlowDefinition || null)
: (rootScope.MultiPageGrokFlowDefinition || null)),
workflow: normalized === 'openai'
? (rootScope.MultiPageOpenAiWorkflow || null)
: (rootScope.MultiPageKiroWorkflow || null),
: (normalized === 'kiro'
? (rootScope.MultiPageKiroWorkflow || null)
: (rootScope.MultiPageGrokWorkflow || null)),
};
}
+21 -207
View File
@@ -14,43 +14,6 @@
'https://app.kiro.dev/*',
'https://kiro.dev/*',
]);
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
@@ -375,32 +338,17 @@
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
completeNodeFromBackground,
ensureContentScriptReadyOnTab = null,
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getMailConfig = null,
getState = async () => ({}),
getTabId = async () => null,
HOTMAIL_PROVIDER = 'hotmail-api',
LUCKMAIL_PROVIDER = 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
YYDS_MAIL_PROVIDER = 'yyds-mail',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false,
maybeSubmitFlowContribution = async () => null,
KIRO_REGISTER_INJECT_FILES = null,
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
pollFlowVerificationCode = null,
registerTab = async () => {},
reuseOrCreateTab = async () => null,
sendToContentScriptResilient = null,
sendToMailContentScriptResilient = null,
setState = async () => {},
sleepWithStop = async (ms) => {
await new Promise((resolve) => setTimeout(resolve, ms));
@@ -658,44 +606,6 @@
return password;
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
if (!mail?.source) {
return;
}
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
if (Number.isInteger(tabId)) {
await activateTab(tabId);
}
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function collectKiroWebSessionTabs(currentState = {}) {
const runtimeState = readKiroRuntime(currentState);
const candidates = [];
@@ -901,131 +811,35 @@
throw new Error(`Kiro Web 登录态尚未建立。请在自动打开的 Kiro 账号页登录后,从步骤 7 继续。${detail}`);
}
function buildDesktopOtpPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
const runtimeState = readKiroRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
const targetEmailHints = targetEmail ? [targetEmail] : [];
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 3
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 15000
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
return {
flowId: 'kiro',
step,
targetEmail,
targetEmailHints,
filterAfterTimestamp,
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
mail2925MatchTargetEmail: isMail2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
maxAttempts,
intervalMs,
};
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + 25000);
}
async function pollDesktopOtpCode(step, state = {}, nodeId = '') {
if (typeof getMailConfig !== 'function') {
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱配置能力,无法继续执行。');
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
if (typeof pollFlowVerificationCode !== 'function') {
throw new Error('Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。');
}
const runtimeState = readKiroRuntime(state);
const requestedAt = Math.max(0, Number(runtimeState.desktopAuth?.otpRequestedAt) || Date.now());
const filterAfterTimestamp = mail.provider === '2925'
const mailProvider = cleanString(state?.mailProvider).toLowerCase();
const filterAfterTimestamp = mailProvider === '2925'
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: requestedAt;
const pollPayload = buildDesktopOtpPollPayload(step, state, mail, filterAfterTimestamp);
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
await ensureIcloudMailSession({
state,
step,
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
});
}
if (mail.provider === HOTMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollHotmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'LuckMail'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollLuckmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloudflare Temp Email'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollCloudflareTempEmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUD_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloud Mail'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollCloudMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === YYDS_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'YYDS Mail'} 轮询桌面授权验证码...`, 'info', nodeId);
return pollYydsMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`, 'info', nodeId);
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
});
} else {
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', nodeId);
await focusOrOpenMailTab(mail);
}
if (typeof sendToMailContentScriptResilient !== 'function') {
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
}
const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: pollPayload,
return pollFlowVerificationCode({
actionLabel: '桌面授权验证码',
filterAfterTimestamp,
flowId: 'kiro',
logStep: step,
logStepKey: 'kiro-complete-desktop-authorize',
missingCapabilityMessage: 'Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。',
nodeId: 'kiro-complete-desktop-authorize',
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`,
state: {
...state,
activeFlowId: 'kiro',
flowId: 'kiro',
visibleStep: step,
},
{
timeoutMs: responseTimeoutMs,
responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: step,
logStepKey: 'kiro-complete-desktop-authorize',
}
);
if (result?.error) {
throw new Error(result.error);
}
if (!result?.code) {
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`);
}
return result;
step,
});
}
async function executeKiroStartDesktopAuthorize(state = {}) {
+21 -209
View File
@@ -32,42 +32,6 @@
'https://profile.aws.amazon.com',
]);
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_REGISTER_PAGE_STATES = Object.freeze([
'kiro_signin_page',
'email_entry',
@@ -387,32 +351,17 @@
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
completeNodeFromBackground,
ensureContentScriptReadyOnTab = null,
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
generatePassword = null,
generateRandomName = null,
getMailConfig = null,
getState = async () => ({}),
getTabId = async () => null,
HOTMAIL_PROVIDER = 'hotmail-api',
LUCKMAIL_PROVIDER = 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
YYDS_MAIL_PROVIDER = 'yyds-mail',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false,
KIRO_REGISTER_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
pollFlowVerificationCode = null,
registerTab = async () => {},
resolveSignupEmailForFlow = null,
reuseOrCreateTab = async () => null,
sendToContentScriptResilient = null,
sendToMailContentScriptResilient = null,
setPasswordState = async () => {},
setState = async () => {},
sleepWithStop = async (ms) => {
@@ -954,173 +903,36 @@
};
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
if (!mail?.source) {
return;
}
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
if (Number.isInteger(tabId)) {
await activateTab(tabId);
}
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
function buildKiroVerificationPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
const targetEmail = resolveKiroRegisterEmail(state);
const targetEmailHints = targetEmail ? [targetEmail] : [];
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 3
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 15000
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
return {
flowId: 'kiro',
step,
targetEmail,
targetEmailHints,
filterAfterTimestamp,
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
mail2925MatchTargetEmail: isMail2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
maxAttempts,
intervalMs,
};
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + 25000);
}
async function pollKiroVerificationCode(step, state = {}, nodeId = '') {
if (typeof getMailConfig !== 'function') {
throw new Error('Kiro 验证码步骤缺少邮箱配置能力,无法继续执行。');
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
if (typeof pollFlowVerificationCode !== 'function') {
throw new Error('Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。');
}
const runtimeState = readKiroRuntime(state);
const recordedRequestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || 0);
const requestedAt = recordedRequestedAt || Math.max(0, Date.now() - MAIL_2925_FILTER_LOOKBACK_MS);
const filterAfterTimestamp = mail.provider === '2925'
const mailProvider = cleanString(state?.mailProvider).toLowerCase();
const filterAfterTimestamp = mailProvider === '2925'
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: requestedAt;
const pollPayload = buildKiroVerificationPollPayload(step, state, mail, filterAfterTimestamp);
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
await ensureIcloudMailSession({
state,
step,
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
});
}
throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询验证码...`, 'info', nodeId);
return pollHotmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'LuckMail'} 轮询验证码...`, 'info', nodeId);
return pollLuckmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloudflare Temp Email'} 轮询验证码...`, 'info', nodeId);
return pollCloudflareTempEmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUD_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloud Mail'} 轮询验证码...`, 'info', nodeId);
return pollCloudMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === YYDS_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'YYDS Mail'} 轮询验证码...`, 'info', nodeId);
return pollYydsMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`, 'info', nodeId);
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
});
} else {
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', nodeId);
await focusOrOpenMailTab(mail);
}
if (typeof sendToMailContentScriptResilient !== 'function') {
throw new Error('Kiro 验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
}
const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: pollPayload,
return pollFlowVerificationCode({
actionLabel: 'Kiro 验证码',
filterAfterTimestamp,
flowId: 'kiro',
logStep: step,
logStepKey: 'kiro-submit-verification-code',
missingCapabilityMessage: 'Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。',
nodeId: 'kiro-submit-verification-code',
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到 Kiro 验证码。`,
state: {
...state,
activeFlowId: 'kiro',
flowId: 'kiro',
visibleStep: step,
},
{
timeoutMs: responseTimeoutMs,
responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: step,
logStepKey: 'kiro-submit-verification-code',
}
);
if (result?.error) {
throw new Error(result.error);
}
if (!result?.code) {
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
}
return result;
step,
});
}
async function executeKiroOpenRegisterPage(state = {}) {
+7 -2
View File
@@ -34,18 +34,23 @@
"kiro-rs"
],
"supportsLuckmail": false,
"supportsOauthTimeoutBudget": false,
"canSwitchFlow": true,
"stepDefinitionMode": "kiro",
"targetSelectorLabel": "来源"
},
"baseGroups": [
"kiro-runtime-status"
"kiro-runtime-status",
"shared-auto-run",
"shared-settings-actions"
],
"targets": {
"kiro-rs": {
"id": "kiro-rs",
"label": "kiro.rs",
"defaultState": {
"baseUrl": "",
"apiKey": ""
},
"groups": [
"kiro-target-kiro-rs"
]
+162
View File
@@ -0,0 +1,162 @@
(function attachKiroMailRules(root, factory) {
root.MultiPageKiroMailRules = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createKiroMailRulesModule(root) {
const kiroStateApi = root.MultiPageBackgroundKiroState || null;
const SUBMIT_VERIFICATION_CODE_RULE_ID = 'kiro-submit-verification-code';
const DESKTOP_AUTHORIZE_CODE_RULE_ID = 'kiro-complete-desktop-authorize';
const SUBMIT_VERIFICATION_CODE_NODE_ID = 'kiro-submit-verification-code';
const DESKTOP_AUTHORIZE_NODE_ID = 'kiro-complete-desktop-authorize';
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
function cleanString(value = '') {
return String(value ?? '').trim();
}
function readKiroRuntime(state = {}) {
if (typeof kiroStateApi?.ensureRuntimeState === 'function') {
return kiroStateApi.ensureRuntimeState(state);
}
return state?.runtimeState?.flowState?.kiro || state?.flowState?.kiro || {};
}
function buildTargetEmailHints(targetEmail = '') {
const normalizedTarget = cleanString(targetEmail).toLowerCase();
return normalizedTarget ? [normalizedTarget] : [];
}
function resolveNodeId(input) {
const directNodeId = cleanString(input?.nodeId || input);
if (directNodeId === DESKTOP_AUTHORIZE_NODE_ID) {
return DESKTOP_AUTHORIZE_NODE_ID;
}
return SUBMIT_VERIFICATION_CODE_NODE_ID;
}
function getVisibleStepForNode(nodeId, state = {}) {
const explicitStep = Number(state?.visibleStep || state?.step);
if (Number.isInteger(explicitStep) && explicitStep > 0) {
return explicitStep;
}
return nodeId === DESKTOP_AUTHORIZE_NODE_ID ? 8 : 4;
}
function isMail2925Provider(state = {}) {
return cleanString(state?.mailProvider).toLowerCase() === '2925';
}
function shouldMatchMail2925TargetEmail(state = {}) {
return isMail2925Provider(state)
&& cleanString(state?.mail2925Mode).toLowerCase() === 'receive';
}
function createKiroMailRules(deps = {}) {
const {
LUCKMAIL_PROVIDER = 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
} = deps;
function getRuleDefinition(input, state = {}) {
const nodeId = resolveNodeId(input);
const normalizedStep = getVisibleStepForNode(nodeId, state);
const runtimeState = readKiroRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
const normalizedProvider = cleanString(state?.mailProvider).toLowerCase();
const mail2925Provider = isMail2925Provider(state);
const luckmailProvider = normalizedProvider === cleanString(LUCKMAIL_PROVIDER).toLowerCase();
return {
flowId: 'kiro',
ruleId: nodeId === DESKTOP_AUTHORIZE_NODE_ID
? DESKTOP_AUTHORIZE_CODE_RULE_ID
: SUBMIT_VERIFICATION_CODE_RULE_ID,
nodeId,
step: normalizedStep,
artifactType: 'code',
codePatterns: KIRO_AWS_VERIFICATION_CODE_PATTERNS,
filterAfterTimestamp: 0,
requiredKeywords: KIRO_AWS_REQUIRED_KEYWORDS,
senderFilters: KIRO_AWS_SENDER_FILTERS,
subjectFilters: KIRO_AWS_SUBJECT_FILTERS,
targetEmail,
targetEmailHints: buildTargetEmailHints(targetEmail),
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
maxAttempts: luckmailProvider
? 3
: (mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5),
intervalMs: luckmailProvider
? 15000
: (mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000),
};
}
function getRuleDefinitionForNode(nodeId, state = {}) {
return getRuleDefinition({ nodeId }, state);
}
function buildVerificationPollPayload(input, state = {}, overrides = {}) {
return {
...getRuleDefinition(input, state),
...(overrides || {}),
};
}
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
return buildVerificationPollPayload({ nodeId }, state, overrides);
}
return {
buildVerificationPollPayload,
buildVerificationPollPayloadForNode,
getRuleDefinition,
getRuleDefinitionForNode,
};
}
return {
DESKTOP_AUTHORIZE_CODE_RULE_ID,
DESKTOP_AUTHORIZE_NODE_ID,
KIRO_AWS_REQUIRED_KEYWORDS,
KIRO_AWS_SENDER_FILTERS,
KIRO_AWS_SUBJECT_FILTERS,
KIRO_AWS_VERIFICATION_CODE_PATTERNS,
SUBMIT_VERIFICATION_CODE_NODE_ID,
SUBMIT_VERIFICATION_CODE_RULE_ID,
createKiroMailRules,
};
});
+38 -4
View File
@@ -6528,18 +6528,27 @@ function getSerializableRect(el) {
// Step 5: Fill Name & Birthday / Age
// ============================================================
function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, outcome = null } = {}) {
function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, navigationEventType = '', outcome = null } = {}) {
const payload = {
profileSubmitted: true,
postSubmitChecked: true,
postSubmitChecked: !navigationStarted,
};
if (isAgeMode) {
payload.ageMode = true;
}
if (navigationStarted) {
payload.navigationStarted = true;
payload.handoffToBackground = true;
const resolvedNavigationEventType = String(navigationEventType || '').trim();
if (resolvedNavigationEventType) {
payload.navigationEventType = resolvedNavigationEventType;
}
if (typeof location !== 'undefined' && location?.href) {
payload.url = location.href;
}
}
if (outcome?.state) {
payload.postSubmitChecked = true;
payload.outcome = outcome.state;
}
if (outcome?.url) {
@@ -6825,6 +6834,7 @@ function installStep5NavigationCompletionReporter(completeOnce) {
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
return () => {};
}
let reportedNavigation = false;
const debugLog = typeof logStep5SubmitDebug === 'function'
? logStep5SubmitDebug
: (message, options = {}) => {
@@ -6838,6 +6848,25 @@ function installStep5NavigationCompletionReporter(completeOnce) {
const onNavigationStarted = (event) => {
const eventType = String(event?.type || 'navigation').trim() || 'navigation';
if (reportedNavigation) {
return;
}
reportedNavigation = true;
if (typeof completeOnce === 'function') {
try {
completeOnce({
navigationStarted: true,
navigationEventType: eventType,
});
} catch (error) {
if (typeof log === 'function') {
log(`步骤 5 [调试] 导航交棒信号发送失败:${error?.message || error}`, 'warn', {
step: 5,
stepKey: 'fill-profile',
});
}
}
}
debugLog(`检测到页面开始导航(event=${eventType})。`, {
level: 'warn',
});
@@ -7240,13 +7269,18 @@ async function step5_fillNameBirthday(payload) {
const completionPayload = getStep5DirectCompletionPayload({
isAgeMode,
navigationStarted: Boolean(extra.navigationStarted),
navigationEventType: extra.navigationEventType || '',
outcome: extra.outcome || null,
});
reportedCompletionPayload = completionPayload;
if (extra?.navigationStarted && typeof reportNodeComplete === 'function') {
reportNodeComplete('fill-profile', completionPayload);
} else {
reportComplete(5, completionPayload);
}
debugLog(`准备发送完成信号(reason=${completionReason}isAgeMode=${isAgeMode})。`, {
level: extra?.navigationStarted ? 'warn' : 'info',
});
reportedCompletionPayload = completionPayload;
reportComplete(5, completionPayload);
return completionPayload;
}
+49 -4
View File
@@ -38,7 +38,6 @@
"codex2api"
],
"supportsLuckmail": true,
"supportsOauthTimeoutBudget": true,
"canSwitchFlow": true,
"stepDefinitionMode": "openai-dynamic",
"targetSelectorLabel": "来源"
@@ -46,13 +45,20 @@
"baseGroups": [
"openai-plus",
"openai-phone",
"shared-auto-run",
"openai-oauth",
"openai-step6"
"openai-step6",
"shared-settings-actions"
],
"targets": {
"cpa": {
"id": "cpa",
"label": "CPA 面板",
"defaultState": {
"vpsUrl": "",
"vpsPassword": "",
"localCpaStep9Mode": "submit"
},
"groups": [
"openai-target-cpa"
]
@@ -60,6 +66,18 @@
"sub2api": {
"id": "sub2api",
"label": "SUB2API",
"defaultState": {
"sub2apiUrl": "",
"sub2apiEmail": "",
"sub2apiPassword": "",
"sub2apiGroupName": "codex",
"sub2apiGroupNames": [
"codex",
"openai-plus"
],
"sub2apiAccountPriority": 1,
"sub2apiDefaultProxyName": ""
},
"groups": [
"openai-target-sub2api"
]
@@ -67,11 +85,37 @@
"codex2api": {
"id": "codex2api",
"label": "Codex2API",
"defaultState": {
"codex2apiUrl": "",
"codex2apiAdminKey": ""
},
"groups": [
"openai-target-codex2api"
]
}
},
"settingsDefaults": {
"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
}
}
},
"runtimeSources": {
"openai-auth": {
"flowId": "openai",
@@ -382,8 +426,8 @@
"id": "openai-oauth",
"label": "OAuth",
"rowIds": [
"row-oauth-flow-timeout",
"row-oauth-display"
"row-oauth-display",
"row-oauth-callback"
]
},
"openai-step6": {
@@ -398,6 +442,7 @@
"cpa": {
"supportsPhoneSignup": true,
"requiresPhoneSignupWarning": true,
"usesOauthTimeoutBudget": true,
"supportedPlusAccountAccessStrategies": [
"oauth",
"cpa_codex_session"
+18
View File
@@ -3027,11 +3027,19 @@
'paypal-hosted-review',
'gopay-subscription-confirm',
]);
const POST_LOGIN_PHONE_VERIFICATION_STEP_KEYS = Object.freeze([
'post-login-phone-verification',
'post-bound-email-phone-verification',
]);
function omitPlusPaymentChainSteps(steps = []) {
return steps.filter((step) => !PLUS_PAYMENT_CHAIN_STEP_KEYS.includes(String(step?.key || '').trim()));
}
function omitPostLoginPhoneVerificationSteps(steps = []) {
return steps.filter((step) => !POST_LOGIN_PHONE_VERIFICATION_STEP_KEYS.includes(String(step?.key || '').trim()));
}
function reindexModeStepDefinitions(steps = []) {
return (Array.isArray(steps) ? steps : []).map((step, index) => ({
...step,
@@ -3116,6 +3124,13 @@
return Boolean(options?.phoneSignupReloginAfterBindEmailEnabled);
}
function isPhoneVerificationEnabled(options = {}) {
if (Object.prototype.hasOwnProperty.call(options || {}, 'phoneVerificationEnabled')) {
return Boolean(options.phoneVerificationEnabled);
}
return true;
}
function normalizePlusAccountAccessStrategy(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
@@ -3206,6 +3221,9 @@
) {
steps = omitPlusPaymentChainSteps(steps);
}
if (!isPhoneVerificationEnabled(options)) {
steps = omitPostLoginPhoneVerificationSteps(steps);
}
return reindexModeStepDefinitions(steps);
}
+7 -2
View File
@@ -1,8 +1,8 @@
{
"manifest_version": 3,
"name": "FlowPilot",
"version": "2.4",
"version_name": "FlowPilot2.4",
"version": "2.5",
"version_name": "FlowPilot2.5",
"description": "用于自动执行多步骤 OAuth 注册流程",
"permissions": [
"sidePanel",
@@ -51,6 +51,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
@@ -72,6 +73,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
@@ -93,6 +95,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
@@ -111,6 +114,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
@@ -128,6 +132,7 @@
"content/activation-utils.js",
"flows/openai/index.js",
"flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js",
"core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js",
+32 -2
View File
@@ -191,15 +191,44 @@
return Boolean(normalizedAdapterId) && getContributionAdapterIds(flowId).includes(normalizedAdapterId);
}
function assertPublishedFlowsHaveContributionAdapters(flowIds = undefined) {
function hasPublicationTargets(flowId = DEFAULT_FLOW_ID) {
if (typeof flowRegistryApi.getPublicationTargetDefinitions !== 'function') {
return false;
}
const definitions = flowRegistryApi.getPublicationTargetDefinitions(flowId) || {};
return Object.keys(definitions).length > 0;
}
function expectsContributionAdapter(flowId = DEFAULT_FLOW_ID) {
const normalizedFlowId = normalizeFlowId(flowId, DEFAULT_FLOW_ID);
const capabilities = typeof flowRegistryApi.getFlowCapabilities === 'function'
? (flowRegistryApi.getFlowCapabilities(normalizedFlowId) || {})
: {};
return Boolean(
hasPublicationTargets(normalizedFlowId)
|| capabilities.supportsAccountContribution
|| capabilities.supportsContributionMode
|| getContributionAdapterIds(normalizedFlowId).length
);
}
function getPublishedContributionFlowIds(flowIds = undefined) {
const ids = Array.isArray(flowIds)
? flowIds
: (typeof flowRegistryApi.getRegisteredFlowIds === 'function'
? flowRegistryApi.getRegisteredFlowIds()
: Object.keys(FLOW_ADAPTER_IDS));
const missing = ids
return ids
.map((flowId) => normalizeString(flowId).toLowerCase())
.filter(Boolean)
.filter((flowId) => expectsContributionAdapter(flowId));
}
function assertPublishedFlowsHaveContributionAdapters(flowIds = undefined) {
const ids = Array.isArray(flowIds)
? flowIds.map((flowId) => normalizeString(flowId).toLowerCase()).filter(Boolean)
: getPublishedContributionFlowIds();
const missing = ids
.filter((flowId) => getContributionAdapterIds(flowId).length === 0);
if (missing.length) {
throw new Error(`缺少账号贡献适配器:${missing.join(', ')}`);
@@ -217,6 +246,7 @@
getContributionAdapterIds,
getContributionAdapters,
getDefaultContributionAdapterId,
getPublishedContributionFlowIds,
hasContributionAdapter,
normalizeAdapterId,
normalizeFlowId,
+1 -1
View File
@@ -1263,7 +1263,7 @@ function updateIpProxyUI(state = latestState) {
const isAccountMode = mode === 'account';
const showSessionOptions = isAccountMode && service === '711proxy';
const hasAccountListConfigured = accountListAvailable && isAccountMode && hasCurrentInputAccountListEntries();
const canOperate = !isAutoRunLockedPhase() && !isAutoRunScheduledPhase();
const canOperate = !isAutoRunLockedPhase();
const actionState = getIpProxyActionState();
const actionBusy = Boolean(actionState.busy);
const busyAction = normalizeIpProxyActionType(actionState.action);
+19 -34
View File
@@ -1268,6 +1268,19 @@ header {
width: 76px;
}
#row-flow-selector .data-label,
#row-source-selector .data-label,
#row-grok-webchat2api-url .data-label,
#row-grok-webchat2api-key .data-label,
#row-grok-register-status .data-label,
#row-grok-sso-status .data-label,
#row-grok-webchat2api-upload-status .data-label,
#row-grok-sso-settings .data-label {
width: 76px;
text-transform: none;
letter-spacing: 0.02em;
}
#row-codex2api-url .data-label {
font-size: 10px;
letter-spacing: 0.02em;
@@ -2197,15 +2210,6 @@ header {
justify-content: flex-end;
}
.auto-delay-setting-pair {
flex-wrap: nowrap;
}
#row-auto-delay-settings .setting-group-secondary {
margin-left: auto;
min-width: 0;
}
.step6-cookie-cleanup-setting {
gap: 8px;
}
@@ -2227,25 +2231,6 @@ header {
text-align: center;
}
.auto-run-delay-setting {
margin-left: auto !important;
}
.oauth-flow-timeout-setting {
gap: 8px;
min-width: 0;
}
.oauth-flow-timeout-caption {
min-width: 0 !important;
overflow: hidden;
text-overflow: ellipsis;
}
#row-auto-delay-settings .setting-caption {
min-width: auto;
}
.setting-caption-left {
text-align: left;
}
@@ -2720,7 +2705,7 @@ header {
.auto-continue-bar svg { color: var(--orange); flex-shrink: 0; }
.auto-hint { font-size: 13px; color: var(--orange); flex: 1; font-weight: 500; }
.auto-schedule-bar {
.auto-countdown-bar {
margin-top: 8px;
padding: 10px;
background: var(--blue-soft);
@@ -2732,12 +2717,12 @@ header {
gap: 10px;
}
.auto-schedule-bar svg {
.auto-countdown-bar svg {
color: var(--blue);
flex-shrink: 0;
}
.auto-schedule-copy {
.auto-countdown-copy {
display: flex;
flex-direction: column;
gap: 2px;
@@ -2745,19 +2730,19 @@ header {
min-width: 0;
}
.auto-schedule-title {
.auto-countdown-title {
font-size: 13px;
font-weight: 700;
color: var(--blue);
}
.auto-schedule-meta {
.auto-countdown-meta {
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
}
.auto-schedule-actions {
.auto-countdown-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
+68 -54
View File
@@ -121,20 +121,24 @@
<section id="data-section">
<div id="settings-card" class="data-card">
<div class="data-row">
<div class="data-row" id="row-flow-selector">
<span class="data-label">注册</span>
<select id="select-flow" class="data-select" aria-label="Flow 选择">
<option value="openai" selected>Codex / OpenAI</option>
<option value="kiro">Kiro</option>
<option value="grok">Grok</option>
</select>
</div>
<div class="data-row">
<div class="data-row" id="row-source-selector">
<span id="label-source-selector" class="data-label">来源</span>
<select id="select-panel-mode" class="data-select">
<option value="cpa">CPA 面板</option>
<option value="sub2api">SUB2API</option>
<option value="codex2api">Codex2API</option>
</select>
<div class="data-inline">
<select id="select-panel-mode" class="data-select">
<option value="cpa">CPA 面板</option>
<option value="sub2api">SUB2API</option>
<option value="codex2api">Codex2API</option>
</select>
<button id="btn-open-webchat2api-github" class="btn btn-ghost btn-xs data-inline-btn" type="button">GitHub</button>
</div>
</div>
<div id="contribution-mode-panel" class="contribution-mode-panel" hidden>
<div class="contribution-mode-panel-header">
@@ -290,6 +294,42 @@
<span class="data-label">上传状态</span>
<span id="display-kiro-upload-status" class="data-value mono">未开始</span>
</div>
<div class="data-row" id="row-grok-webchat2api-url" style="display:none;">
<span class="data-label">webchat</span>
<input type="text" id="input-grok-webchat2api-url" class="data-input"
placeholder="请输入 webchat2api 地址,例如 http://localhost:83" />
</div>
<div class="data-row" id="row-grok-webchat2api-key" style="display:none;">
<span class="data-label">管理密钥</span>
<div class="input-with-icon">
<input type="password" id="input-grok-webchat2api-key" class="data-input data-input-with-icon"
placeholder="请输入 webchat2api 管理密钥" />
<button id="btn-toggle-grok-webchat2api-key" class="input-icon-btn" type="button"
data-password-toggle="input-grok-webchat2api-key" data-show-label="显示 webchat2api 管理密钥"
data-hide-label="隐藏 webchat2api 管理密钥" aria-label="显示 webchat2api 管理密钥"
title="显示 webchat2api 管理密钥"></button>
</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-webchat2api-upload-status" style="display:none;">
<span class="data-label">上传状态</span>
<span id="display-grok-webchat2api-upload-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-clear-grok-sso" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button>
</div>
</div>
<div class="data-row module-divider-start" id="row-custom-password">
<span class="data-label">账户密码</span>
<div class="input-with-icon">
@@ -657,26 +697,7 @@
</div>
</div>
</div>
<div class="data-row module-divider-start" id="row-auto-delay-settings">
<span class="data-label">启动前</span>
<div class="data-inline setting-pair auto-delay-setting-pair">
<div class="setting-group setting-group-secondary auto-run-delay-setting">
<span class="setting-caption">延迟</span>
<label class="toggle-switch" for="input-auto-delay-enabled">
<input type="checkbox" id="input-auto-delay-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
<div class="setting-controls">
<input type="number" id="input-auto-delay-minutes" class="data-input auto-delay-input" value="30" min="1"
max="1440" step="1" title="启动前倒计时分钟数" />
<span class="data-unit">分钟</span>
</div>
</div>
</div>
</div>
<div class="data-row">
<div class="data-row module-divider-start" id="row-shared-auto-run">
<span class="data-label">自动重试</span>
<div class="data-inline setting-pair">
<div class="setting-group setting-group-primary">
@@ -697,27 +718,13 @@
</div>
</div>
</div>
<div class="data-row" id="row-oauth-flow-timeout">
<span class="data-label">授权总超时</span>
<div class="data-inline setting-pair">
<div class="setting-group setting-group-primary oauth-flow-timeout-setting">
<label class="toggle-switch" for="input-oauth-flow-timeout-enabled"
title="关闭后不再用 5 分钟总预算重启授权链,但页面、点击、回调等本地等待超时仍会生效">
<input type="checkbox" id="input-oauth-flow-timeout-enabled" checked />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>启用</span>
</label>
<span class="setting-caption oauth-flow-timeout-caption">关闭后只取消 Step 7 后链总预算</span>
</div>
<div class="setting-group setting-group-secondary">
<span class="setting-caption">线程间隔</span>
<div class="setting-controls">
<input type="number" id="input-auto-skip-failures-thread-interval-minutes"
class="data-input auto-delay-input" value="0" min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
<span class="data-unit">分钟</span>
</div>
<div class="data-row" id="row-auto-run-thread-interval">
<span class="data-label">线程间隔</span>
<div class="data-inline">
<div class="setting-controls">
<input type="number" id="input-auto-skip-failures-thread-interval-minutes"
class="data-input auto-delay-input" value="0" min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
<span class="data-unit">分钟</span>
</div>
</div>
</div>
@@ -745,10 +752,15 @@
<span class="data-label">OAuth</span>
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
</div>
<div class="data-row">
<div class="data-row" id="row-oauth-callback">
<span class="data-label">回调</span>
<div class="data-inline data-value-actions">
<span id="display-localhost-url" class="data-value data-value-fill mono truncate">等待中...</span>
</div>
</div>
<div class="data-row module-divider-start" id="row-settings-actions">
<span class="data-label">设置</span>
<div class="data-inline data-value-actions">
<button id="btn-save-settings" class="btn btn-outline btn-sm data-inline-btn" type="button">保存</button>
</div>
</div>
@@ -1761,16 +1773,16 @@
<span class="auto-hint">先自动获取邮箱,或手动粘贴邮箱后再继续</span>
<button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button>
</div>
<div id="auto-schedule-bar" class="auto-schedule-bar" style="display:none;">
<div id="auto-countdown-bar" class="auto-countdown-bar" style="display:none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<div class="auto-schedule-copy">
<span id="auto-schedule-title" class="auto-schedule-title">已计划自动运行</span>
<span id="auto-schedule-meta" class="auto-schedule-meta">等待倒计时开始...</span>
<div class="auto-countdown-copy">
<span id="auto-countdown-title" class="auto-countdown-title">等待继续</span>
<span id="auto-countdown-meta" class="auto-countdown-meta">等待倒计时开始...</span>
</div>
<div class="auto-schedule-actions">
<div class="auto-countdown-actions">
<button id="btn-auto-run-now" class="btn btn-primary btn-sm" type="button">立即开始</button>
<button id="btn-auto-cancel-schedule" class="btn btn-outline btn-sm" type="button">取消</button>
</div>
@@ -1881,6 +1893,8 @@
<script src="../flows/openai/workflow.js"></script>
<script src="../flows/kiro/index.js"></script>
<script src="../flows/kiro/workflow.js"></script>
<script src="../flows/grok/index.js"></script>
<script src="../flows/grok/workflow.js"></script>
<script src="../flows/index.js"></script>
<script src="../core/flow-kernel/flow-registry.js"></script>
<script src="../shared/contribution-registry.js"></script>
+332 -173
View File
File diff suppressed because it is too large Load Diff
+16 -32
View File
@@ -134,8 +134,6 @@ test('auto-run controller skips add-phone failures to the next round instead of
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
@@ -185,7 +183,7 @@ test('auto-run controller skips add-phone failures to the next round instead of
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -201,7 +199,7 @@ test('auto-run controller skips add-phone failures to the next round instead of
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -297,8 +295,6 @@ test('auto-run controller treats phone-number supply exhaustion as round-fatal a
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
@@ -348,7 +344,7 @@ test('auto-run controller treats phone-number supply exhaustion as round-fatal a
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -364,7 +360,7 @@ test('auto-run controller treats phone-number supply exhaustion as round-fatal a
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -460,8 +456,6 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
@@ -511,7 +505,7 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -527,7 +521,7 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -621,8 +615,6 @@ test('auto-run controller keeps same-round retrying for step9 local replacement
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
@@ -672,7 +664,7 @@ test('auto-run controller keeps same-round retrying for step9 local replacement
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -688,7 +680,7 @@ test('auto-run controller keeps same-round retrying for step9 local replacement
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -779,8 +771,6 @@ test('auto-run controller skips user_already_exists failures to the next round i
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
@@ -830,7 +820,7 @@ test('auto-run controller skips user_already_exists failures to the next round i
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -846,7 +836,7 @@ test('auto-run controller skips user_already_exists failures to the next round i
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -943,8 +933,6 @@ test('auto-run controller skips step 4 repeated 405 recovery failures to the nex
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
@@ -994,7 +982,7 @@ test('auto-run controller skips step 4 repeated 405 recovery failures to the nex
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -1010,7 +998,7 @@ test('auto-run controller skips step 4 repeated 405 recovery failures to the nex
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -1108,8 +1096,6 @@ test('auto-run controller keeps retrying the same custom mail provider pool emai
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: 'custom',
customMailProviderPool: ['first@example.com'],
@@ -1160,7 +1146,7 @@ test('auto-run controller keeps retrying the same custom mail provider pool emai
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -1176,7 +1162,7 @@ test('auto-run controller keeps retrying the same custom mail provider pool emai
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -1274,8 +1260,6 @@ test('auto-run controller retries 5sim rate limit failures instead of treating c
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
@@ -1325,7 +1309,7 @@ test('auto-run controller retries 5sim rate limit failures instead of treating c
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -1341,7 +1325,7 @@ test('auto-run controller retries 5sim rate limit failures instead of treating c
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -130,8 +130,6 @@ let currentState = {
customPassword: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
signupMethod: 'phone',
resolvedSignupMethod: 'phone',
@@ -204,8 +202,6 @@ async function resetState() {
customPassword: prev.customPassword,
autoRunSkipFailures: prev.autoRunSkipFailures,
autoRunFallbackThreadIntervalMinutes: prev.autoRunFallbackThreadIntervalMinutes,
autoRunDelayEnabled: prev.autoRunDelayEnabled,
autoRunDelayMinutes: prev.autoRunDelayMinutes,
autoStepDelaySeconds: prev.autoStepDelaySeconds,
signupMethod: prev.signupMethod,
resolvedSignupMethod: null,
+2 -4
View File
@@ -104,8 +104,6 @@ test('auto-run controller verifies hotmail mailbox before each fresh attempt sta
customPassword: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: 'hotmail-api',
emailGenerator: 'duck',
@@ -149,7 +147,7 @@ test('auto-run controller verifies hotmail mailbox before each fresh attempt sta
broadcastAutoRunStatus: async (phase, payload = {}) => {
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -169,7 +167,7 @@ test('auto-run controller verifies hotmail mailbox before each fresh attempt sta
events.preflightCalls.push({ ...payload });
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
+4 -10
View File
@@ -36,8 +36,6 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
phoneSignupReloginAfterBindEmailEnabled: false,
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
signupMethod: 'email',
stepExecutionRangeByFlow: {
@@ -94,7 +92,7 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
currentState = {
...currentState,
...extraState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? currentState.autoRunCurrentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? currentState.autoRunTotalRuns ?? 1,
@@ -124,7 +122,7 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
},
ensureHotmailMailboxReadyForAutoRunRound: async () => {},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -179,8 +177,6 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
phoneSignupReloginAfterBindEmailEnabled: false,
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
signupMethod: 'email',
stepExecutionRangeByFlow: {
@@ -445,8 +441,6 @@ test('auto-run controller stops immediately on kiro proxy failures even when ski
flowId: 'kiro',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
nodeStatuses: {
'kiro-open-register-page': 'pending',
@@ -496,7 +490,7 @@ test('auto-run controller stops immediately on kiro proxy failures even when ski
events.phases.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? currentState.autoRunCurrentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? currentState.autoRunTotalRuns ?? 1,
@@ -514,7 +508,7 @@ test('auto-run controller stops immediately on kiro proxy failures even when ski
},
ensureHotmailMailboxReadyForAutoRunRound: async () => {},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
+5 -112
View File
@@ -63,7 +63,6 @@ const helperBundle = [
test('launchAutoRunTimerPlan ignores stale timer plans after stop invalidates the session', async () => {
const api = new Function(`
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
@@ -76,14 +75,15 @@ let autoRunAttemptRun = 0;
let autoRunSessionId = 0;
const state = {
autoRunDelayEnabled: false,
autoRunTimerPlan: {
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START,
kind: AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
fireAt: Date.now() + 60_000,
totalRuns: 2,
autoRunSkipFailures: false,
autoRunSessionId: 42,
countdownTitle: '已计划自动运行',
currentRun: 1,
attemptRun: 1,
countdownTitle: '等待继续',
countdownNote: '等待启动',
},
};
@@ -106,7 +106,6 @@ async function clearAutoRunTimerAlarm() {
async function broadcastAutoRunStatus() {}
async function addLog() {}
async function setAutoRunDelayEnabledState() {}
function serializeAutoRunRoundSummaries(totalRuns, summaries = []) {
return Array.isArray(summaries) ? summaries : [];
}
@@ -137,116 +136,10 @@ return {
const started = await api.launchAutoRunTimerPlan('alarm');
const snapshot = api.snapshot();
assert.equal(started, false);
assert.equal(snapshot.startCalls, 0, 'stale timer plan should not restart auto-run');
assert.equal(snapshot.clearStopCalls, 0, 'stale timer plan should not clear the stop flag for a cancelled run');
assert.equal(snapshot.clearAlarmCalls, 0, 'stale timer plan should not clear a potentially newer alarm');
assert.equal(snapshot.autoRunCurrentRun, 0);
assert.equal(snapshot.autoRunTotalRuns, 1);
assert.equal(snapshot.autoRunAttemptRun, 0);
});
test('launchAutoRunTimerPlan cancels an invalid scheduled start before restarting auto-run', async () => {
const api = new Function(`
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
let autoRunTimerLaunching = false;
let autoRunActive = false;
let autoRunCurrentRun = 0;
let autoRunTotalRuns = 1;
let autoRunAttemptRun = 0;
let autoRunSessionId = 0;
const state = {
activeFlowId: 'site-a',
panelMode: 'cpa',
signupMethod: 'phone',
autoRunDelayEnabled: false,
autoRunTimerPlan: {
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START,
fireAt: Date.now() + 60_000,
totalRuns: 2,
autoRunSkipFailures: false,
autoRunSessionId: 0,
countdownTitle: '已计划自动运行',
countdownNote: '等待启动',
},
};
let startCalls = 0;
let clearStopCalls = 0;
let clearAlarmCalls = 0;
const broadcasts = [];
const logs = [];
async function getState() {
return { ...state };
}
function getPendingAutoRunTimerPlan() {
return state.autoRunTimerPlan;
}
async function clearAutoRunTimerAlarm() {
clearAlarmCalls += 1;
}
async function broadcastAutoRunStatus(phase, statusPayload, statePayload) {
broadcasts.push({ phase, statusPayload, statePayload });
}
async function addLog(message, level) {
logs.push({ message, level });
}
async function setAutoRunDelayEnabledState() {}
function serializeAutoRunRoundSummaries(totalRuns, summaries = []) {
return Array.isArray(summaries) ? summaries : [];
}
function clearStopRequest() {
clearStopCalls += 1;
}
function startAutoRunLoop() {
startCalls += 1;
}
function validateAutoRunStartState() {
return {
ok: false,
errors: [{ message: '当前 flow 不支持手机号注册。' }],
};
}
${helperBundle}
return {
launchAutoRunTimerPlan,
snapshot() {
return {
startCalls,
clearStopCalls,
clearAlarmCalls,
broadcasts,
logs,
autoRunCurrentRun,
autoRunTotalRuns,
autoRunAttemptRun,
};
},
};
`)();
const started = await api.launchAutoRunTimerPlan('alarm');
const snapshot = api.snapshot();
assert.equal(started, false);
assert.equal(snapshot.startCalls, 0);
assert.equal(snapshot.clearStopCalls, 0);
assert.equal(snapshot.clearAlarmCalls, 1);
assert.equal(snapshot.broadcasts.length, 1);
assert.equal(snapshot.broadcasts[0].phase, 'idle');
assert.match(snapshot.logs[0].message, /自动运行计划已取消:当前 flow 不支持手机号注册。/);
assert.equal(snapshot.logs[0].level, 'error');
assert.equal(snapshot.clearAlarmCalls, 0);
assert.equal(snapshot.autoRunCurrentRun, 0);
assert.equal(snapshot.autoRunTotalRuns, 1);
assert.equal(snapshot.autoRunAttemptRun, 0);
@@ -196,7 +196,6 @@ const PERSISTED_SETTING_DEFAULTS = {
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value == null || value === '' ? null : Number(value); }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
function normalizeMail2925Mode(value) { return String(value || '').trim().toLowerCase() === 'receive' ? 'receive' : 'provide'; }
+51 -6
View File
@@ -48,6 +48,33 @@ function extractFunction(name) {
return source.slice(start, end);
}
const OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY = `
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const self = {
MultiPageFlowRegistry: {
normalizeFlowId(value, fallback = 'openai') {
const normalized = String(value || '').trim().toLowerCase();
return ['openai', 'kiro', 'grok'].includes(normalized) ? normalized : fallback;
},
normalizeTargetId(flowId, value, fallback = 'cpa') {
const normalized = String(value || '').trim().toLowerCase();
if (flowId === 'openai' && ['cpa', 'sub2api', 'codex2api'].includes(normalized)) return normalized;
if (flowId === 'kiro') return 'kiro-rs';
if (flowId === 'grok') return 'webchat2api';
return fallback;
},
getDefaultTargetId(flowId) {
return flowId === 'openai' ? 'cpa' : '';
},
getTargetCapabilities(flowId, targetId) {
return flowId === 'openai' && targetId === 'cpa'
? { usesOauthTimeoutBudget: true }
: { usesOauthTimeoutBudget: false };
},
},
};
`;
test('background auth chain set does not include Plus session import nodes', () => {
const authChainStart = source.indexOf('const AUTH_CHAIN_NODE_IDS = new Set([');
const authChainEnd = source.indexOf(']);', authChainStart);
@@ -431,6 +458,9 @@ test('oauth timeout budget ignores stale deadlines from an old oauth url', async
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY}
${extractFunction('resolveOAuthTimeoutBudgetScope')}
${extractFunction('shouldUseOAuthTimeoutBudget')}
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
@@ -444,6 +474,8 @@ return {
step: 8,
actionLabel: '登录验证码流程',
state: {
activeFlowId: 'openai',
targetId: 'cpa',
oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() + 1200,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/old',
@@ -457,7 +489,10 @@ test('oauth timeout budget clamps local timeout when enabled by default', async
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY}
${extractFunction('buildOAuthFlowTimeoutError')}
${extractFunction('resolveOAuthTimeoutBudgetScope')}
${extractFunction('shouldUseOAuthTimeoutBudget')}
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
@@ -471,6 +506,8 @@ return {
step: 8,
actionLabel: '登录验证码流程',
state: {
activeFlowId: 'openai',
targetId: 'cpa',
oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() + 1200,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
@@ -481,11 +518,14 @@ return {
assert(timeoutMs >= 1000);
});
test('oauth timeout budget disabled mode ignores active deadlines', async () => {
test('oauth timeout budget ignores active deadlines outside openai cpa target', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY}
${extractFunction('buildOAuthFlowTimeoutError')}
${extractFunction('resolveOAuthTimeoutBudgetScope')}
${extractFunction('shouldUseOAuthTimeoutBudget')}
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
@@ -499,7 +539,8 @@ return {
step: 9,
actionLabel: 'OAuth localhost 回调',
state: {
oauthFlowTimeoutEnabled: false,
activeFlowId: 'openai',
targetId: 'sub2api',
oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() - 1000,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
@@ -509,16 +550,18 @@ return {
assert.equal(timeoutMs, 15000);
});
test('startOAuthFlowTimeoutWindow clears stale deadline when timeout is disabled', async () => {
test('startOAuthFlowTimeoutWindow clears stale deadline outside openai cpa target', async () => {
const events = {
stateUpdates: [],
logs: [],
};
const api = new Function('events', `
const api = new Function('events', `
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY}
async function getState() {
return {
oauthFlowTimeoutEnabled: false,
activeFlowId: 'openai',
targetId: 'sub2api',
oauthFlowDeadlineAt: Date.now() - 1000,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/old',
};
@@ -529,6 +572,8 @@ async function setState(update) {
async function addLog(message, level) {
events.logs.push({ message, level });
}
${extractFunction('resolveOAuthTimeoutBudgetScope')}
${extractFunction('shouldUseOAuthTimeoutBudget')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('startOAuthFlowTimeoutWindow')}
return {
@@ -546,7 +591,7 @@ return {
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
}]);
assert.match(events.logs[0].message, /授权后链总超时已关闭/);
assert.deepStrictEqual(events.logs, []);
});
test('oauth localhost timeout recovery resumes from bound-email relogin tail when present', async () => {
@@ -82,7 +82,6 @@ function normalizeSignupMethod(value = '') { return String(value || '').trim().t
function resolveSignupMethod(state = {}) { return normalizeSignupMethod(state?.signupMethod); }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
@@ -157,12 +156,11 @@ return {
});
});
test('oauth flow timeout setting is persisted and normalized as boolean', () => {
test('cloudflare temp email settings payload ignores removed UI-only flags', () => {
const api = new Function(`
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa',
oauthFlowTimeoutEnabled: true,
autoStepDelaySeconds: null,
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
mailProvider: '163',
@@ -180,7 +178,6 @@ function normalizeSignupMethod(value = '') { return String(value || '').trim().t
function resolveSignupMethod(state = {}) { return normalizeSignupMethod(state?.signupMethod); }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
@@ -224,15 +221,10 @@ return {
};
`)();
assert.equal(api.normalizePersistentSettingValue('oauthFlowTimeoutEnabled', 0), false);
assert.equal(api.normalizePersistentSettingValue('oauthFlowTimeoutEnabled', 1), true);
assert.deepEqual(api.buildPersistentSettingsPayload({
oauthFlowTimeoutEnabled: false,
}), {
oauthFlowTimeoutEnabled: false,
});
removedUiOnlyFlag: false,
}), {});
const defaults = api.buildPersistentSettingsPayload({}, { fillDefaults: true });
assert.equal(defaults.oauthFlowTimeoutEnabled, true);
assert.equal(Object.prototype.hasOwnProperty.call(defaults, 'removedUiOnlyFlag'), false);
});
+1 -18
View File
@@ -372,7 +372,7 @@ test('message router re-syncs contribution mode before AUTO_RUN when sidepanel p
]);
});
test('message router blocks AUTO_RUN and SCHEDULE_AUTO_RUN when shared auto-run validation fails', async () => {
test('message router blocks AUTO_RUN when shared auto-run validation fails', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
@@ -388,10 +388,6 @@ test('message router blocks AUTO_RUN and SCHEDULE_AUTO_RUN when shared auto-run
stepStatuses: {},
}),
normalizeRunCount: (value) => Number(value) || 1,
scheduleAutoRun: async () => {
calls.push({ type: 'scheduleAutoRun' });
return { ok: true };
},
setState: async (updates) => {
calls.push({ type: 'setState', updates });
},
@@ -415,19 +411,6 @@ test('message router blocks AUTO_RUN and SCHEDULE_AUTO_RUN when shared auto-run
}),
/当前 flow 不支持手机号注册。/
);
await assert.rejects(
() => router.handleMessage({
type: 'SCHEDULE_AUTO_RUN',
payload: {
totalRuns: 2,
delayMinutes: 5,
autoRunSkipFailures: false,
},
}),
/当前 flow 不支持手机号注册。/
);
assert.deepStrictEqual(calls, []);
});
@@ -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,274 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadPublisherApi() {
const stateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8');
const publisherSource = fs.readFileSync('flows/grok/background/publisher-webchat2api.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${publisherSource}; return self;`)(globalScope);
return globalScope.MultiPageBackgroundGrokPublisherWebchat2Api;
}
function createJsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
statusText: status >= 200 && status < 300 ? 'OK' : 'Error',
text: async () => JSON.stringify(payload),
};
}
function getGrokRuntime(state = {}) {
return state?.runtimeState?.flowState?.grok || {};
}
test('grok webchat2api publisher exposes helpers and normalizes to origin inject endpoint', () => {
const api = loadPublisherApi();
assert.equal(typeof api?.createGrokWebchat2ApiPublisher, 'function');
assert.equal(
api.buildWebchat2ApiInjectUrl('https://remote.example.com/admin/deep/path'),
'https://remote.example.com/api/remote-account/inject'
);
assert.equal(
api.buildWebchat2ApiInjectUrl('remote.example.com/admin'),
'http://remote.example.com/api/remote-account/inject'
);
});
test('grok webchat2api publisher requires URL, admin key, and SSO cookie', async () => {
const api = loadPublisherApi();
assert.throws(
() => api.buildWebchat2ApiInjectUrl(''),
/缺少 webchat2api 地址/
);
assert.throws(
() => api.buildGrokSsoInjectPayload(''),
/缺少 Grok SSO Cookie/
);
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api('http://remote.example.com', '', 'sso-cookie', async () => createJsonResponse({})),
/缺少 webchat2api 管理密钥/
);
});
test('grok webchat2api publisher posts Grok SSO payload with bearer admin key', async () => {
const api = loadPublisherApi();
const requests = [];
const result = await api.uploadGrokSsoToWebchat2Api(
'https://remote.example.com/admin/deep/path',
' admin-secret ',
'sso-cookie-001',
async (url, options = {}) => {
requests.push({
url,
method: options.method,
authorization: options.headers?.Authorization,
contentType: options.headers?.['Content-Type'],
body: JSON.parse(options.body),
});
return createJsonResponse({ code: 0, data: { total: 1 }, message: 'ok' });
}
);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://remote.example.com/api/remote-account/inject');
assert.equal(requests[0].method, 'POST');
assert.equal(requests[0].authorization, 'Bearer admin-secret');
assert.equal(requests[0].contentType, 'application/json');
assert.deepEqual(requests[0].body, {
accounts: [{
token: 'sso-cookie-001',
provider: 'grok',
type: 'sso',
}],
strategy: 'merge',
source_id: 'flowpilot-grok-sso',
source_name: 'FlowPilot Grok SSO',
provider: 'grok',
});
assert.equal(result.endpointUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(result.message, 'ok');
});
test('grok webchat2api publisher accepts code-zero envelopes and rejects HTTP/API errors', async () => {
const api = loadPublisherApi();
const success = await api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ code: 0, data: { added: 1 } })
);
assert.equal(success.message, '上传成功');
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ error: 'invalid admin key' }, 403)
),
/webchat2api SSO 上传失败:invalid admin key/
);
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ code: 4001, message: 'bad token' })
),
/webchat2api SSO 上传失败:bad token/
);
});
test('grok webchat2api executor reads latest state and writes upload runtime without leaking secrets to logs', async () => {
const api = loadPublisherApi();
const requests = [];
const logs = [];
const completed = [];
let liveState = {
grokWebchat2ApiUrl: '',
grokWebchat2ApiAdminKey: '',
grokSsoCookie: '',
settingsState: {
flows: {
grok: {
targets: {
webchat2api: {
baseUrl: 'https://remote.example.com/admin',
apiKey: 'live-admin-key',
},
},
},
},
},
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'live-sso-cookie',
},
},
},
},
};
const publisher = api.createGrokWebchat2ApiPublisher({
addLog: async (message, level) => {
logs.push({ message, level });
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
fetchImpl: async (url, options = {}) => {
requests.push({
url,
authorization: options.headers?.Authorization,
body: JSON.parse(options.body),
});
return createJsonResponse({ code: 0, message: 'uploaded' });
},
getState: async () => ({ ...liveState }),
setState: async (updates = {}) => {
liveState = {
...liveState,
...updates,
runtimeState: {
...(liveState.runtimeState || {}),
...(updates.runtimeState || {}),
flowState: {
...(liveState.runtimeState?.flowState || {}),
...(updates.runtimeState?.flowState || {}),
},
},
};
},
});
await publisher.executeGrokUploadSsoToWebchat2Api({
nodeId: 'grok-upload-sso-to-webchat2api',
grokWebchat2ApiAdminKey: 'stale-key',
grokSsoCookie: 'stale-sso-cookie',
});
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://remote.example.com/api/remote-account/inject');
assert.equal(requests[0].authorization, 'Bearer live-admin-key');
assert.equal(requests[0].body.accounts[0].token, 'live-sso-cookie');
assert.equal(completed.length, 1);
assert.equal(completed[0].nodeId, 'grok-upload-sso-to-webchat2api');
assert.equal(getGrokRuntime(completed[0].payload).upload.status, 'uploaded');
assert.equal(getGrokRuntime(completed[0].payload).upload.message, 'uploaded');
assert.equal(getGrokRuntime(completed[0].payload).upload.targetUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(typeof getGrokRuntime(completed[0].payload).upload.uploadedAt, 'number');
assert.equal(logs.some(({ message }) => message.includes('live-sso-cookie') || message.includes('live-admin-key')), false);
});
test('grok webchat2api executor persists failure state without completing or leaking secrets', async () => {
const api = loadPublisherApi();
const logs = [];
const completed = [];
let liveState = {
settingsState: {
flows: {
grok: {
targets: {
webchat2api: {
baseUrl: 'https://remote.example.com/admin',
apiKey: 'secret-admin-key',
},
},
},
},
},
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'secret-sso-cookie',
},
},
},
},
};
const publisher = api.createGrokWebchat2ApiPublisher({
addLog: async (message, level) => {
logs.push({ message, level });
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
fetchImpl: async () => createJsonResponse({ error: 'invalid admin key' }, 403),
getState: async () => ({ ...liveState }),
setState: async (updates = {}) => {
liveState = {
...liveState,
...updates,
runtimeState: {
...(liveState.runtimeState || {}),
...(updates.runtimeState || {}),
flowState: {
...(liveState.runtimeState?.flowState || {}),
...(updates.runtimeState?.flowState || {}),
},
},
};
},
});
await assert.rejects(
() => publisher.executeGrokUploadSsoToWebchat2Api({ nodeId: 'grok-upload-sso-to-webchat2api' }),
/webchat2api SSO 上传失败:invalid admin key/
);
assert.equal(completed.length, 0);
assert.equal(getGrokRuntime(liveState).upload.status, 'error');
assert.equal(getGrokRuntime(liveState).upload.uploadedAt, 0);
assert.equal(getGrokRuntime(liveState).upload.message, 'webchat2api SSO 上传失败:invalid admin key');
assert.equal(getGrokRuntime(liveState).upload.targetUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(logs.some(({ message }) => message.includes('secret-sso-cookie') || message.includes('secret-admin-key')), false);
});
+212
View File
@@ -0,0 +1,212 @@
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,
},
upload: {
status: 'uploaded',
uploadedAt: 4000,
message: 'ok',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
},
},
},
});
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.grokWebchat2ApiUploadStatus, 'uploaded');
assert.equal(view.grokWebchat2ApiUploadedAt, 4000);
assert.equal(view.grokWebchat2ApiUploadMessage, 'ok');
assert.equal(view.grokWebchat2ApiTargetUrl, 'https://remote.example.com/api/remote-account/inject');
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.flowState.grok.upload.status, 'uploaded');
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,
grokWebchat2ApiUploadStatus: 'uploaded',
grokWebchat2ApiUploadedAt: 789,
grokWebchat2ApiUploadMessage: '上传成功',
grokWebchat2ApiTargetUrl: 'http://remote.example.com/api/remote-account/inject',
});
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.grokWebchat2ApiUploadStatus, 'uploaded');
assert.equal(patch.grokWebchat2ApiUploadedAt, 789);
assert.equal(patch.grokWebchat2ApiUploadMessage, '上传成功');
assert.equal(patch.grokWebchat2ApiTargetUrl, 'http://remote.example.com/api/remote-account/inject');
assert.equal(patch.runtimeState.flowState.grok.register.email, 'grok@example.com');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-z');
assert.equal(patch.runtimeState.flowState.grok.upload.status, 'uploaded');
});
test('grok state keeps canonical runtime values when flat compatibility fields are empty', () => {
const api = loadGrokStateApi();
const runtimeState = api.ensureRuntimeState({
grokSsoCookie: '',
grokSsoCookies: [],
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'canonical-cookie',
cookies: ['canonical-cookie'],
extractedAt: 1234,
},
},
},
},
});
assert.equal(runtimeState.sso.currentCookie, 'canonical-cookie');
assert.deepEqual(runtimeState.sso.cookies, ['canonical-cookie']);
assert.equal(runtimeState.sso.extractedAt, 1234);
});
test('grok fresh keep-state reset clears registration, SSO, and upload 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,
},
upload: {
status: 'uploaded',
uploadedAt: 3000,
message: 'ok',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
},
},
},
});
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, '');
assert.deepEqual(patch.grokSsoCookies, []);
assert.equal(patch.grokSsoExtractedAt, 0);
assert.equal(patch.grokWebchat2ApiUploadStatus, '');
assert.equal(patch.grokWebchat2ApiUploadedAt, 0);
assert.equal(patch.grokWebchat2ApiUploadMessage, '');
assert.equal(patch.grokWebchat2ApiTargetUrl, '');
assert.equal(patch.runtimeState.flowState.grok.register.email, '');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, '');
assert.equal(patch.runtimeState.flowState.grok.upload.status, '');
});
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);
});
-3
View File
@@ -99,9 +99,6 @@ function normalizeMailProvider(value = '') {
function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
return Math.max(0, Math.floor(Number(value) || 0));
}
function normalizeAutoRunDelayMinutes(value) {
return Math.max(1, Math.floor(Number(value) || 30));
}
function normalizeAutoStepDelaySeconds(value, fallback = null) {
const numeric = Number(value);
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : fallback;
@@ -91,6 +91,18 @@ test('kiro desktop authorize runner uses a shared 3-minute page-load timeout bud
assert.match(source, /finalizeDesktopAuthorizeCallback/);
});
test('kiro desktop authorize runner delegates OTP mail polling to the shared flow mail service', () => {
const source = fs.readFileSync('flows/kiro/background/desktop-authorize-runner.js', 'utf8');
assert.match(source, /pollFlowVerificationCode/);
assert.doesNotMatch(source, /buildDesktopOtpPollPayload/);
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
});
test('background wires the Kiro register injector into desktop authorization runner', () => {
const source = fs.readFileSync('background.js', 'utf8');
const start = source.indexOf('const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({');
@@ -37,6 +37,18 @@ test('kiro register runner uses a shared 3-minute page-load timeout budget', ()
assert.match(source, /onRetryableError: buildKiroRetryRecovery\(tabId, \{\s*\.\.\.options,\s*timeoutBudget,/);
});
test('kiro register runner delegates verification mail polling to the shared flow mail service', () => {
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
assert.match(source, /pollFlowVerificationCode/);
assert.doesNotMatch(source, /buildKiroVerificationPollPayload/);
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
});
test('kiro register consent step treats Kiro Web signed-in page as completion', () => {
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
assert.match(source, /readKiroRegisterPageState\(tabId, \{/);
@@ -231,8 +243,8 @@ test('kiro verification polling uses the registration email field instead of pag
getState: async () => currentState,
getTabId: async () => 103,
isTabAlive: async () => true,
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
pollPayloads.push(payload);
pollFlowVerificationCode: async (options) => {
pollPayloads.push(options);
return { code: '123456', emailTimestamp: 2000, mailId: 'mail-1' };
},
sendToContentScriptResilient: async (_sourceId, message) => {
@@ -266,8 +278,11 @@ test('kiro verification polling uses the registration email field instead of pag
});
assert.equal(pollPayloads.length, 1);
assert.equal(pollPayloads[0].targetEmail, 'skater-twine-carve@duck.com');
assert.deepEqual(pollPayloads[0].targetEmailHints, ['skater-twine-carve@duck.com']);
assert.equal(pollPayloads[0].flowId, 'kiro');
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
assert.equal(pollPayloads[0].filterAfterTimestamp, 1000);
assert.equal(pollPayloads[0].state.email, 'skater-twine-carve@duck.com');
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'skater-twine-carve@duck.com');
assert.equal(sentMessages.some((message) => (
message.type === 'EXECUTE_NODE'
&& message.nodeId === 'kiro-submit-verification-code'
@@ -315,8 +330,8 @@ test('kiro verification step can adopt the active AWS verify-otp page without st
getState: async () => currentState,
getTabId: async () => null,
isTabAlive: async () => false,
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
pollPayloads.push(payload);
pollFlowVerificationCode: async (options) => {
pollPayloads.push(options);
return { code: '248680', emailTimestamp: 2000, mailId: 'mail-active' };
},
registerTab: async (source, tabId) => {
@@ -355,7 +370,10 @@ test('kiro verification step can adopt the active AWS verify-otp page without st
assert.deepEqual(registeredTabs, [{ source: 'kiro-register-page', tabId: 301 }]);
assert.equal(getKiroRuntime(statePatches[0]).session?.registerTabId, 301);
assert.equal(pollPayloads[0].targetEmail, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
assert.equal(pollPayloads[0].flowId, 'kiro');
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
assert.equal(pollPayloads[0].state.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
assert.equal(sentMessages.some((message) => (
message.type === 'EXECUTE_NODE'
&& message.nodeId === 'kiro-submit-verification-code'
@@ -419,8 +437,8 @@ test('kiro verification step reinjects the register driver when only the generic
'content/utils.js',
'flows/kiro/content/register-page.js',
],
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
pollPayloads.push(payload);
pollFlowVerificationCode: async (options) => {
pollPayloads.push(options);
return { code: '248680', emailTimestamp: 2000, mailId: 'mail-reinject' };
},
sendToContentScriptResilient: async (_sourceId, message) => {
@@ -464,7 +482,10 @@ test('kiro verification step reinjects the register driver when only the generic
'content/utils.js',
'flows/kiro/content/register-page.js',
]);
assert.equal(pollPayloads[0].targetEmail, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
assert.equal(pollPayloads[0].flowId, 'kiro');
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
assert.equal(pollPayloads[0].state.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
assert.equal(getKiroRuntime(completedPayload).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
});
@@ -2,10 +2,13 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('background imports mail rule registry and OpenAI mail rules modules', () => {
test('background imports mail rule registry and flow mail rules modules', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /background\/mail-rule-registry\.js/);
assert.match(source, /flows\/openai\/mail-rules\.js/);
assert.match(source, /flows\/kiro\/mail-rules\.js/);
assert.match(source, /flows\/grok\/mail-rules\.js/);
assert.match(source, /background\/flow-mail-polling\.js/);
});
test('mail rule registry exposes canonical OpenAI verification poll payloads', () => {
@@ -133,3 +136,185 @@ test('mail rule registry rejects unknown active flow ids instead of silently usi
/未找到 flow=site-a 的邮件轮询规则构造器/
);
});
test('mail rule registry exposes Kiro AWS verification poll payloads by node', () => {
const stateSource = fs.readFileSync('flows/kiro/background/state.js', 'utf8');
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
const kiroSource = fs.readFileSync('flows/kiro/mail-rules.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${registrySource}; ${kiroSource}; return self;`)(globalScope);
const kiroMailRules = globalScope.MultiPageKiroMailRules.createKiroMailRules({
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
});
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
defaultFlowId: 'openai',
flowBuilders: {
kiro: kiroMailRules,
},
});
const baseState = {
activeFlowId: 'kiro',
mailProvider: '2925',
mail2925Mode: 'receive',
runtimeState: {
flowState: {
kiro: {
register: {
email: 'kiro-user@example.com',
},
},
},
},
};
assert.deepEqual(
registry.buildVerificationPollPayloadForNode(
'kiro-submit-verification-code',
baseState,
{ filterAfterTimestamp: 12345 }
),
{
flowId: 'kiro',
ruleId: 'kiro-submit-verification-code',
nodeId: 'kiro-submit-verification-code',
step: 4,
artifactType: 'code',
codePatterns: [
{
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
},
{
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
},
{
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
},
],
filterAfterTimestamp: 12345,
requiredKeywords: ['verification', '验证码', 'code', 'aws'],
senderFilters: [
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
],
subjectFilters: ['aws builder id', 'verification', '验证码', 'code', 'aws'],
targetEmail: 'kiro-user@example.com',
targetEmailHints: ['kiro-user@example.com'],
mail2925MatchTargetEmail: true,
maxAttempts: 17,
intervalMs: 16000,
}
);
const desktopPayload = registry.buildVerificationPollPayloadForNode(
'kiro-complete-desktop-authorize',
{
...baseState,
mailProvider: 'luckmail-api',
mail2925Mode: 'provide',
}
);
assert.equal(desktopPayload.ruleId, 'kiro-complete-desktop-authorize');
assert.equal(desktopPayload.step, 8);
assert.equal(desktopPayload.mail2925MatchTargetEmail, false);
assert.equal(desktopPayload.maxAttempts, 3);
assert.equal(desktopPayload.intervalMs, 15000);
});
test('mail rule registry exposes Grok xAI verification poll payloads by node', () => {
const stateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8');
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
const grokSource = fs.readFileSync('flows/grok/mail-rules.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${registrySource}; ${grokSource}; return self;`)(globalScope);
const grokMailRules = globalScope.MultiPageGrokMailRules.createGrokMailRules({
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
});
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
defaultFlowId: 'openai',
flowBuilders: {
grok: grokMailRules,
},
});
assert.deepEqual(
registry.buildVerificationPollPayloadForNode(
'grok-submit-verification-code',
{
activeFlowId: 'grok',
mailProvider: '2925',
mail2925Mode: 'receive',
runtimeState: {
flowState: {
grok: {
register: {
email: 'grok-user@example.com',
},
},
},
},
},
{ filterAfterTimestamp: 12345 }
),
{
flowId: 'grok',
ruleId: 'grok-submit-verification-code',
nodeId: 'grok-submit-verification-code',
step: 3,
artifactType: 'code',
codePatterns: [
{
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
flags: 'gi',
},
{
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[:\\s]*(\\d{6})',
flags: 'gi',
},
{
source: '(?:验证码|代码|确认码)[:\\s为]+(\\d{6})',
flags: 'gi',
},
{
source: '(?<!#)\\b(\\d{6})\\b',
flags: 'g',
},
],
filterAfterTimestamp: 12345,
requiredKeywords: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
senderFilters: ['x.ai', 'xai', 'grok'],
subjectFilters: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
targetEmail: 'grok-user@example.com',
targetEmailHints: ['grok-user@example.com'],
mail2925MatchTargetEmail: true,
maxAttempts: 17,
intervalMs: 16000,
}
);
const luckmailPayload = registry.buildVerificationPollPayloadForNode(
'grok-submit-verification-code',
{
activeFlowId: 'grok',
mailProvider: 'luckmail-api',
mail2925Mode: 'provide',
grokEmail: 'fallback@example.com',
}
);
assert.equal(luckmailPayload.mail2925MatchTargetEmail, false);
assert.equal(luckmailPayload.maxAttempts, 3);
assert.equal(luckmailPayload.intervalMs, 15000);
});
@@ -625,6 +625,47 @@ test('SAVE_SETTING mirrors activeFlowId into flowId when switching to kiro flow'
});
});
test('CLEAR_GROK_SSO_COOKIES delegates canonical runtime cleanup to background', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
let called = false;
const router = api.createMessageRouter({
clearGrokSsoCookies: async () => {
called = true;
return {
ok: true,
state: {
grokSsoCookie: '',
grokSsoCookies: [],
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: '',
cookies: [],
extractedAt: 0,
},
},
},
},
},
};
},
});
const response = await router.handleMessage({
type: 'CLEAR_GROK_SSO_COOKIES',
source: 'sidepanel',
payload: {},
});
assert.equal(called, true);
assert.equal(response.ok, true);
assert.deepStrictEqual(response.state.grokSsoCookies, []);
assert.equal(response.state.runtimeState.flowState.grok.sso.currentCookie, '');
});
test('SAVE_SETTING syncs canonical kiro settingsState back into session state', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
@@ -35,7 +35,6 @@ function createRouterWithFinalNode(options = {}) {
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: () => ({}),
broadcastDataUpdate: () => {},
cancelScheduledAutoRun: async () => {},
checkIcloudSession: async () => {},
clearAutoRunTimerAlarm: async () => {},
clearLuckmailRuntimeState: async () => {},
@@ -87,7 +86,6 @@ function createRouterWithFinalNode(options = {}) {
listLuckmailPurchasesForManagement: async () => [],
normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyNodeComplete: () => {},
notifyNodeError: () => {},
patchHotmailAccount: async () => {},
@@ -96,7 +94,6 @@ function createRouterWithFinalNode(options = {}) {
requestStop: async () => {},
resetState: async () => {},
resumeAutoRun: async () => {},
scheduleAutoRun: async () => {},
selectLuckmailPurchase: async () => {},
setCurrentHotmailAccount: async () => {},
setCurrentMail2925Account: async () => {},
@@ -103,7 +103,6 @@ function createRouter(overrides = {}) {
broadcastDataUpdate: (updates) => {
events.broadcasts.push(updates);
},
cancelScheduledAutoRun: async () => {},
checkIcloudSession: async () => {},
clearAutoRunTimerAlarm: async () => {},
clearLuckmailRuntimeState: async () => {},
@@ -171,7 +170,6 @@ function createRouter(overrides = {}) {
listLuckmailPurchasesForManagement: async () => [],
normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyNodeComplete: (nodeId, payload) => {
events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload });
},
@@ -183,7 +181,6 @@ function createRouter(overrides = {}) {
requestStop: async () => {},
resetState: async () => {},
resumeAutoRun: async () => {},
scheduleAutoRun: async () => {},
selectLuckmailPurchase: async () => {},
setCurrentHotmailAccount: async () => {},
setEmailState: async (email) => {
@@ -188,6 +188,7 @@ return {
plusPaymentMethod: 'gopay',
plusAccountAccessStrategy: 'oauth',
signupMethod: 'phone',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
}]);
assert.equal(steps[0].title, '注册并输入手机号');
+15
View File
@@ -17,11 +17,16 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
assert.match(source, /flows\/kiro\/background\/desktop-client\.js/);
assert.match(source, /flows\/kiro\/background\/desktop-authorize-runner\.js/);
assert.match(source, /flows\/kiro\/background\/publisher-kiro-rs\.js/);
assert.match(source, /flows\/grok\/background\/state\.js/);
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
assert.match(source, /flows\/grok\/background\/publisher-webchat2api\.js/);
assert.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/);
assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/);
assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/);
assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/);
assert.match(source, /const grokRegisterRunner = self\.MultiPageBackgroundGrokRegisterRunner\?\.createGrokRegisterRunner\(/);
assert.match(source, /const grokWebchat2ApiPublisher = self\.MultiPageBackgroundGrokPublisherWebchat2Api\?\.createGrokWebchat2ApiPublisher\(/);
assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/);
assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/);
@@ -32,11 +37,21 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
assert.match(source, /'kiro-start-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroStartDesktopAuthorize\(state\)/);
assert.match(source, /'kiro-complete-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroCompleteDesktopAuthorize\(state\)/);
assert.match(source, /'kiro-upload-credential': \(state\) => kiroPublisher\.executeKiroUploadCredential\(state\)/);
assert.match(source, /'grok-open-signup-page': \(state\) => grokRegisterRunner\.executeGrokOpenSignupPage\(state\)/);
assert.match(source, /'grok-submit-email': \(state\) => grokRegisterRunner\.executeGrokSubmitEmail\(state\)/);
assert.match(source, /'grok-submit-verification-code': \(state\) => grokRegisterRunner\.executeGrokSubmitVerificationCode\(state\)/);
assert.match(source, /'grok-submit-profile': \(state\) => grokRegisterRunner\.executeGrokSubmitProfile\(state\)/);
assert.match(source, /'grok-extract-sso-cookie': \(state\) => grokRegisterRunner\.executeGrokExtractSsoCookie\(state\)/);
assert.match(source, /'grok-upload-sso-to-webchat2api': \(state\) => grokWebchat2ApiPublisher\.executeGrokUploadSsoToWebchat2Api\(state\)/);
assert.match(
source,
/'kiro-open-register-page',[\s\S]*'kiro-submit-email',[\s\S]*'kiro-submit-name',[\s\S]*'kiro-submit-verification-code',[\s\S]*'kiro-submit-password',[\s\S]*'kiro-complete-register-consent',[\s\S]*'kiro-start-desktop-authorize',[\s\S]*'kiro-complete-desktop-authorize',[\s\S]*'kiro-upload-credential'/
);
assert.match(
source,
/'grok-open-signup-page',[\s\S]*'grok-submit-email',[\s\S]*'grok-submit-verification-code',[\s\S]*'grok-submit-profile',[\s\S]*'grok-extract-sso-cookie',[\s\S]*'grok-upload-sso-to-webchat2api'/
);
});
test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
@@ -201,3 +201,59 @@ return {
true
);
});
test('step 5 recovers from bfcache transport close when tab already reached chatgpt', async () => {
const api = new Function(`
const logs = [];
let waitCalls = 0;
async function getTabId(source) {
return source === 'openai-auth' ? 42 : null;
}
async function waitForTabStableComplete(tabId) {
waitCalls += 1;
if (tabId !== 42) throw new Error('unexpected tab id');
return { url: 'https://chatgpt.com/' };
}
async function addLog(message, level, meta) {
logs.push({ message, level, meta });
}
function getErrorMessage(error) {
return String(error?.message || error || '');
}
${extractFunction('parseUrlSafely')}
${extractFunction('isSignupEntryHost')}
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
${extractFunction('isStep5CompletionChatgptUrl')}
${extractFunction('completeStep5FromTabUrlAfterTransportError')}
return {
async run() {
return completeStep5FromTabUrlAfterTransportError(new Error('The page keeping the extension port is moved into back/forward cache, so the message channel is closed.'));
},
snapshot() {
return { logs, waitCalls };
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
profileSubmitted: true,
postSubmitChecked: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
recoveredFromTransportError: true,
});
assert.equal(snapshot.waitCalls, 1);
assert.equal(
snapshot.logs.some(({ message }) => /通信中断但后台确认标签页已进入 chatgpt\.com/.test(message)),
true
);
});
-2
View File
@@ -195,7 +195,6 @@ let step8TabUpdatedListener = null;
let step8PendingReject = null;
let resumeWaiter = null;
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
const DEFAULT_STATE = {
stepStatuses: Object.fromEntries([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((step) => [step, 'pending'])),
};
@@ -218,7 +217,6 @@ function abortActiveIcloudRequests() {}
function getPendingAutoRunTimerPlan() {
return null;
}
async function cancelScheduledAutoRun() {}
async function clearAutoRunTimerAlarm() {}
function clearStopRequest() {
stopRequested = false;
+14
View File
@@ -69,9 +69,23 @@ test('contribution registry resolves the combined tutorial entry per flow', () =
);
});
test('contribution registry keeps Grok SSO export flow out of published contribution checks', () => {
const api = loadApi();
assert.equal(api.getContributionTutorialEntry('grok'), null);
assert.deepEqual(api.getContributionAdapterIds('grok'), []);
assert.deepEqual(api.getPublishedContributionFlowIds(['openai', 'kiro', 'grok']), ['openai', 'kiro']);
assert.throws(
() => api.assertPublishedFlowsHaveContributionAdapters(['grok']),
/缺少账号贡献适配器:grok/
);
});
test('contribution registry fails fast when a published flow has no adapter', () => {
const api = loadApi();
assert.deepEqual(api.getPublishedContributionFlowIds(), ['openai', 'kiro']);
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(), true);
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true);
assert.throws(
() => api.assertPublishedFlowsHaveContributionAdapters(['openai', 'missing-flow']),
+35 -1
View File
@@ -27,6 +27,8 @@ test('flow capability registry keeps OpenAI phone signup available only when run
assert.equal(enabledState.canUsePhoneSignup, true);
assert.equal(enabledState.effectiveSignupMethod, 'phone');
assert.equal(enabledState.shouldWarnCpaPhoneSignup, true);
assert.equal(enabledState.targetCapabilities.usesOauthTimeoutBudget, true);
assert.equal(enabledState.stepDefinitionOptions.phoneVerificationEnabled, true);
assert.deepEqual(enabledState.effectiveSignupMethods, ['email', 'phone']);
const plusLockedState = registry.resolveSidepanelCapabilities({
@@ -42,7 +44,9 @@ test('flow capability registry keeps OpenAI phone signup available only when run
assert.equal(plusLockedState.canUsePhoneSignup, false);
assert.equal(plusLockedState.effectiveSignupMethod, 'email');
assert.equal(plusLockedState.stepDefinitionOptions.phoneVerificationEnabled, true);
assert.equal(plusLockedState.shouldWarnCpaPhoneSignup, false);
assert.equal(plusLockedState.targetCapabilities.usesOauthTimeoutBudget, false);
assert.deepEqual(plusLockedState.effectiveSignupMethods, ['email']);
});
@@ -94,7 +98,37 @@ test('flow capability registry exposes Kiro as an independent flow with its own
assert.deepEqual(capabilityState.flowCapabilities.contributionAdapterIds, ['kiro-builder-id']);
assert.deepEqual(
capabilityState.visibleGroupIds,
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
['kiro-runtime-status', 'shared-auto-run', 'shared-settings-actions', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
);
});
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', 'shared-auto-run', 'shared-settings-actions', 'grok-target-webchat2api', 'service-account', 'service-email', 'service-proxy']
);
});
+152 -3
View File
@@ -16,31 +16,71 @@ function loadApis() {
test('flow registry exposes canonical flow and target metadata', () => {
const { flowRegistry } = loadApis();
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro']);
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
assert.equal(flowRegistry.normalizeFlowId('kiro'), 'kiro');
assert.equal(flowRegistry.normalizeFlowId('grok'), 'grok');
assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai');
assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI');
assert.deepEqual(
flowRegistry.getFlowDefinition('openai')?.settingsDefaults?.autoRun?.stepExecutionRange,
{ enabled: false, fromStep: 1, toStep: 11 }
);
assert.deepEqual(
flowRegistry.getFlowDefinition('openai')?.targets?.cpa?.defaultState,
{ vpsUrl: '', vpsPassword: '', localCpaStep9Mode: 'submit' }
);
assert.equal(
flowRegistry.getTargetCapabilities('openai', 'cpa')?.usesOauthTimeoutBudget,
true
);
assert.equal(
flowRegistry.getTargetCapabilities('openai', 'sub2api')?.usesOauthTimeoutBudget,
undefined
);
assert.deepEqual(
flowRegistry.getFlowDefinition('kiro')?.targets?.['kiro-rs']?.defaultState,
{ baseUrl: '', apiKey: '' }
);
assert.equal(flowRegistry.normalizeTargetId('openai', 'sub2api'), 'sub2api');
assert.equal(flowRegistry.normalizeTargetId('kiro', 'anything-else'), 'kiro-rs');
assert.equal(flowRegistry.normalizeTargetId('grok', 'anything-else'), 'webchat2api');
assert.deepEqual(
flowRegistry.getVisibleGroupIds('openai', 'cpa'),
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy']
['openai-plus', 'openai-phone', 'shared-auto-run', 'openai-oauth', 'openai-step6', 'shared-settings-actions', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy']
);
assert.deepEqual(
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
['kiro-runtime-status', 'shared-auto-run', 'shared-settings-actions', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
);
assert.deepEqual(
flowRegistry.getVisibleGroupIds('grok', 'webchat2api'),
['grok-runtime-status', 'shared-auto-run', 'shared-settings-actions', 'grok-target-webchat2api', 'service-account', 'service-email', 'service-proxy']
);
assert.deepEqual(
flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
['cpa', 'sub2api', 'codex2api']
);
assert.deepEqual(
flowRegistry.getTargetOptions('grok').map((entry) => entry.id),
['webchat2api']
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method']
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('shared-auto-run')?.rowIds,
['row-shared-auto-run', 'row-auto-run-thread-interval', 'row-step-execution-range']
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('shared-settings-actions')?.rowIds,
['row-settings-actions']
);
assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true);
assert.equal(flowRegistry.getFlowCapabilities('kiro').supportsAccountContribution, true);
assert.equal(flowRegistry.getFlowCapabilities('grok').supportsAccountContribution, false);
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').supportedTargetIds, ['webchat2api']);
assert.deepEqual(
flowRegistry.getFlowCapabilities('openai').contributionAdapterIds,
['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']
@@ -49,6 +89,7 @@ test('flow registry exposes canonical flow and target metadata', () => {
flowRegistry.getFlowCapabilities('kiro').contributionAdapterIds,
['kiro-builder-id']
);
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').contributionAdapterIds, []);
});
test('settings schema normalizes view input into canonical nested namespaces', () => {
@@ -68,6 +109,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
stepExecutionRangeByFlow: {
openai: { enabled: true, fromStep: 2, toStep: 9 },
kiro: { enabled: true, fromStep: 1, toStep: 9 },
grok: { enabled: true, fromStep: 2, toStep: 4 },
},
});
@@ -78,6 +120,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
assert.equal(normalized.flows.openai.selectedTargetId, 'cpa');
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(normalized.flows.kiro.selectedTargetId, 'kiro-rs');
assert.equal(normalized.flows.grok.selectedTargetId, 'webchat2api');
assert.equal(normalized.flows.kiro.targets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin');
assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key');
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
@@ -85,6 +128,11 @@ test('settings schema normalizes view input into canonical nested namespaces', (
fromStep: 1,
toStep: 9,
});
assert.deepEqual(normalized.flows.grok.autoRun.stepExecutionRange, {
enabled: true,
fromStep: 2,
toStep: 4,
});
});
test('settings schema lets explicit flat step range override stale canonical range', () => {
@@ -130,6 +178,11 @@ test('settings schema can project canonical state into a read view without legac
assert.equal(view.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(view.settingsSchemaVersion, 5);
assert.equal(view.settingsState.activeFlowId, 'kiro');
assert.deepEqual(view.stepExecutionRangeByFlow.grok, {
enabled: false,
fromStep: 1,
toStep: 6,
});
});
test('settings schema preserves CPA session strategy in canonical state and read view', () => {
@@ -143,3 +196,99 @@ test('settings schema preserves CPA session strategy in canonical state and read
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'cpa_codex_session');
assert.equal(view.plusAccountAccessStrategy, 'cpa_codex_session');
});
test('settings schema preserves registered custom flow settings without openai/kiro hardcoding', () => {
const { settingsSchema } = loadApis();
const customFlowRegistry = {
DEFAULT_FLOW_ID: 'openai',
getRegisteredFlowIds: () => ['openai', 'kiro', 'sample'],
getDefaultTargetId(flowId) {
return flowId === 'sample' ? 'sample-target' : (flowId === 'kiro' ? 'kiro-rs' : 'cpa');
},
getFlowDefinition(flowId) {
if (flowId !== 'sample') {
return null;
}
return {
id: 'sample',
defaultTargetId: 'sample-target',
settingsDefaults: {
targets: {
'sample-target': {
endpoint: 'https://sample.example.com',
},
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 3 },
},
},
};
},
getTargetDefinitions(flowId) {
if (flowId === 'sample') {
return {
'sample-target': { id: 'sample-target', label: 'Sample Target' },
};
}
if (flowId === 'kiro') {
return {
'kiro-rs': { id: 'kiro-rs', label: 'kiro.rs' },
};
}
return {
cpa: { id: 'cpa', label: 'CPA' },
sub2api: { id: 'sub2api', label: 'SUB2API' },
codex2api: { id: 'codex2api', label: 'Codex2API' },
};
},
normalizeFlowId(value = '', fallback = 'openai') {
const normalized = String(value || '').trim().toLowerCase();
return ['openai', 'kiro', 'sample'].includes(normalized)
? normalized
: (['openai', 'kiro', 'sample'].includes(fallback) ? fallback : 'openai');
},
normalizeTargetId(flowId, targetId = '', fallback = '') {
const targets = Object.keys(customFlowRegistry.getTargetDefinitions(flowId));
const normalized = String(targetId || '').trim().toLowerCase();
if (targets.includes(normalized)) {
return normalized;
}
if (targets.includes(fallback)) {
return fallback;
}
return customFlowRegistry.getDefaultTargetId(flowId);
},
};
const schema = settingsSchema.createSettingsSchema({ flowRegistry: customFlowRegistry });
const normalized = schema.normalizeSettingsState({
activeFlowId: 'sample',
targetId: 'sample-target',
settingsState: {
flows: {
sample: {
selectedTargetId: 'sample-target',
targets: {
'sample-target': {
endpoint: 'https://custom.example.com',
},
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 2, toStep: 3 },
},
},
},
},
});
const view = schema.buildSettingsView(normalized);
assert.equal(normalized.activeFlowId, 'sample');
assert.equal(normalized.flows.sample.selectedTargetId, 'sample-target');
assert.equal(normalized.flows.sample.targets['sample-target'].endpoint, 'https://custom.example.com');
assert.deepEqual(view.stepExecutionRangeByFlow.sample, {
enabled: true,
fromStep: 2,
toStep: 3,
});
assert.equal(schema.getSelectedTargetId(normalized, 'sample'), 'sample-target');
});
+268
View File
@@ -0,0 +1,268 @@
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 });
if (message.nodeId === 'GET_PAGE_STATE') {
return { submitted: true, state: 'verification_code_entry', url: 'https://accounts.x.ai/verify' };
}
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' && entry.message.nodeId === 'grok-submit-verification-code'
));
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 verification runner waits for verification page before polling mail', async () => {
const api = loadGrokRunnerApi();
let pollCalled = false;
let currentState = {
activeFlowId: 'grok',
grokRegisterTabId: 102,
grokEmail: 'grok-user@example.com',
grokVerificationRequestedAt: 1000000,
runtimeState: {
flowState: {
grok: {
session: {
registerTabId: 102,
},
register: {
email: 'grok-user@example.com',
verificationRequestedAt: 1000000,
},
},
},
},
};
const originalDateNow = Date.now;
let fakeNow = 1000000;
const runner = api.createGrokRegisterRunner({
addLog: async () => {},
chrome: {
tabs: {
get: async (tabId) => ({ id: tabId }),
update: async () => {},
},
},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getState: async () => currentState,
getTabId: async () => 102,
isTabAlive: async () => true,
pollFlowVerificationCode: async () => {
pollCalled = true;
return { code: 'ABC-123', messageId: 'mail-001' };
},
registerTab: async () => {},
sendToContentScriptResilient: async (_sourceId, message) => {
if (message.nodeId === 'GET_PAGE_STATE') {
return { submitted: true, state: 'email_entry', url: 'https://accounts.x.ai/sign-up' };
}
return { submitted: true, state: 'profile_entry', url: 'https://accounts.x.ai/profile' };
},
setState: async (patch) => {
currentState = { ...currentState, ...patch };
},
sleepWithStop: async (ms = 0) => {
fakeNow += Number(ms) || 1000;
},
waitForTabStableComplete: async () => {},
});
Date.now = () => fakeNow;
try {
await assert.rejects(
() => runner.executeGrokSubmitVerificationCode({ nodeId: 'grok-submit-verification-code', ...currentState }),
/尚未进入验证码页面/
);
} finally {
Date.now = originalDateNow;
}
assert.equal(pollCalled, false);
});
test('grok SSO extraction stores only the current cookie without logging the secret value', async () => {
const api = loadGrokRunnerApi();
const logs = [];
let completedPayload = null;
let markUsedPayload = null;
let currentState = {
activeFlowId: 'grok',
grokRegisterTabId: 202,
grokSsoCookies: ['old-cookie'],
runtimeState: {
flowState: {
grok: {
session: {
registerTabId: 202,
},
sso: {
cookies: ['old-cookie'],
},
upload: {
status: 'uploaded',
uploadedAt: 1000,
message: 'old upload',
targetUrl: 'https://old.example.com/api/remote-account/inject',
},
},
},
},
};
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, ['new-cookie']);
assert.equal(completedPayload.grokWebchat2ApiUploadStatus, '');
assert.equal(getGrokRuntime(completedPayload).sso.currentCookie, 'new-cookie');
assert.deepEqual(getGrokRuntime(completedPayload).sso.cookies, ['new-cookie']);
assert.equal(getGrokRuntime(completedPayload).upload.status, '');
assert.equal(getGrokRuntime(completedPayload).upload.targetUrl, '');
assert.equal(markUsedPayload.grokSsoCookie, 'new-cookie');
assert.equal(logs.some(({ message }) => message.includes('new-cookie')), false);
});
test('grok register runner requires background node completion dependency', () => {
const api = loadGrokRunnerApi();
assert.throws(
() => api.createGrokRegisterRunner({}),
/requires completeNodeFromBackground/
);
});
+2
View File
@@ -3,12 +3,14 @@ const fs = require('node:fs');
const FLOW_DEFINITION_FILES = Object.freeze([
'flows/openai/index.js',
'flows/kiro/index.js',
'flows/grok/index.js',
'flows/index.js',
]);
const FLOW_WORKFLOW_FILES = Object.freeze([
'flows/openai/workflow.js',
'flows/kiro/workflow.js',
'flows/grok/workflow.js',
]);
function readBundle(files = []) {
@@ -263,6 +263,7 @@ const window = {
},
};
let latestState = { activeFlowId: 'openai' };
const inputPhoneVerificationEnabled = { checked: false };
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
@@ -270,6 +271,7 @@ const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_sessio
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
let currentPlusAccountAccessStrategy = 'oauth';
let currentSignupMethod = 'email';
let currentPhoneVerificationEnabled = false;
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
let currentStepDefinitionFlowId = 'openai';
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
@@ -326,6 +328,7 @@ return {
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
accountContributionEnabled: false,
},
@@ -338,6 +341,7 @@ return {
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
accountContributionEnabled: false,
},
+2 -8
View File
@@ -85,7 +85,6 @@ test('sidepanel idle auto-run status does not reset manual run count input', ()
extractFunction(source, 'syncAutoRunState'),
extractFunction(source, 'isAutoRunLockedPhase'),
extractFunction(source, 'isAutoRunPausedPhase'),
extractFunction(source, 'isAutoRunScheduledPhase'),
extractFunction(source, 'applyAutoRunStatus'),
].join('\n');
@@ -96,7 +95,6 @@ let currentAutoRun = {
currentRun: 0,
totalRuns: 1,
attemptRun: 0,
scheduledAt: null,
countdownAt: null,
countdownTitle: '',
countdownNote: '',
@@ -116,9 +114,8 @@ function getLockedRunCountFromEmailPool() { return lockedRunCount; }
function isCustomMailProvider() { return false; }
function usesCustomEmailPoolGenerator() { return false; }
function setDefaultAutoRunButton() {}
function updateAutoDelayInputState() {}
function updateFallbackThreadIntervalInputState() {}
function syncScheduledCountdownTicker() {}
function syncAutoRunCountdownTicker() {}
function updateStopButtonState() {}
function getStepStatuses() { return {}; }
function updateConfigMenuControls() {}
@@ -171,7 +168,6 @@ test('sidepanel pending auto-run start ignores stale active run count sync', ()
extractFunction(source, 'syncAutoRunState'),
extractFunction(source, 'isAutoRunLockedPhase'),
extractFunction(source, 'isAutoRunPausedPhase'),
extractFunction(source, 'isAutoRunScheduledPhase'),
extractFunction(source, 'isAutoRunSourceSyncPhase'),
extractFunction(source, 'shouldSyncRunCountFromAutoRunSource'),
extractFunction(source, 'applyAutoRunStatus'),
@@ -184,7 +180,6 @@ let currentAutoRun = {
currentRun: 0,
totalRuns: 1,
attemptRun: 0,
scheduledAt: null,
countdownAt: null,
countdownTitle: '',
countdownNote: '',
@@ -205,9 +200,8 @@ function getLockedRunCountFromEmailPool() { return 0; }
function isCustomMailProvider() { return false; }
function usesCustomEmailPoolGenerator() { return false; }
function setDefaultAutoRunButton() {}
function updateAutoDelayInputState() {}
function updateFallbackThreadIntervalInputState() {}
function syncScheduledCountdownTicker() {}
function syncAutoRunCountdownTicker() {}
function updateStopButtonState() {}
function getStepStatuses() { return {}; }
function updateConfigMenuControls() {}
@@ -73,8 +73,6 @@ const inputAutoSkipFailures = { checked: false };
const inputContributionNickname = { value: 'tester' };
const inputContributionQq = { value: '123456' };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const btnAutoRun = { disabled: false, innerHTML: '' };
const inputRunCount = { disabled: false };
const selectFlow = { value: latestState.activeFlowId };
@@ -132,7 +130,6 @@ function shouldWarnAutoRunFallbackRisk() { return false; }
function isAutoRunFallbackRiskPromptDismissed() { return false; }
async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); }
function setAutoRunFallbackRiskPromptDismissed() {}
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
@@ -272,8 +269,6 @@ const inputAutoSkipFailures = { checked: false };
const inputContributionNickname = { value: 'tester' };
const inputContributionQq = { value: '123456' };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const btnAutoRun = { disabled: false, innerHTML: '' };
const inputRunCount = { disabled: false, value: '1' };
const inputPhoneVerificationEnabled = { checked: true };
@@ -324,7 +319,6 @@ function shouldWarnAutoRunFallbackRisk() { return false; }
function isAutoRunFallbackRiskPromptDismissed() { return false; }
async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); }
function setAutoRunFallbackRiskPromptDismissed() {}
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
return null;
@@ -247,10 +247,7 @@ const inputTempEmailReceiveMailbox = { value: 'relay@example.com' };
const inputTempEmailUseRandomSubdomain = { checked: true };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: true };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '10' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '6' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
@@ -297,7 +294,6 @@ function normalizeLuckmailEmailType(value) { return String(value || '').trim();
function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizePlusAccountAccessStrategy(value = '') { return String(value || '').trim().toLowerCase() === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; }
@@ -36,8 +36,14 @@ function extractFunction(source, name) {
test('sidepanel html exposes flow selector and kiro source fields', () => {
[
'id="select-flow"',
'<option value="grok">Grok</option>',
'id="label-source-selector"',
'id="btn-open-webchat2api-github"',
'id="row-step6-cookie-settings"',
'id="row-shared-auto-run"',
'id="row-auto-run-thread-interval"',
'id="row-oauth-callback"',
'id="row-settings-actions"',
'id="row-kiro-rs-url"',
'id="btn-open-kiro-rs-github"',
'id="row-kiro-rs-key"',
@@ -46,9 +52,100 @@ test('sidepanel html exposes flow selector and kiro source fields', () => {
'id="row-kiro-web-status"',
'id="row-kiro-login-url"',
'id="row-kiro-upload-status"',
'id="row-grok-register-status"',
'id="row-grok-sso-status"',
'id="row-grok-webchat2api-upload-status"',
'id="display-grok-webchat2api-upload-status"',
'id="row-grok-sso-settings"',
'id="btn-copy-grok-sso"',
'id="btn-clear-grok-sso"',
'<script src="../flows/grok/index.js"></script>',
'<script src="../flows/grok/workflow.js"></script>',
].forEach((snippet) => {
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});
assert.doesNotMatch(sidepanelHtml, /id="btn-export-grok-sso"/);
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, 'getGrokWebchat2ApiUploadStatusLabel'),
extractFunction(sidepanelSource, 'renderGrokRuntimeState'),
].join('\n');
const api = new Function(`
let latestState = {};
const displayGrokRegisterStatus = { textContent: '' };
const displayGrokSsoStatus = { textContent: '' };
const displayGrokSsoCookie = { textContent: '', title: '' };
const displayGrokWebchat2ApiUploadStatus = { textContent: '', title: '' };
const buttons = [];
const btnCopyGrokSso = { disabled: false };
const btnClearGrokSso = { disabled: false };
${bundle}
return {
displayGrokRegisterStatus,
displayGrokSsoStatus,
displayGrokSsoCookie,
displayGrokWebchat2ApiUploadStatus,
btnCopyGrokSso,
btnClearGrokSso,
renderGrokRuntimeState,
};
`)();
api.renderGrokRuntimeState({
runtimeState: {
flowState: {
grok: {
register: { status: 'completed' },
sso: {
currentCookie: '1234567890abcdef',
cookies: ['1234567890abcdef', 'second-cookie'],
extractedAt: 0,
},
upload: {
status: 'uploaded',
uploadedAt: 0,
message: '上传成功',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
},
},
},
});
assert.equal(api.displayGrokRegisterStatus.textContent, '已完成');
assert.match(api.displayGrokSsoStatus.textContent, /^已提取 2 条/);
assert.equal(api.displayGrokSsoCookie.textContent, '12345678...abcdef');
assert.equal(api.displayGrokWebchat2ApiUploadStatus.textContent, '已上传:上传成功');
assert.equal(api.displayGrokWebchat2ApiUploadStatus.title, 'https://remote.example.com/api/remote-account/inject');
assert.equal(api.btnCopyGrokSso.disabled, false);
assert.equal(api.btnClearGrokSso.disabled, false);
});
test('sidepanel Kiro GitHub button opens the configured fork', () => {
@@ -56,6 +153,10 @@ test('sidepanel Kiro GitHub button opens the configured fork', () => {
assert.doesNotMatch(sidepanelSource, /github\.com\/hank9999\/kiro\.rs/);
});
test('sidepanel webchat2api GitHub button opens the configured repository', () => {
assert.match(sidepanelSource, /openExternalUrl\('https:\/\/github\.com\/zqbxdev\/webchat2api'\)/);
});
test('sidepanel step definitions rerender when active flow changes even if plus/signup settings stay the same', () => {
const bundle = [
extractFunction(sidepanelSource, 'normalizeSignupMethod'),
@@ -80,6 +181,7 @@ let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = 'oauth';
let currentSignupMethod = 'email';
let currentPhoneVerificationEnabled = false;
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
let currentStepDefinitionFlowId = 'openai';
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
@@ -122,6 +224,7 @@ return {
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'oauth',
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
accountContributionEnabled: false,
},
+1 -9
View File
@@ -132,10 +132,7 @@ const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
@@ -220,7 +217,6 @@ function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value |
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
function normalizePlusAccountAccessStrategy(value = '') { return String(value || '').trim().toLowerCase() === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; }
@@ -408,10 +404,7 @@ const selectLuckmailEmailType = { value: 'ms_graph' };
const inputLuckmailDomain = { value: '' };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '' };
const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '' };
const inputPhoneVerificationEnabled = { checked: false };
const selectPhoneSmsProvider = { value: 'hero-sms' };
@@ -483,7 +476,6 @@ function applyCloudflareTempEmailSettingsState() {}
function renderCloudflareDomainOptions() {}
function setCloudflareDomainEditMode() {}
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); }
@@ -519,12 +511,12 @@ function updateHeroSmsPlatformDisplay() {}
function updatePhoneSmsProviderOrderSummary() {}
function applyAutoRunStatus() {}
function markSettingsDirty() {}
function updateAutoDelayInputState() {}
function updateFallbackThreadIntervalInputState() {}
function updateAccountRunHistorySettingsUI() {}
function updatePhoneVerificationSettingsUI() {}
function updatePanelModeUI() {}
function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); }
function renderGrokRuntimeState() {}
function renderSub2ApiGroupOptions() {}
function isLuckmailProvider() { return false; }
function updateButtonStates() {}
@@ -203,10 +203,7 @@ const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
@@ -258,7 +255,6 @@ function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value |
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
function normalizePlusAccountAccessStrategy(value = '') { return String(value || '').trim().toLowerCase() === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; }
+19 -8
View File
@@ -38,25 +38,36 @@ function extractFunction(name) {
throw new Error(`unterminated ${name}`);
}
test('sidepanel no longer exposes operation delay switch and places step execution range below oauth timeout', () => {
test('sidepanel splits shared auto-run controls from openai oauth controls', () => {
assert.doesNotMatch(html, /id="row-operation-delay-settings"/);
assert.doesNotMatch(html, /id="input-operation-delay-enabled"/);
assert.doesNotMatch(html, /id="row-auto-delay-settings"/);
assert.doesNotMatch(html, /id="input-auto-delay-enabled"/);
assert.doesNotMatch(html, /id="input-auto-delay-minutes"/);
const step6CookieIndex = html.indexOf('id="row-step6-cookie-settings"');
const autoDelayIndex = html.indexOf('id="row-auto-delay-settings"');
const oauthTimeoutIndex = html.indexOf('id="row-oauth-flow-timeout"');
const sharedAutoRunIndex = html.indexOf('id="row-shared-auto-run"');
const threadIntervalIndex = html.indexOf('id="row-auto-run-thread-interval"');
const stepRangeIndex = html.indexOf('id="row-step-execution-range"');
const oauthDisplayIndex = html.indexOf('id="row-oauth-display"');
const oauthCallbackIndex = html.indexOf('id="row-oauth-callback"');
const settingsActionsIndex = html.indexOf('id="row-settings-actions"');
assert.notEqual(step6CookieIndex, -1);
assert.notEqual(autoDelayIndex, -1);
assert.notEqual(oauthTimeoutIndex, -1);
assert.notEqual(sharedAutoRunIndex, -1);
assert.notEqual(threadIntervalIndex, -1);
assert.doesNotMatch(html, /id="row-oauth-flow-timeout"/);
assert.doesNotMatch(html, /id="input-oauth-flow-timeout-enabled"/);
assert.notEqual(stepRangeIndex, -1);
assert.notEqual(oauthDisplayIndex, -1);
assert.ok(autoDelayIndex > step6CookieIndex, 'startup delay row should render below the openai step6 cookie row');
assert.ok(stepRangeIndex > autoDelayIndex, 'step execution range should still remain below the startup delay row');
assert.ok(stepRangeIndex > oauthTimeoutIndex, 'step execution range should render below oauth timeout');
assert.notEqual(oauthCallbackIndex, -1);
assert.notEqual(settingsActionsIndex, -1);
assert.ok(sharedAutoRunIndex > step6CookieIndex, 'shared auto-run should render below the openai step6 cookie row');
assert.ok(threadIntervalIndex > sharedAutoRunIndex, 'thread interval should be part of the shared auto-run block');
assert.ok(stepRangeIndex > threadIntervalIndex, 'step execution range should render below shared thread interval');
assert.ok(stepRangeIndex < oauthDisplayIndex, 'step execution range should stay above oauth runtime display');
assert.ok(oauthCallbackIndex > oauthDisplayIndex, 'openai callback row should follow the oauth display');
assert.ok(settingsActionsIndex > oauthCallbackIndex, 'save settings action should live outside the callback row');
});
test('sidepanel operation delay state is always normalized back to enabled', () => {
@@ -713,7 +713,6 @@ function setFreePhoneReuseControlsLocked(locked) {
|| !Boolean(inputPhoneVerificationEnabled.checked && phoneVerificationSectionExpanded);
}
function isAutoRunLockedPhase() { return false; }
function isAutoRunScheduledPhase() { return false; }
${extractFunction('normalizeSignupMethod')}
${extractFunction('normalizeHeroSmsReuseEnabledValue')}
@@ -979,8 +978,6 @@ const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true };
const inputFreePhoneReuseEnabled = { checked: true };
@@ -1072,7 +1069,6 @@ function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value |
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
function normalizePlusAccountAccessStrategy(value = '') { return String(value || '').trim().toLowerCase() === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; }
+4 -2
View File
@@ -89,6 +89,7 @@ const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email';
let currentPhoneVerificationEnabled = false;
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email';
let stepDefinitions = [];
@@ -113,7 +114,7 @@ return {
assert.deepEqual(api.getStepIds(), [7]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false, accountContributionEnabled: false },
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneVerificationEnabled: false, phoneSignupReloginAfterBindEmailEnabled: false, accountContributionEnabled: false },
});
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
});
@@ -400,6 +401,7 @@ const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email';
let currentPhoneVerificationEnabled = false;
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email';
let stepDefinitions = [];
@@ -424,7 +426,7 @@ return {
assert.deepEqual(api.getStepIds(), [13]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false, accountContributionEnabled: false },
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneVerificationEnabled: false, phoneSignupReloginAfterBindEmailEnabled: false, accountContributionEnabled: false },
});
});
+63 -2
View File
@@ -15,6 +15,11 @@ test('background imports shared source registry module', () => {
assert.match(source, /core\/flow-kernel\/settings-schema\.js/);
assert.match(source, /core\/flow-kernel\/source-registry\.js/);
assert.match(source, /shared\/kiro-timeouts\.js/);
assert.match(source, /flows\/grok\/index\.js/);
assert.match(source, /flows\/grok\/workflow\.js/);
assert.match(source, /flows\/grok\/background\/state\.js/);
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
assert.match(source, /flows\/grok\/mail-rules\.js/);
});
test('manifest loads shared source registry before content utils in static bundles', () => {
@@ -41,15 +46,38 @@ test('manifest no longer ships a static Kiro content bundle', () => {
assert.equal(hasStaticKiroBundle, false);
});
test('manifest loads Grok flow definition in static bundles but not Grok content runtime', () => {
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
for (const entry of manifest.content_scripts || []) {
const scripts = Array.isArray(entry.js) ? entry.js : [];
if (!scripts.includes('flows/index.js')) continue;
assert.ok(scripts.includes('flows/kiro/index.js'));
assert.ok(scripts.includes('flows/grok/index.js'));
assert.ok(
scripts.indexOf('flows/kiro/index.js') < scripts.indexOf('flows/grok/index.js'),
'Kiro definition should load before Grok definition'
);
assert.ok(
scripts.indexOf('flows/grok/index.js') < scripts.indexOf('flows/index.js'),
'Grok definition must load before flows/index.js'
);
assert.equal(scripts.includes('flows/grok/content/register-page.js'), false);
}
});
test('background injects shared Kiro timeout module before Kiro content scripts', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(
source,
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
);
assert.match(
source,
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
);
assert.match(
source,
/const GROK_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'content\/utils\.js', 'flows\/grok\/content\/register-page\.js'\];/
);
});
@@ -83,6 +111,20 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
}),
'kiro-desktop-authorize'
);
assert.equal(
registry.detectSourceFromLocation({
url: 'https://accounts.x.ai/sign-up',
hostname: 'accounts.x.ai',
}),
'grok-register-page'
);
assert.equal(
registry.detectSourceFromLocation({
url: 'https://grok.com/',
hostname: 'grok.com',
}),
'grok-register-page'
);
assert.equal(
registry.detectSourceFromLocation({
url: 'https://example.com/',
@@ -115,6 +157,22 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
),
true
);
assert.equal(
registry.matchesSourceUrlFamily(
'grok-register-page',
'https://accounts.x.ai/sign-up',
'https://grok.com/'
),
true
);
assert.equal(
registry.matchesSourceUrlFamily(
'grok-register-page',
'https://grok.com/',
'https://accounts.x.ai/sign-up'
),
true
);
assert.equal(
registry.matchesSourceUrlFamily(
'kiro-desktop-authorize',
@@ -137,4 +195,7 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/register-runner', 'kiro-open-register-page'), true);
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/desktop-authorize-runner', 'kiro-start-desktop-authorize'), true);
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/publisher-kiro-rs', 'kiro-upload-credential'), true);
assert.equal(registry.driverAcceptsCommand('flows/grok/content/register-page', 'grok-submit-profile'), true);
assert.equal(registry.driverAcceptsCommand('flows/grok/background/register-runner', 'grok-extract-sso-cookie'), true);
assert.equal(registry.driverAcceptsCommand('flows/grok/background/publisher-webchat2api', 'grok-upload-sso-to-webchat2api'), true);
});
+63 -1
View File
@@ -23,6 +23,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
const kiroSteps = api.getSteps({ activeFlowId: 'kiro' });
const grokSteps = api.getSteps({ activeFlowId: 'grok' });
assert.equal(Array.isArray(steps), true);
assert.equal(steps.length, 11);
@@ -162,8 +163,9 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19);
assert.equal(api.hasFlow('openai'), true);
assert.equal(api.hasFlow('kiro'), true);
assert.equal(api.hasFlow('grok'), true);
assert.equal(api.hasFlow('site-a'), false);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro']);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
assert.deepStrictEqual(
@@ -210,6 +212,38 @@ 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',
'grok-upload-sso-to-webchat2api',
]
);
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', '上传 SSO 到 webchat2api']
);
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5, 6]);
assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 6);
assert.deepStrictEqual(
api.getNodes({ activeFlowId: 'grok' }).map((node) => node.next),
[
['grok-submit-email'],
['grok-submit-verification-code'],
['grok-submit-profile'],
['grok-extract-sso-cookie'],
['grok-upload-sso-to-webchat2api'],
[],
]
);
assert.equal(plusSteps[6].title, '创建 Plus Checkout');
assert.equal(plusSteps[8].title, 'PayPal 登录与授权');
@@ -399,6 +433,34 @@ test('Plus no-payment mode removes only payment chain nodes', () => {
assert.deepStrictEqual(cpaNodes.find((node) => node.nodeId === 'wait-registration-success')?.next, ['cpa-session-import']);
});
test('OpenAI OAuth workflow removes post-login phone verification when phone verification is disabled', () => {
const globalScope = {};
const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope);
[
{ label: 'normal', options: { phoneVerificationEnabled: false } },
{ label: 'plus paypal', options: { plusModeEnabled: true, phoneVerificationEnabled: false } },
{ label: 'plus gopay', options: { plusModeEnabled: true, plusPaymentMethod: 'gopay', phoneVerificationEnabled: false } },
{ label: 'phone relogin', options: { signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true, phoneVerificationEnabled: false } },
].forEach(({ label, options }) => {
const steps = api.getSteps(options);
const nodes = api.getNodes(options);
const keys = steps.map((step) => step.key);
const nodeIds = nodes.map((node) => node.nodeId);
const expectedNextAfterLoginCode = keys.includes('bind-email') ? 'bind-email' : 'confirm-oauth';
assert.equal(keys.includes('post-login-phone-verification'), false, `${label} should hide post-login phone step`);
assert.equal(keys.includes('post-bound-email-phone-verification'), false, `${label} should hide bound-email phone step`);
assert.equal(nodeIds.includes('post-login-phone-verification'), false, `${label} nodes should hide post-login phone step`);
assert.equal(nodeIds.includes('post-bound-email-phone-verification'), false, `${label} nodes should hide bound-email phone step`);
assert.deepStrictEqual(
nodes.find((node) => node.nodeId === 'fetch-login-code')?.next,
[expectedNextAfterLoginCode],
`${label} fetch-login-code should link to the next non-phone node`
);
});
});
test('Plus session strategy swaps the OAuth tail for a single SUB2API import node', () => {
const globalScope = {};
const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope);
+16 -7
View File
@@ -1109,7 +1109,7 @@ return {
assert.equal(api.isCompletion('https://chatgpt.com/add-phone'), false);
});
test('step 5 navigation reporter does not complete on beforeunload alone', () => {
test('step 5 navigation reporter hands off to background on beforeunload', () => {
const api = new Function(`
const events = [];
const listeners = new Map();
@@ -1147,22 +1147,31 @@ ${extractFunction('installStep5NavigationCompletionReporter')}
return {
run() {
let completionCount = 0;
const cleanup = installStep5NavigationCompletionReporter(() => {
completionCount += 1;
events.push({ type: 'complete' });
const cleanup = installStep5NavigationCompletionReporter((payload) => {
events.push({ type: 'complete', payload });
});
const beforeUnload = listeners.get('beforeunload');
if (beforeUnload) {
beforeUnload({ type: 'beforeunload' });
}
cleanup();
return { completionCount, events };
return { events };
},
};
`)();
const result = api.run();
assert.equal(result.completionCount, 0);
assert.deepStrictEqual(
result.events.filter((entry) => entry.type === 'complete'),
[
{
type: 'complete',
payload: {
navigationStarted: true,
navigationEventType: 'beforeunload',
},
},
]
);
assert.equal(result.events.some((entry) => entry.type === 'log' && /检测到页面开始导航/.test(entry.message)), true);
});
-4
View File
@@ -76,7 +76,6 @@ let autoRunCurrentRun = 2;
let autoRunTotalRuns = 3;
let autoRunAttemptRun = 4;
let autoRunSessionId = 99;
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
const DEFAULT_STATE = {
nodeStatuses: {},
};
@@ -158,9 +157,6 @@ async function getState() {
function getPendingAutoRunTimerPlan() {
return null;
}
function isAutoRunScheduledState() {
return false;
}
function getStep8CallbackUrlFromNavigation() { return ''; }
function getStep8CallbackUrlFromTabUpdate() { return ''; }
function getStep8EffectLabel() { return 'no_effect'; }
+43 -11
View File
@@ -10,10 +10,11 @@
## 1. 项目目标
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册条主 flow
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册条主 flow
- `openai`OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路
- `kiro`AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路
- `grok`Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 远程上传链路
它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路:
@@ -28,7 +29,7 @@
- 自动确认 OAuth 同意页
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。
同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro / Grok 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。
## 2. 核心运行参与者
@@ -43,8 +44,8 @@
- 向后台发送命令
- 接收后台广播并更新 UI
- 动态渲染步骤列表
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target`kiro` flow 使用独立`flows.kiro.targetId`
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态不显示 OpenAI 接码 / Plus / 平台绑定配置
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target`kiro` / `grok` flow 使用各自`flows.<flowId>.targetId`
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态Grok flow 只显示 `webchat2api` URL / 管理密钥、SSO 提取与上传状态、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置
- 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
@@ -150,13 +151,13 @@
- `runtimeState`:当前运行中的 node、nodeStatuses、activeRunId 等核心执行态
- `sharedState`automation window、tab registry、最近来源 URL 这类跨 flow 共享运行态
- `serviceState`:代理、邮箱等共享服务运行态
- `flowState`OpenAI / Kiro 各自的 flow 私有运行态
- `flowState`OpenAI / Kiro / Grok 各自的 flow 私有运行态
这两个模块的作用,是把“当前该跑哪些节点”“当前 state 应该怎么看”“前后台恢复时如何合并 patch”从 `background.js` 主入口里抽离出来,避免再次退回到旧的平铺 step 状态机。
### 3.4 步骤注册
[data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import``cpa-session-import``kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow。
[data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import``cpa-session-import``kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow`grok` flow 输出固定的 6 节点注册、SSO Cookie 提取与 `webchat2api` 上传 workflow。
[core/flow-kernel/step-registry.js](./core/flow-kernel/step-registry.js) 负责把“workflow node 元数据”映射到“步骤执行器”,同时保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据。
这意味着:
@@ -168,7 +169,7 @@
### 3.4.1 验证码规则注册
[background/mail-rule-registry.js](./background/mail-rule-registry.js) 负责按当前 `activeFlowId` 选择验证码规则构造器。[flows/openai/mail-rules.js](./flows/openai/mail-rules.js) 提供 OpenAI flow 当前使用的注册验证码 / 登录验证码规则。
[background/mail-rule-registry.js](./background/mail-rule-registry.js) 负责按当前 `activeFlowId` 选择验证码规则构造器。[flows/openai/mail-rules.js](./flows/openai/mail-rules.js) 提供 OpenAI flow 当前使用的注册验证码 / 登录验证码规则[flows/kiro/mail-rules.js](./flows/kiro/mail-rules.js) 提供 Kiro / AWS Builder ID 注册验证码与桌面授权验证码规则,[flows/grok/mail-rules.js](./flows/grok/mail-rules.js) 提供 xAI / Grok 注册验证码规则
这层统一产出:
@@ -180,7 +181,8 @@
它的职责边界是:
- `background/verification-flow.js` 只消费统一规则 payload,不再在每个 provider 或内容脚本里各自硬写一套 OpenAI 验证码过滤条件。
- “邮件从哪里读”由 provider 决定,“哪封邮件算命中验证码”优先由规则注册层决定
- `background/flow-mail-polling.js` 负责共享的 provider 分派、iCloud / 2925 会话准备、网页邮箱标签页打开与内容脚本轮询;flow runner 只提供 `flowId / nodeId / step / filterAfterTimestamp` 这类业务上下文
- “邮件从哪里读”由公共轮询调度层和 provider 决定,“哪封邮件算命中验证码”优先由规则注册层决定。
- 新 flow 如需接入邮箱验证码,应优先新增自己的 mail rules builder,而不是继续改写 OpenAI 规则。
### 3.5 日志步骤号链路
@@ -214,6 +216,7 @@
- `contributionLastPollAt`
- OAuth 链接
- Kiro 运行态:使用 `kiroRuntime.session / register / webAuth / desktopAuth / upload` 命名空间,保存 Kiro 官方登录入口、Builder ID 注册页状态、Kiro Web 登录态摘要、桌面授权 PKCE 凭据、上传状态和当前 Kiro 会话进度
- Grok 运行态:权威状态位于 `runtimeState.flowState.grok`,包含 `session / register / sso / upload`;顶层 `grokRegisterTabId / grokPageState / grokEmail / grokPassword / grokSsoCookie / grokSsoCookies / grokWebchat2ApiUpload*` 只作为兼容投影,不应当再形成第二套状态机;兼容 flat 字段只有非空值才能覆盖 canonical runtime,避免空旧字段反向覆盖本轮 SSO
- 当前轮冻结后的注册方式 `resolvedSignupMethod`
- 当前统一账号标识 `accountIdentifierType / accountIdentifier`
- 当前邮箱 / 密码
@@ -278,7 +281,7 @@
- Kiro 目标配置:`flows.kiro.targetId``flows.kiro.targets["kiro-rs"] = { baseUrl, apiKey }`
- 自动运行默认配置
- 操作间延迟兼容字段 `operationDelayEnabled`:当前会被严格归一为启用;sidepanel 不再暴露真正关闭入口,但内容脚本仍通过 `chrome.storage.local` 监听该字段完成兼容同步
- OAuth 授权后链总超时开关 `oauthFlowTimeoutEnabled`:默认开启;关闭后会立即清空已存在的 OAuth 总预算 deadline,仅保留各步骤本地等待超时
- OAuth 授权后链总超时:不再作为侧栏配置项持久化,只由 `openai + cpa` target capability 自动启用;其他来源与 flow 不启动 OAuth 总预算,仅保留各步骤本地等待超时
- flow 级执行范围配置 `stepExecutionRangeByFlow`:按 flowId 保存,例如 `openai: { enabled: true, fromStep: 3, toStep: 6 }`;侧栏中的 `codex` 会归一到内部默认 flowId `openai`。这是通用配置结构,但当前只给 codex/openai flow 暴露 UI。
- 账号运行历史 `accountRunHistory`(以统一账号标识为主键;邮箱账号保留邮箱主键,手机号注册保留手机号主键;同一轮中后续绑定的邮箱或手机号会写入同一条记录的副身份字段,保存最近一次状态:成功/失败/停止)
- 账号运行历史本地同步 helper 地址
@@ -762,9 +765,34 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
- Kiro 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
- Kiro 自动运行走通用 linear node runner,但执行器、运行态、页面驱动和上传器全部独立在 `background/kiro/*``content/kiro/*`
- Kiro 运行态统一落在 `kiroRuntime` 命名空间,不再散落为多个平铺字段
- Kiro 注册验证码与桌面授权验证码只在 runner 内计算业务时间窗,具体 AWS 邮件匹配规则由 `flows/kiro/mail-rules.js` 提供,provider 分派统一走 `background/flow-mail-polling.js`
- `kiro.rs` 上传固定发送 BuilderId profileArn `arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX`machineId 固定按 `sha256("KotlinNativeAPI/" + refreshToken)` 生成
- Kiro 注册页覆盖 `app.kiro.dev` 与 AWS Builder ID 页面;桌面授权页只绑定 AWS authorize 页面,二者由后台按 source 动态注入,避免同域 AWS 页面被错误归到另一条 Kiro 子链路
## 6.3 Grok Flow 链路
Grok flow 的目标是完成 Grok / xAI 注册页自动化,并把本轮提取到的 `sso` Cookie 上传到 `webchat2api` 的远程账号注入接口。它不是 OpenAI 目标来源,也不是公开贡献 / 发布 flow。
链路如下:
1. sidepanel 把当前 flow 切到 `grok`target 固定为 `webchat2api`
2. 步骤 1 打开 Grok / xAI 注册入口,并清理本轮注册页运行态
3. 步骤 2 通过共享邮箱服务生成或复用注册邮箱,并提交到 xAI 登录/注册页
4. 步骤 3 调用 `background/flow-mail-polling.js`,由 `flows/grok/mail-rules.js` 构造 xAI / Grok 验证码规则并轮询邮箱验证码,再交给 Grok 内容脚本提交
5. 步骤 4 填写资料和密码,推进注册完成页状态
6. 步骤 5 先通过 `chrome.cookies.get``x.ai / grok.com / accounts.x.ai` 查找 `sso` Cookie,必要时 fallback 到 Grok 内容脚本读取 `document.cookie`;提取到的当前值保存到 `runtimeState.flowState.grok.sso` 并投影到 `grokSsoCookie / grokSsoCookies`,其中 `grokSsoCookies` 只保留本轮当前 Cookie,不再累积历史列表
7. 步骤 6 由 `flows/grok/background/publisher-webchat2api.js` 读取 `settingsState.flows.grok.targets.webchat2api.baseUrl / apiKey` 和当前 SSO,向 `${origin}/api/remote-account/inject` 发送 Bearer 管理密钥请求,payload 使用 `accounts[{ token, provider:'grok', type:'sso' }] + strategy:'merge'`
8. 上传结果保存到 `runtimeState.flowState.grok.upload` 并投影到 `grokWebchat2ApiUploadStatus / grokWebchat2ApiUploadedAt / grokWebchat2ApiUploadMessage / grokWebchat2ApiTargetUrl`;缺 URL、缺管理密钥或缺 SSO 都必须报错,不允许静默跳过
9. sidepanel 只展示掩码后的 SSO 值与上传状态;复制按钮仅用于排障,不提供本地文件导出;清空动作必须发 `CLEAR_GROK_SSO_COOKIES` 给后台,由 `flows/grok/background/state.js` 构造 patch,并同步清空 upload 状态,不能在 sidepanel 直接写 `chrome.storage`
这条链路的关键边界是:
- Grok 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
- Grok 自动运行走通用 linear node runner,执行器、运行态、页面驱动、邮件规则和 `webchat2api` 上传器全部放在 `flows/grok/*`
- Grok 内容脚本不能全局 patch 浏览器原型;点击事件需要的坐标只能在局部事件参数里补齐
- Grok SSO 上传是 Grok flow 自己的收尾节点,`flows/grok/index.js``publicationTargets` 保持为空,`supportsAccountContribution` 保持为 `false`
- `shared/contribution-registry.js` 的默认发布贡献校验只覆盖声明了发布目标或支持贡献的 flow;Grok 不应被自动纳入贡献 adapter 缺失检查。如果未来要把 Grok 改成正式发布 flow,必须先新增明确的 publication target 与 contribution adapter,再打开对应能力
## 7. 邮箱与 provider 链路
### 7.1 共享邮箱生成补充
@@ -840,8 +868,11 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
- `background/registration-email-state.js`
统一维护当前注册邮箱、上一份比较基线邮箱、来源与更新时间,并提供 Duck 生成前解析比较基线、Step 8 清空当前邮箱但保留历史基线,以及 Step 8 `add-email` 写入邮箱时保留手机号身份的共享工具。
- `background/mail-rule-registry.js` + `flows/openai/mail-rules.js`
统一构造 Step 4 / Step 8 的验证码规则 payload,区分注册验证码与登录/绑定验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到同一层。
- `background/mail-rule-registry.js` + `flows/*/mail-rules.js`
统一构造各 flow 的验证码规则 payload,区分 OpenAI 注册/登录验证码、Kiro 注册/桌面授权验证码与 Grok 注册验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到 flow mail rules 层。
- `background/flow-mail-polling.js`
统一执行 flow 邮箱验证码轮询调度:读取当前邮箱配置,调用 `mailRuleRegistry.buildVerificationPollPayloadForNode`,分派 Hotmail / LuckMail / Cloudflare Temp Email / Cloud Mail / YYDS Mail 这类 API provider,并为 iCloud、2925 和普通网页邮箱准备会话与内容脚本轮询。Kiro 与后续新 flow 不应在各自 runner 内复制 provider 分派逻辑。
- `background/generated-email-helpers.js`
在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入;Duck 生成前会按“侧栏当前可见邮箱 -> `registrationEmailState.current` -> `registrationEmailState.previous`”的顺序解析比较基线。
@@ -1131,6 +1162,7 @@ Hide My Email 获取与管理链路:
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
- fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口
- Grok fresh-attempt reset 会清理注册页 runtime、当前 SSO 与 upload 状态,避免下一轮误上传上一轮旧 SSO
5. 执行 `runAutoSequenceFromStep`
- 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog
- 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点
+3
View File
@@ -171,6 +171,7 @@
- `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod`
- 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod`
- flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。
- flow 私有远程上传、发布型 flow、贡献型 flow 必须显式区分:只有声明了 `publicationTargets` 或明确支持贡献能力的 flow 才能进入发布/贡献 adapter 校验;例如 Grok / `webchat2api` SSO Cookie 上传属于 Grok flow 自己的收尾节点,不是 publication target,也不能因为有 target 就默认接入贡献模式。
### 1.7 日志步骤号原则
@@ -285,6 +286,7 @@
- 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js`
- 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js`
- 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败
- 如果新增 flow 只是私有收尾产物,例如 Cookie、JSON、会话文件、手动复制值,或只上传到该 flow 自己的专属远端服务,必须保持 `publicationTargets` 为空并关闭 `supportsAccountContribution`;如果后续产品决定把它纳入正式发布 / 贡献体系,必须先设计 publication target、贡献 adapter、敏感字段脱敏和测试,再打开能力开关。
### 3.4.1 iCloud Hide My Email 维护补充
@@ -495,6 +497,7 @@ npm test
15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交?
16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态?
17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId`
18. 如果新增或调整 flow target,我有没有确认它到底是 flow 私有收尾目标、正式发布目标还是贡献 flow,并同步检查 `publicationTargets / supportsAccountContribution / contributionAdapterIds / shared/contribution-registry.js`
## 9. 完成标准
+23 -8
View File
@@ -21,7 +21,7 @@
- `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/``.github/``_metadata/``.vscode/` 等目录。
- `LICENSE`:项目许可证文件。
- `README.md`:面向使用者的精简说明文档,主要介绍项目用途、功能范围、快速开始与文档入口;不再承载过多技术实现细节。
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled` `stepExecutionRangeByFlow``kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路。
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,OAuth 后链 5 分钟总预算只由 `openai + cpa` target capability 自动启用,其他来源与 flow 不启用该预算;`openai` flow 继续承接 `stepExecutionRangeByFlow``kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路,`grok` flow 走独立的“注册页 1-4 步 -> SSO Cookie 提取 5 步 -> `webchat2api` 上传 6 步”链路。
- `cloudmail-utils.js`Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
- `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
- `gopay-utils.js`GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。
@@ -59,12 +59,16 @@
- `flows/kiro/background/desktop-client.js`:Kiro 桌面授权协议层,负责桌面 OIDC client 注册、PKCE 参数生成、授权地址组装、callback 参数校验和授权码换 token。
- `flows/kiro/background/desktop-authorize-runner.js`:Kiro 桌面授权执行器,负责步骤 7~8 的标签页打开、授权页轮询、localhost callback 捕获,以及桌面授权完成后的凭据落库。
- `flows/kiro/background/publisher-kiro-rs.js`Kiro 发布器,负责 `kiro.rs` 地址归一化、连通性探测、BuilderId profileArn 固定映射、machineId 计算、上传 payload 构建与最终凭据上传。
- `flows/grok/background/state.js`:Grok 独立运行态模型与状态工具,负责 `runtimeState.flowState.grok.session / register / sso / upload` 的默认值、归一化、兼容字段投影、节点完成回写、下游重置和 fresh-attempt keep-state 构建。
- `flows/grok/background/register-runner.js`:Grok 注册页执行器,负责步骤 1~5 的编排、Grok/xAI 注册标签页管理、邮箱提交、验证码轮询提交、资料/密码提交、当前 SSO Cookie 提取与账号记录收尾。
- `flows/grok/background/publisher-webchat2api.js`Grok 专属 `webchat2api` 上传器,负责地址归一化到 `/api/remote-account/inject`、Bearer 管理密钥请求头、SSO payload 构建、成功/失败响应解析与 Grok upload 运行态回写。
- `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。
- `background/ip-proxy-provider-711proxy.js`711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `background/mail-rule-registry.js`flow-aware 邮件规则注册表,负责按 `activeFlowId` 选择验证码规则构造器,并统一输出注册/登录验证码轮询 payload。
- `background/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/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`,同步维护注册邮箱运行态;手动 `EXECUTE_NODE` 会先按 `stepExecutionRangeByFlow` 校验当前节点是否允许执行。
- `background/registration-email-state.js`:注册邮箱运行态共享模块,负责维护 `registrationEmailState = { current, previous, source, updatedAt }`,并提供“当前邮箱清空时保留上一比较基线”“Duck 生成前解析比较基线”“Step 8 `add-email` 写入邮箱时按需保留手机号身份”的统一工具。
- `core/flow-kernel/runtime-state.js`:运行态视图与 patch 构造模块,负责把会话字段归一到 `runtimeState / sharedState / serviceState / flowState` 结构,并维护 flow-aware 的 session patch、节点状态默认值与兼容视图输出。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
@@ -80,7 +84,7 @@
## `background/steps/`
- `flows/openai/background/steps/wait-registration-success.js`:步骤 6 实现,负责注册资料提交后等待 20 秒,让注册成功状态和页面跳转稳定;默认不清理 cookies,只有侧栏第六步 `清 Cookies` 开关开启时才会在等待结束后清理 ChatGPT / OpenAI 相关 cookies。
- `flows/openai/background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。
- `flows/openai/background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算,只有 `openai + cpa` 来源启用 5 分钟后链预算,其他来源和 flow 仅保留本地回调等待超时。
- `flows/openai/background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 PayPal / GoPay checkout session,或在 GPC 模式下读取 accessToken 并通过 `https://gpc.qlhazycoder.top/api/gp/tasks` 创建队列任务,记录 checkout / GPC task 运行态。
- `flows/openai/background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用 OpenAI 专属手机号验证码流程;手机号注册登录后若进入 `add-email`,会先按共享邮箱状态持久化规则生成/解析邮箱并提交绑定,在不丢失当前手机号身份的前提下再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录;命中 `email_in_use` 时会仅清空当前邮箱并保留上一轮比较基线,再在当前 Step 8 内重开 `add-email` 获取新邮箱,`max_check_attempts` 也只重启当前步骤恢复链。
- `flows/openai/background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
@@ -152,6 +156,11 @@
## `flows/`
- `flows/openai/mail-rules.js`OpenAI flow 的验证码邮件规则定义,负责按注册/登录节点输出 code pattern、关键词、目标邮箱提示、2925 receive 弱匹配与轮询参数。
- `flows/kiro/mail-rules.js`Kiro flow 的 AWS Builder ID 邮件规则定义,负责按注册验证码节点与桌面授权验证码节点输出 AWS 发件人、主题、关键词、code pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。
- `flows/grok/index.js`Grok flow 定义,声明 `webchat2api` target、Grok 注册页 runtime source、Grok 专属设置分组、能力边界与 flow 私有远程上传配置;该 flow 当前没有 publication target,也不支持贡献 adapter。
- `flows/grok/workflow.js`Grok workflow 定义,固定输出打开注册页、提交邮箱、提交验证码、提交资料、提取 SSO Cookie、上传 SSO 到 `webchat2api` 六个节点。
- `flows/grok/mail-rules.js`Grok flow 的 xAI / Grok 验证码邮件规则定义,负责输出发件人、主题、关键词、`ABC-123` 与 6 位码 pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。
- `flows/grok/content/register-page.js`:Grok 注册页内容脚本,负责五个 Grok 节点的 DOM 操作、页面状态读取与局部点击事件坐标补齐,不修改全局浏览器原型。
## `icons/`
@@ -178,9 +187,10 @@
## `shared/`
- `core/flow-kernel/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界、步骤范围 UI 作用域,以及 Plus 模式下 `账号接入策略` 的可选项、可编辑性与最终生效值。
- `core/flow-kernel/flow-registry.js`flow/source/settings group 总注册表,定义 `openai / kiro` 套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。
- `core/flow-kernel/flow-registry.js`flow/source/settings group 总注册表,定义 `openai / kiro / grok` 套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。
- `shared/kiro-timeouts.js`:Kiro 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。
- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*``settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro 的 `flows.kiro.targetId / targets``stepExecutionRangeByFlow`
- `shared/contribution-registry.js`:贡献 adapter 与教程入口注册表,负责 OpenAI / Kiro 贡献 adapter、教程入口和发布贡献 flow 校验;默认发布校验只覆盖声明了 publication target 或支持贡献的 flowGrok 这类 flow 私有远程上传链路不自动纳入贡献 adapter 缺失检查
- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*``settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro/Grok 的 `flows.<flowId>.targetId / targets``stepExecutionRangeByFlow`
- `core/flow-kernel/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。
## `sidepanel/`
@@ -207,7 +217,7 @@
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱`
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;当邮箱服务或邮箱生成器为 Cloud Mail 时,额外显示 API 地址、管理员账号、接收邮箱和生成域名配置行;当邮箱服务为 YYDS Mail 时,额外显示 API Key 与 Base URL 配置行,并隐藏普通邮箱生成/转发邮箱相关配置;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关,并在 `Plus 支付` 之上新增 `账号接入策略` 下拉;该下拉只显示 `OAuth / 使用会话 JSON 导入` 两种接入方式,会话导入目标由当前来源自动决定,不支持的来源会直接禁用并显示“当前来源仅支持 OAuth”;PayPal 账号下拉框继续使用公共表单弹窗添加账号;GPC Plus 配置额外提供只读 API 地址、API Key、专用手机号、OTP 渠道、本地 OTP helper 开关、helper URL 与 PIN,并提供 `购买卡密``转换 API Key` 入口;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算;注册邮箱下方新增 `执行范围` 行,目前只在 codex/openai flow 显示,用于配置允许执行的起止步骤。
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关,并在 `Plus 支付` 之上新增 `账号接入策略` 下拉;该下拉只显示 `OAuth / 使用会话 JSON 导入` 两种接入方式,会话导入目标由当前来源自动决定,不支持的来源会直接禁用并显示“当前来源仅支持 OAuth”;PayPal 账号下拉框继续使用公共表单弹窗添加账号;GPC Plus 配置额外提供只读 API 地址、API Key、专用手机号、OTP 渠道、本地 OTP helper 开关、helper URL 与 PIN,并提供 `购买卡密``转换 API Key` 入口;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;注册邮箱下方新增 `执行范围` 行,目前只在 codex/openai flow 显示,用于配置允许执行的起止步骤。
- `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
@@ -221,7 +231,7 @@
会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只
要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;
Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;IP 代理配置保存、服务配置快照、开关应用和运行态回显由这里接入消息路由,具体解析/状态渲染下沉到 `sidepanel/ip-proxy-panel.js`;侧边栏初始化与点击“自动”前会刷新一次贡献站
公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示;当前会保存、回显并热更新 `oauthFlowTimeoutEnabled` 设置;日志渲染只读取结构化 `entry.step` 生成步骤标签,不再正则解析日志正文;注册手机号输入框只同步运行态身份,不写入持久配置。
公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示;日志渲染只读取结构化 `entry.step` 生成步骤标签,不再正则解析日志正文;注册手机号输入框只同步运行态身份,不写入持久配置。
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Ultra` / 历史 `Pro` / legacy `v` 版本族排序、缓存读取与版本展示。
- 补充:`sidepanel/sidepanel.html``sidepanel/sidepanel.js` 当前已改为 flow-aware 侧边栏主界面;顶部提供 `flow/target` 双层选择,`openai` flow 继续把 `panelMode` 归一为 integration target`kiro` flow 则使用独立 `targetId``kiro.rs URL / API Key` 专属字段,并只显示 Kiro 运行状态与共享邮箱/IP 代理配置。
@@ -270,7 +280,10 @@
- `tests/background-mail2925-relogin-wait.test.js`:测试 2925 强制重登入口 URL、长登录超时与前后固定等待窗口。
- `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。
- `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI mail rules 模块接入,并覆盖验证码轮询 payload 构造
- `tests/background-flow-mail-polling-module.test.js`:测试 flow-aware 邮件轮询调度层对 API provider、网页邮箱 provider、2925 会话准备和 2925 上限恢复的共享分派行为
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI / Kiro mail rules 模块接入,并覆盖各 flow 验证码轮询 payload 构造。
- `tests/background-grok-state-module.test.js`:测试 Grok 运行态 helper 的默认值、兼容字段投影、节点完成回写、下游重置、fresh-attempt keep-state 与 SSO/upload 清理规则。
- `tests/background-grok-publisher-webchat2api.test.js`:测试 Grok `webchat2api` 上传器的地址归一化、必填配置校验、SSO payload、响应处理、运行态回写和敏感值日志保护。
- `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
- `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。
@@ -358,6 +371,7 @@
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
- `tests/contribution-registry.test.js`:测试贡献 adapter 注册、教程入口解析、默认发布贡献 flow 校验,以及 Grok 私有 `webchat2api` 上传 flow 不被自动纳入发布贡献检查。
- `tests/flow-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。
- `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。
- `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。
@@ -365,6 +379,7 @@
- `tests/sidepanel-contribution-mode-flow-scope.test.js`:测试贡献模式只在 `openai` flow 内生效,不会污染 Kiro flow 的来源与面板作用域。
- `tests/sidepanel-contribution-update-hint.test.js`:测试顶部贡献更新轻提示在公告、教程和托管 auto-run 提醒并存时的文案合成。
- `tests/sidepanel-flow-source-registry.test.js`:测试 sidepanel 的 flow/source 选择器、Kiro 来源切换与显隐分组接线。
- `tests/grok-runner.test.js`:测试 Grok 注册页执行器对邮箱提交、验证码轮询、资料提交、SSO Cookie 提取、账号记录收尾和完成状态持久化的关键链路。
- `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先冻结当前轮数并刷新贡献内容更新摘要,且刷新失败或启动前状态回灌不会阻塞或改写自动流程启动目标。
- `tests/sidepanel-auto-run-risk-warning.test.js`:测试自动运行在高轮数且未配置节点轮询时会弹出风险提醒。
- `tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js`:测试 Cloudflare Temp Email 独立区域、随机子域名开关、域名列表恢复与提示文案联动。